Compare commits
39 Commits
notebook--
...
navigate-t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c69e7499af | ||
|
|
a78f3cfea2 | ||
|
|
0acd556106 | ||
|
|
2f7a62780a | ||
|
|
f1e6b144e8 | ||
|
|
10a4760f90 | ||
|
|
cdd07fdf29 | ||
|
|
8a3fb890b0 | ||
|
|
30af8d0a81 | ||
|
|
ceb7fc2cb2 | ||
|
|
d8694510b5 | ||
|
|
ec7ce41324 | ||
|
|
b2921bd3fd | ||
|
|
64756fa96f | ||
|
|
ff6844300e | ||
|
|
b1b6401ce7 | ||
|
|
07ba7c8c44 | ||
|
|
6516249302 | ||
|
|
fc8218d728 | ||
|
|
a8d56877ee | ||
|
|
dd0de3cfa9 | ||
|
|
198c36811e | ||
|
|
133704a419 | ||
|
|
d1302a7a08 | ||
|
|
2b28b5969f | ||
|
|
bc941bfc97 | ||
|
|
72a9429ef6 | ||
|
|
f020291039 | ||
|
|
4b3a2a33a8 | ||
|
|
1f257f4704 | ||
|
|
f517050548 | ||
|
|
535ba75bc7 | ||
|
|
ee280b0d05 | ||
|
|
a43793493a | ||
|
|
3b3c379852 | ||
|
|
147f407b7a | ||
|
|
822f42e8fe | ||
|
|
7e097d529a | ||
|
|
65f76e6f4d |
5
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
@@ -298,8 +298,9 @@ jobs:
|
||||
env:
|
||||
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
|
||||
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
||||
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
|
||||
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
|
||||
APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }}
|
||||
APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }}
|
||||
APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }}
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
|
||||
25
.github/workflows/community_update_all_top_ranking_issues.yml
vendored
Normal file
25
.github/workflows/community_update_all_top_ranking_issues.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: Update All Top Ranking Issues
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 */12 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
update_top_ranking_issues:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'zed-industries/zed'
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3
|
||||
with:
|
||||
version: "latest"
|
||||
enable-cache: true
|
||||
cache-dependency-glob: "script/update_top_ranking_issues/pyproject.toml"
|
||||
- name: Install Python 3.13
|
||||
run: uv python install 3.13
|
||||
- name: Install dependencies
|
||||
run: uv sync --project script/update_top_ranking_issues -p 3.13
|
||||
- name: Run script
|
||||
run: uv run --project script/update_top_ranking_issues script/update_top_ranking_issues/main.py --github-token ${{ secrets.GITHUB_TOKEN }} --issue-reference-number 5393
|
||||
25
.github/workflows/community_update_weekly_top_ranking_issues.yml
vendored
Normal file
25
.github/workflows/community_update_weekly_top_ranking_issues.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: Update Weekly Top Ranking Issues
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 15 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
update_top_ranking_issues:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'zed-industries/zed'
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3
|
||||
with:
|
||||
version: "latest"
|
||||
enable-cache: true
|
||||
cache-dependency-glob: "script/update_top_ranking_issues/pyproject.toml"
|
||||
- name: Install Python 3.13
|
||||
run: uv python install 3.13
|
||||
- name: Install dependencies
|
||||
run: uv sync --project script/update_top_ranking_issues -p 3.13
|
||||
- name: Run script
|
||||
run: uv run --project script/update_top_ranking_issues script/update_top_ranking_issues/main.py --github-token ${{ secrets.GITHUB_TOKEN }} --issue-reference-number 6952 --query-day-interval 7
|
||||
5
.github/workflows/release_nightly.yml
vendored
5
.github/workflows/release_nightly.yml
vendored
@@ -62,8 +62,9 @@ jobs:
|
||||
env:
|
||||
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
|
||||
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
||||
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
|
||||
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
|
||||
APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }}
|
||||
APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }}
|
||||
APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }}
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
|
||||
59
Cargo.lock
generated
59
Cargo.lock
generated
@@ -1269,6 +1269,30 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-bedrockruntime"
|
||||
version = "1.74.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6938541d1948a543bca23303fec4cff9c36bf0e63b8fa3ae1b337bcb9d5b81af"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-eventstream",
|
||||
"aws-smithy-http",
|
||||
"aws-smithy-json",
|
||||
"aws-smithy-runtime",
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-types",
|
||||
"aws-types",
|
||||
"bytes 1.10.0",
|
||||
"fastrand 2.3.0",
|
||||
"http 0.2.12",
|
||||
"once_cell",
|
||||
"regex-lite",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-kinesis"
|
||||
version = "1.61.0"
|
||||
@@ -1598,6 +1622,17 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws_http_client"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-types",
|
||||
"futures 0.3.31",
|
||||
"http_client",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.6.20"
|
||||
@@ -1727,6 +1762,22 @@ version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
||||
|
||||
[[package]]
|
||||
name = "bedrock"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"aws-sdk-bedrockruntime",
|
||||
"aws-smithy-types",
|
||||
"futures 0.3.31",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bigdecimal"
|
||||
version = "0.4.7"
|
||||
@@ -3112,7 +3163,9 @@ dependencies = [
|
||||
"clock",
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
"ctor",
|
||||
"editor",
|
||||
"env_logger 0.11.6",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
@@ -3120,6 +3173,7 @@ dependencies = [
|
||||
"indoc",
|
||||
"inline_completion",
|
||||
"language",
|
||||
"log",
|
||||
"lsp",
|
||||
"menu",
|
||||
"node_runtime",
|
||||
@@ -5295,6 +5349,7 @@ dependencies = [
|
||||
"pretty_assertions",
|
||||
"regex",
|
||||
"rope",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"smol",
|
||||
@@ -6960,16 +7015,12 @@ dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
"collections",
|
||||
"deepseek",
|
||||
"futures 0.3.31",
|
||||
"google_ai",
|
||||
"gpui",
|
||||
"http_client",
|
||||
"image",
|
||||
"lmstudio",
|
||||
"log",
|
||||
"mistral",
|
||||
"ollama",
|
||||
"open_ai",
|
||||
"parking_lot",
|
||||
"proto",
|
||||
|
||||
@@ -15,6 +15,8 @@ members = [
|
||||
"crates/audio",
|
||||
"crates/auto_update",
|
||||
"crates/auto_update_ui",
|
||||
"crates/aws_http_client",
|
||||
"crates/bedrock",
|
||||
"crates/breadcrumbs",
|
||||
"crates/buffer_diff",
|
||||
"crates/call",
|
||||
@@ -218,6 +220,8 @@ assistant_tools = { path = "crates/assistant_tools" }
|
||||
audio = { path = "crates/audio" }
|
||||
auto_update = { path = "crates/auto_update" }
|
||||
auto_update_ui = { path = "crates/auto_update_ui" }
|
||||
aws_http_client = { path = "crates/aws_http_client" }
|
||||
bedrock = { path = "crates/bedrock" }
|
||||
breadcrumbs = { path = "crates/breadcrumbs" }
|
||||
call = { path = "crates/call" }
|
||||
channel = { path = "crates/channel" }
|
||||
@@ -382,6 +386,11 @@ async-trait = "0.1"
|
||||
async-tungstenite = "0.28"
|
||||
async-watch = "0.3.1"
|
||||
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
|
||||
aws-config = { version = "1.5.16", features = ["behavior-version-latest"] }
|
||||
aws-credential-types = { version = "1.2.1", features = ["hardcoded-credentials"] }
|
||||
aws-sdk-bedrockruntime = { version = "1.73.0", features = ["behavior-version-latest"] }
|
||||
aws-smithy-runtime-api = { version = "1.7.3", features = ["http-1x", "client"] }
|
||||
aws-smithy-types = { version = "1.2.13", features = ["http-body-1-x"] }
|
||||
base64 = "0.22"
|
||||
bitflags = "2.6.0"
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "b16f5c7bd873c7126f48c82c39e7ae64602ae74f" }
|
||||
|
||||
@@ -184,9 +184,9 @@
|
||||
"ctrl-alt-/": "assistant::ToggleModelSelector",
|
||||
"ctrl-k h": "assistant::DeployHistory",
|
||||
"ctrl-k l": "assistant::DeployPromptLibrary",
|
||||
"new": "assistant::NewContext",
|
||||
"ctrl-t": "assistant::NewContext",
|
||||
"ctrl-n": "assistant::NewContext"
|
||||
"new": "assistant::NewChat",
|
||||
"ctrl-t": "assistant::NewChat",
|
||||
"ctrl-n": "assistant::NewChat"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -368,7 +368,12 @@
|
||||
"ctrl-\\": "pane::SplitRight",
|
||||
"ctrl-k v": "markdown::OpenPreviewToTheSide",
|
||||
"ctrl-shift-v": "markdown::OpenPreview",
|
||||
"ctrl-alt-shift-c": "editor::DisplayCursorNames"
|
||||
"ctrl-alt-shift-c": "editor::DisplayCursorNames",
|
||||
"ctrl-alt-y": "git::ToggleStaged",
|
||||
"alt-y": "git::StageAndNext",
|
||||
"alt-shift-y": "git::UnstageAndNext",
|
||||
"alt-.": "editor::GoToHunk",
|
||||
"alt-,": "editor::GoToPrevHunk"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -705,12 +710,6 @@
|
||||
"space": "project_panel::Open"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitPanel && !CommitEditor",
|
||||
"bindings": {
|
||||
"escape": "git_panel::Close"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitPanel && ChangesList",
|
||||
"bindings": {
|
||||
@@ -722,19 +721,36 @@
|
||||
"ctrl-shift-space": "git::UnstageAll",
|
||||
"tab": "git_panel::FocusEditor",
|
||||
"shift-tab": "git_panel::FocusEditor",
|
||||
"escape": "git_panel::ToggleFocus"
|
||||
"escape": "git_panel::ToggleFocus",
|
||||
"ctrl-enter": "git::Commit",
|
||||
"alt-enter": "menu::SecondaryConfirm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitCommit > Editor",
|
||||
"bindings": {
|
||||
"enter": "editor::Newline",
|
||||
"ctrl-enter": "git::Commit"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitPanel > Editor",
|
||||
"bindings": {
|
||||
"escape": "git_panel::FocusChanges",
|
||||
"ctrl-enter": "git::Commit",
|
||||
"tab": "git_panel::FocusChanges",
|
||||
"shift-tab": "git_panel::FocusChanges",
|
||||
"ctrl-enter": "git::Commit",
|
||||
"alt-up": "git_panel::FocusChanges"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitCommit > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "editor::Newline",
|
||||
"ctrl-enter": "git::Commit"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "CollabPanel && not_editing",
|
||||
"bindings": {
|
||||
@@ -813,6 +829,7 @@
|
||||
"pagedown": ["terminal::SendKeystroke", "pagedown"],
|
||||
"escape": ["terminal::SendKeystroke", "escape"],
|
||||
"enter": ["terminal::SendKeystroke", "enter"],
|
||||
"ctrl-b": ["terminal::SendKeystroke", "ctrl-b"],
|
||||
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],
|
||||
"shift-pageup": "terminal::ScrollPageUp",
|
||||
"shift-pagedown": "terminal::ScrollPageDown",
|
||||
|
||||
@@ -157,7 +157,8 @@
|
||||
"cmd->": "assistant::QuoteSelection",
|
||||
"cmd-<": "assistant::InsertIntoEditor",
|
||||
"cmd-alt-e": "editor::SelectEnclosingSymbol",
|
||||
"alt-enter": "editor::OpenSelectionsInMultibuffer"
|
||||
"alt-enter": "editor::OpenSelectionsInMultibuffer",
|
||||
"cmd-g": "git::Commit"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -211,8 +212,8 @@
|
||||
"cmd-alt-/": "assistant::ToggleModelSelector",
|
||||
"cmd-k h": "assistant::DeployHistory",
|
||||
"cmd-k l": "assistant::DeployPromptLibrary",
|
||||
"cmd-t": "assistant::NewContext",
|
||||
"cmd-n": "assistant::NewContext"
|
||||
"cmd-t": "assistant::NewChat",
|
||||
"cmd-n": "assistant::NewChat"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -742,14 +743,6 @@
|
||||
"escape": "git_panel::ToggleFocus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitCommit > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "editor::Newline",
|
||||
"cmd-enter": "git::Commit"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitPanel > Editor",
|
||||
"use_key_equivalents": true,
|
||||
@@ -761,6 +754,14 @@
|
||||
"alt-up": "git_panel::FocusChanges"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitCommit > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "editor::Newline",
|
||||
"cmd-enter": "git::Commit"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "CollabPanel && not_editing",
|
||||
"use_key_equivalents": true,
|
||||
|
||||
@@ -48,6 +48,8 @@
|
||||
"ctrl-_": "editor::Undo", // undo
|
||||
"ctrl-/": "editor::Undo", // undo
|
||||
"ctrl-x u": "editor::Undo", // undo
|
||||
"alt-{": "editor::MoveToStartOfParagraph", // backward-paragraph
|
||||
"alt-}": "editor::MoveToEndOfParagraph", // forward-paragraph
|
||||
"ctrl-v": "editor::MovePageDown", // scroll-up
|
||||
"alt-v": "editor::MovePageUp", // scroll-down
|
||||
"ctrl-x [": "editor::MoveToBeginning", // beginning-of-buffer
|
||||
|
||||
@@ -48,6 +48,8 @@
|
||||
"ctrl-_": "editor::Undo", // undo
|
||||
"ctrl-/": "editor::Undo", // undo
|
||||
"ctrl-x u": "editor::Undo", // undo
|
||||
"alt-{": "editor::MoveToStartOfParagraph", // backward-paragraph
|
||||
"alt-}": "editor::MoveToEndOfParagraph", // forward-paragraph
|
||||
"ctrl-v": "editor::MovePageDown", // scroll-up
|
||||
"alt-v": "editor::MovePageUp", // scroll-down
|
||||
"ctrl-x [": "editor::MoveToBeginning", // beginning-of-buffer
|
||||
|
||||
@@ -581,7 +581,7 @@
|
||||
// The provider to use.
|
||||
"provider": "zed.dev",
|
||||
// The model to use.
|
||||
"model": "claude-3-5-sonnet"
|
||||
"model": "claude-3-5-sonnet-latest"
|
||||
}
|
||||
},
|
||||
// The settings for slash commands.
|
||||
@@ -1093,6 +1093,7 @@
|
||||
"tab_size": 2
|
||||
},
|
||||
"Diff": {
|
||||
"show_edit_predictions": false,
|
||||
"remove_trailing_whitespace_on_save": false,
|
||||
"ensure_final_newline_on_save": false
|
||||
},
|
||||
|
||||
@@ -379,7 +379,7 @@
|
||||
"font_weight": null
|
||||
},
|
||||
"variable": {
|
||||
"color": "#83a598ff",
|
||||
"color": "#ebdbb2ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
@@ -767,7 +767,7 @@
|
||||
"font_weight": null
|
||||
},
|
||||
"variable": {
|
||||
"color": "#83a598ff",
|
||||
"color": "#ebdbb2ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
@@ -1155,7 +1155,7 @@
|
||||
"font_weight": null
|
||||
},
|
||||
"variable": {
|
||||
"color": "#83a598ff",
|
||||
"color": "#ebdbb2ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
@@ -1543,7 +1543,7 @@
|
||||
"font_weight": null
|
||||
},
|
||||
"variable": {
|
||||
"color": "#066578ff",
|
||||
"color": "#282828ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
@@ -1931,7 +1931,7 @@
|
||||
"font_weight": null
|
||||
},
|
||||
"variable": {
|
||||
"color": "#066578ff",
|
||||
"color": "#282828ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
@@ -2319,7 +2319,7 @@
|
||||
"font_weight": null
|
||||
},
|
||||
"variable": {
|
||||
"color": "#066578ff",
|
||||
"color": "#282828ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
|
||||
@@ -365,7 +365,7 @@
|
||||
"font_weight": null
|
||||
},
|
||||
"variable": {
|
||||
"color": "#dce0e5ff",
|
||||
"color": "#acb2beff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
|
||||
@@ -30,6 +30,8 @@ pub enum Model {
|
||||
#[default]
|
||||
#[serde(rename = "claude-3-5-sonnet", alias = "claude-3-5-sonnet-latest")]
|
||||
Claude3_5Sonnet,
|
||||
#[serde(rename = "claude-3-7-sonnet", alias = "claude-3-7-sonnet-latest")]
|
||||
Claude3_7Sonnet,
|
||||
#[serde(rename = "claude-3-5-haiku", alias = "claude-3-5-haiku-latest")]
|
||||
Claude3_5Haiku,
|
||||
#[serde(rename = "claude-3-opus", alias = "claude-3-opus-latest")]
|
||||
@@ -59,6 +61,8 @@ impl Model {
|
||||
pub fn from_id(id: &str) -> Result<Self> {
|
||||
if id.starts_with("claude-3-5-sonnet") {
|
||||
Ok(Self::Claude3_5Sonnet)
|
||||
} else if id.starts_with("claude-3-7-sonnet") {
|
||||
Ok(Self::Claude3_7Sonnet)
|
||||
} else if id.starts_with("claude-3-5-haiku") {
|
||||
Ok(Self::Claude3_5Haiku)
|
||||
} else if id.starts_with("claude-3-opus") {
|
||||
@@ -75,6 +79,7 @@ impl Model {
|
||||
pub fn id(&self) -> &str {
|
||||
match self {
|
||||
Model::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
|
||||
Model::Claude3_7Sonnet => "claude-3-7-sonnet-latest",
|
||||
Model::Claude3_5Haiku => "claude-3-5-haiku-latest",
|
||||
Model::Claude3Opus => "claude-3-opus-latest",
|
||||
Model::Claude3Sonnet => "claude-3-sonnet-20240229",
|
||||
@@ -85,6 +90,7 @@ impl Model {
|
||||
|
||||
pub fn display_name(&self) -> &str {
|
||||
match self {
|
||||
Self::Claude3_7Sonnet => "Claude 3.7 Sonnet",
|
||||
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
|
||||
Self::Claude3_5Haiku => "Claude 3.5 Haiku",
|
||||
Self::Claude3Opus => "Claude 3 Opus",
|
||||
@@ -98,13 +104,14 @@ impl Model {
|
||||
|
||||
pub fn cache_configuration(&self) -> Option<AnthropicModelCacheConfiguration> {
|
||||
match self {
|
||||
Self::Claude3_5Sonnet | Self::Claude3_5Haiku | Self::Claude3Haiku => {
|
||||
Some(AnthropicModelCacheConfiguration {
|
||||
min_total_token: 2_048,
|
||||
should_speculate: true,
|
||||
max_cache_anchors: 4,
|
||||
})
|
||||
}
|
||||
Self::Claude3_5Sonnet
|
||||
| Self::Claude3_5Haiku
|
||||
| Self::Claude3_7Sonnet
|
||||
| Self::Claude3Haiku => Some(AnthropicModelCacheConfiguration {
|
||||
min_total_token: 2_048,
|
||||
should_speculate: true,
|
||||
max_cache_anchors: 4,
|
||||
}),
|
||||
Self::Custom {
|
||||
cache_configuration,
|
||||
..
|
||||
@@ -117,6 +124,7 @@ impl Model {
|
||||
match self {
|
||||
Self::Claude3_5Sonnet
|
||||
| Self::Claude3_5Haiku
|
||||
| Self::Claude3_7Sonnet
|
||||
| Self::Claude3Opus
|
||||
| Self::Claude3Sonnet
|
||||
| Self::Claude3Haiku => 200_000,
|
||||
@@ -127,7 +135,7 @@ impl Model {
|
||||
pub fn max_output_tokens(&self) -> u32 {
|
||||
match self {
|
||||
Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3Haiku => 4_096,
|
||||
Self::Claude3_5Sonnet | Self::Claude3_5Haiku => 8_192,
|
||||
Self::Claude3_5Sonnet | Self::Claude3_7Sonnet | Self::Claude3_5Haiku => 8_192,
|
||||
Self::Custom {
|
||||
max_output_tokens, ..
|
||||
} => max_output_tokens.unwrap_or(4_096),
|
||||
@@ -137,6 +145,7 @@ impl Model {
|
||||
pub fn default_temperature(&self) -> f32 {
|
||||
match self {
|
||||
Self::Claude3_5Sonnet
|
||||
| Self::Claude3_7Sonnet
|
||||
| Self::Claude3_5Haiku
|
||||
| Self::Claude3Opus
|
||||
| Self::Claude3Sonnet
|
||||
|
||||
@@ -33,7 +33,7 @@ actions!(
|
||||
[
|
||||
InsertActivePrompt,
|
||||
DeployHistory,
|
||||
NewContext,
|
||||
NewChat,
|
||||
CycleNextInlineAssist,
|
||||
CyclePreviousInlineAssist
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::assistant_configuration::{ConfigurationView, ConfigurationViewEvent};
|
||||
use crate::{
|
||||
terminal_inline_assistant::TerminalInlineAssistant, DeployHistory, InlineAssistant, NewContext,
|
||||
terminal_inline_assistant::TerminalInlineAssistant, DeployHistory, InlineAssistant, NewChat,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_context_editor::{
|
||||
@@ -129,7 +129,7 @@ impl AssistantPanel {
|
||||
workspace.project().clone(),
|
||||
Default::default(),
|
||||
None,
|
||||
NewContext.boxed_clone(),
|
||||
NewChat.boxed_clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -228,12 +228,12 @@ impl AssistantPanel {
|
||||
IconButton::new("new-chat", IconName::Plus)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(cx.listener(|_, _, window, cx| {
|
||||
window.dispatch_action(NewContext.boxed_clone(), cx)
|
||||
window.dispatch_action(NewChat.boxed_clone(), cx)
|
||||
}))
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"New Chat",
|
||||
&NewContext,
|
||||
&NewChat,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
@@ -256,7 +256,7 @@ impl AssistantPanel {
|
||||
let focus_handle = _pane.focus_handle(cx);
|
||||
Some(ContextMenu::build(window, cx, move |menu, _, _| {
|
||||
menu.context(focus_handle.clone())
|
||||
.action("New Chat", Box::new(NewContext))
|
||||
.action("New Chat", Box::new(NewChat))
|
||||
.action("History", Box::new(DeployHistory))
|
||||
.action("Prompt Library", Box::new(DeployPromptLibrary))
|
||||
.action("Configure", Box::new(ShowConfiguration))
|
||||
@@ -760,7 +760,7 @@ impl AssistantPanel {
|
||||
|
||||
pub fn create_new_context(
|
||||
workspace: &mut Workspace,
|
||||
_: &NewContext,
|
||||
_: &NewChat,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
@@ -978,7 +978,7 @@ impl AssistantPanel {
|
||||
.active_provider()
|
||||
.map_or(true, |p| p.id() != provider.id())
|
||||
{
|
||||
if let Some(model) = provider.provided_models(cx).first().cloned() {
|
||||
if let Some(model) = provider.default_model(cx) {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
this.fs.clone(),
|
||||
cx,
|
||||
@@ -1206,7 +1206,7 @@ impl Render for AssistantPanel {
|
||||
v_flex()
|
||||
.key_context("AssistantPanel")
|
||||
.size_full()
|
||||
.on_action(cx.listener(|this, _: &NewContext, window, cx| {
|
||||
.on_action(cx.listener(|this, _: &NewChat, window, cx| {
|
||||
this.new_context(window, cx);
|
||||
}))
|
||||
.on_action(cx.listener(|this, _: &ShowConfiguration, window, cx| {
|
||||
|
||||
@@ -431,7 +431,7 @@ impl AssistantPanel {
|
||||
active_provider.id() != provider.id()
|
||||
})
|
||||
{
|
||||
if let Some(model) = provider.provided_models(cx).first().cloned() {
|
||||
if let Some(model) = provider.default_model(cx) {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
self.fs.clone(),
|
||||
cx,
|
||||
|
||||
@@ -13,7 +13,7 @@ use rope::Point;
|
||||
use settings::Settings;
|
||||
use std::time::Duration;
|
||||
use text::Bias;
|
||||
use theme::{get_ui_font_size, ThemeSettings};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
prelude::*, ButtonLike, KeyBinding, PopoverMenu, PopoverMenuHandle, Switch, TintColor, Tooltip,
|
||||
};
|
||||
@@ -369,7 +369,7 @@ impl Render for MessageEditor {
|
||||
.anchor(gpui::Corner::BottomLeft)
|
||||
.offset(gpui::Point {
|
||||
x: px(0.0),
|
||||
y: (-get_ui_font_size(cx) * 2) - px(4.0),
|
||||
y: (-ThemeSettings::get_global(cx).ui_font_size(cx) * 2) - px(4.0),
|
||||
})
|
||||
.with_handle(self.inline_context_picker_menu_handle.clone()),
|
||||
)
|
||||
|
||||
@@ -1234,8 +1234,8 @@ impl ContextEditor {
|
||||
.px_1()
|
||||
.mr_0p5()
|
||||
.border_1()
|
||||
.border_color(theme::color_alpha(colors.border_variant, 0.6))
|
||||
.bg(theme::color_alpha(colors.element_background, 0.6))
|
||||
.border_color(colors.border_variant.alpha(0.6))
|
||||
.bg(colors.element_background.alpha(0.6))
|
||||
.child("esc"),
|
||||
)
|
||||
.child("to cancel")
|
||||
|
||||
@@ -512,7 +512,7 @@ mod tests {
|
||||
AssistantSettings::get_global(cx).default_model,
|
||||
LanguageModelSelection {
|
||||
provider: "zed.dev".into(),
|
||||
model: "claude-3-5-sonnet".into(),
|
||||
model: "claude-3-5-sonnet-latest".into(),
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -17,7 +17,7 @@ pub enum Timezone {
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct FileToolInput {
|
||||
pub struct NowToolInput {
|
||||
/// The timezone to use for the datetime.
|
||||
timezone: Timezone,
|
||||
}
|
||||
@@ -34,7 +34,7 @@ impl Tool for NowTool {
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
let schema = schemars::schema_for!(FileToolInput);
|
||||
let schema = schemars::schema_for!(NowToolInput);
|
||||
serde_json::to_value(&schema).unwrap()
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ impl Tool for NowTool {
|
||||
_window: &mut Window,
|
||||
_cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
let input: FileToolInput = match serde_json::from_value(input) {
|
||||
let input: NowToolInput = match serde_json::from_value(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
};
|
||||
|
||||
22
crates/aws_http_client/Cargo.toml
Normal file
22
crates/aws_http_client/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "aws_http_client"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/aws_http_client.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
[dependencies]
|
||||
aws-smithy-runtime-api.workspace = true
|
||||
aws-smithy-types.workspace = true
|
||||
futures.workspace = true
|
||||
http_client.workspace = true
|
||||
tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }
|
||||
1
crates/aws_http_client/LICENSE-GPL
Symbolic link
1
crates/aws_http_client/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
118
crates/aws_http_client/src/aws_http_client.rs
Normal file
118
crates/aws_http_client/src/aws_http_client.rs
Normal file
@@ -0,0 +1,118 @@
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use aws_smithy_runtime_api::client::http::{
|
||||
HttpClient as AwsClient, HttpConnector as AwsConnector,
|
||||
HttpConnectorFuture as AwsConnectorFuture, HttpConnectorFuture, HttpConnectorSettings,
|
||||
SharedHttpConnector,
|
||||
};
|
||||
use aws_smithy_runtime_api::client::orchestrator::{HttpRequest as AwsHttpRequest, HttpResponse};
|
||||
use aws_smithy_runtime_api::client::result::ConnectorError;
|
||||
use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents;
|
||||
use aws_smithy_runtime_api::http::StatusCode;
|
||||
use aws_smithy_types::body::SdkBody;
|
||||
use futures::AsyncReadExt;
|
||||
use http_client::{AsyncBody, Inner};
|
||||
use http_client::{HttpClient, Request};
|
||||
use tokio::runtime::Handle;
|
||||
|
||||
struct AwsHttpConnector {
|
||||
client: Arc<dyn HttpClient>,
|
||||
handle: Handle,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for AwsHttpConnector {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("AwsHttpConnector").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl AwsConnector for AwsHttpConnector {
|
||||
fn call(&self, request: AwsHttpRequest) -> AwsConnectorFuture {
|
||||
let req = match request.try_into_http1x() {
|
||||
Ok(req) => req,
|
||||
Err(err) => {
|
||||
return HttpConnectorFuture::ready(Err(ConnectorError::other(err.into(), None)))
|
||||
}
|
||||
};
|
||||
|
||||
let (parts, body) = req.into_parts();
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.send(Request::from_parts(parts, convert_to_async_body(body)));
|
||||
|
||||
let handle = self.handle.clone();
|
||||
|
||||
HttpConnectorFuture::new(async move {
|
||||
let response = match response.await {
|
||||
Ok(response) => response,
|
||||
Err(err) => return Err(ConnectorError::other(err.into(), None)),
|
||||
};
|
||||
let (parts, body) = response.into_parts();
|
||||
let body = convert_to_sdk_body(body, handle).await;
|
||||
|
||||
Ok(HttpResponse::new(
|
||||
StatusCode::try_from(parts.status.as_u16()).unwrap(),
|
||||
body,
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AwsHttpClient {
|
||||
client: Arc<dyn HttpClient>,
|
||||
handler: Handle,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for AwsHttpClient {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("AwsHttpClient").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl AwsHttpClient {
|
||||
pub fn new(client: Arc<dyn HttpClient>, handle: Handle) -> Self {
|
||||
Self {
|
||||
client,
|
||||
handler: handle,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AwsClient for AwsHttpClient {
|
||||
fn http_connector(
|
||||
&self,
|
||||
_settings: &HttpConnectorSettings,
|
||||
_components: &RuntimeComponents,
|
||||
) -> SharedHttpConnector {
|
||||
SharedHttpConnector::new(AwsHttpConnector {
|
||||
client: self.client.clone(),
|
||||
handle: self.handler.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn convert_to_sdk_body(body: AsyncBody, handle: Handle) -> SdkBody {
|
||||
match body.0 {
|
||||
Inner::Empty => SdkBody::empty(),
|
||||
Inner::Bytes(bytes) => SdkBody::from(bytes.into_inner()),
|
||||
Inner::AsyncReader(mut reader) => {
|
||||
let buffer = handle.spawn(async move {
|
||||
let mut buffer = Vec::new();
|
||||
let _ = reader.read_to_end(&mut buffer).await;
|
||||
buffer
|
||||
});
|
||||
|
||||
SdkBody::from(buffer.await.unwrap_or_default())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn convert_to_async_body(body: SdkBody) -> AsyncBody {
|
||||
match body.bytes() {
|
||||
Some(bytes) => AsyncBody::from((*bytes).to_vec()),
|
||||
None => AsyncBody::empty(),
|
||||
}
|
||||
}
|
||||
28
crates/bedrock/Cargo.toml
Normal file
28
crates/bedrock/Cargo.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "bedrock"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/bedrock.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
schemars = ["dep:schemars"]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
aws-sdk-bedrockruntime = { workspace = true, features = ["behavior-version-latest"] }
|
||||
aws-smithy-types = {workspace = true}
|
||||
futures.workspace = true
|
||||
schemars = { workspace = true, optional = true }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
strum.workspace = true
|
||||
thiserror.workspace = true
|
||||
tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }
|
||||
1
crates/bedrock/LICENSE-GPL
Symbolic link
1
crates/bedrock/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
166
crates/bedrock/src/bedrock.rs
Normal file
166
crates/bedrock/src/bedrock.rs
Normal file
@@ -0,0 +1,166 @@
|
||||
mod models;
|
||||
|
||||
use std::pin::Pin;
|
||||
|
||||
use anyhow::{anyhow, Context, Error, Result};
|
||||
use aws_sdk_bedrockruntime as bedrock;
|
||||
pub use aws_sdk_bedrockruntime as bedrock_client;
|
||||
pub use aws_sdk_bedrockruntime::types::{
|
||||
ContentBlock as BedrockInnerContent, SpecificToolChoice as BedrockSpecificTool,
|
||||
ToolChoice as BedrockToolChoice, ToolInputSchema as BedrockToolInputSchema,
|
||||
ToolSpecification as BedrockTool,
|
||||
};
|
||||
use aws_smithy_types::{Document, Number as AwsNumber};
|
||||
pub use bedrock::operation::converse_stream::ConverseStreamInput as BedrockStreamingRequest;
|
||||
pub use bedrock::types::{
|
||||
ContentBlock as BedrockRequestContent, ConversationRole as BedrockRole,
|
||||
ConverseOutput as BedrockResponse, ConverseStreamOutput as BedrockStreamingResponse,
|
||||
Message as BedrockMessage, ResponseStream as BedrockResponseStream,
|
||||
};
|
||||
use futures::stream::{self, BoxStream, Stream};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Number, Value};
|
||||
use thiserror::Error;
|
||||
|
||||
pub use crate::models::*;
|
||||
|
||||
pub async fn complete(
|
||||
client: &bedrock::Client,
|
||||
request: Request,
|
||||
) -> Result<BedrockResponse, BedrockError> {
|
||||
let response = bedrock::Client::converse(client)
|
||||
.model_id(request.model.clone())
|
||||
.set_messages(request.messages.into())
|
||||
.send()
|
||||
.await
|
||||
.context("failed to send request to Bedrock");
|
||||
|
||||
match response {
|
||||
Ok(output) => output
|
||||
.output
|
||||
.ok_or_else(|| BedrockError::Other(anyhow!("no output"))),
|
||||
Err(err) => Err(BedrockError::Other(err)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn stream_completion(
|
||||
client: bedrock::Client,
|
||||
request: Request,
|
||||
handle: tokio::runtime::Handle,
|
||||
) -> Result<BoxStream<'static, Result<BedrockStreamingResponse, BedrockError>>, Error> {
|
||||
handle
|
||||
.spawn(async move {
|
||||
let response = bedrock::Client::converse_stream(&client)
|
||||
.model_id(request.model.clone())
|
||||
.set_messages(request.messages.into())
|
||||
.send()
|
||||
.await;
|
||||
|
||||
match response {
|
||||
Ok(output) => {
|
||||
let stream: Pin<
|
||||
Box<
|
||||
dyn Stream<Item = Result<BedrockStreamingResponse, BedrockError>>
|
||||
+ Send,
|
||||
>,
|
||||
> = Box::pin(stream::unfold(output.stream, |mut stream| async move {
|
||||
match stream.recv().await {
|
||||
Ok(Some(output)) => Some((Ok(output), stream)),
|
||||
Ok(None) => None,
|
||||
Err(err) => {
|
||||
Some((
|
||||
// TODO: Figure out how we can capture Throttling Exceptions
|
||||
Err(BedrockError::ClientError(anyhow!(
|
||||
"{:?}",
|
||||
aws_sdk_bedrockruntime::error::DisplayErrorContext(err)
|
||||
))),
|
||||
stream,
|
||||
))
|
||||
}
|
||||
}
|
||||
}));
|
||||
Ok(stream)
|
||||
}
|
||||
Err(err) => Err(anyhow!(
|
||||
"{:?}",
|
||||
aws_sdk_bedrockruntime::error::DisplayErrorContext(err)
|
||||
)),
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(|err| anyhow!("failed to spawn task: {err:?}"))?
|
||||
}
|
||||
|
||||
pub fn aws_document_to_value(document: &Document) -> Value {
|
||||
match document {
|
||||
Document::Null => Value::Null,
|
||||
Document::Bool(value) => Value::Bool(*value),
|
||||
Document::Number(value) => match *value {
|
||||
AwsNumber::PosInt(value) => Value::Number(Number::from(value)),
|
||||
AwsNumber::NegInt(value) => Value::Number(Number::from(value)),
|
||||
AwsNumber::Float(value) => Value::Number(Number::from_f64(value).unwrap()),
|
||||
},
|
||||
Document::String(value) => Value::String(value.clone()),
|
||||
Document::Array(array) => Value::Array(array.iter().map(aws_document_to_value).collect()),
|
||||
Document::Object(map) => Value::Object(
|
||||
map.iter()
|
||||
.map(|(key, value)| (key.clone(), aws_document_to_value(value)))
|
||||
.collect(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn value_to_aws_document(value: &Value) -> Document {
|
||||
match value {
|
||||
Value::Null => Document::Null,
|
||||
Value::Bool(value) => Document::Bool(*value),
|
||||
Value::Number(value) => {
|
||||
if let Some(value) = value.as_u64() {
|
||||
Document::Number(AwsNumber::PosInt(value))
|
||||
} else if let Some(value) = value.as_i64() {
|
||||
Document::Number(AwsNumber::NegInt(value))
|
||||
} else if let Some(value) = value.as_f64() {
|
||||
Document::Number(AwsNumber::Float(value))
|
||||
} else {
|
||||
Document::Null
|
||||
}
|
||||
}
|
||||
Value::String(value) => Document::String(value.clone()),
|
||||
Value::Array(array) => Document::Array(array.iter().map(value_to_aws_document).collect()),
|
||||
Value::Object(map) => Document::Object(
|
||||
map.iter()
|
||||
.map(|(key, value)| (key.clone(), value_to_aws_document(value)))
|
||||
.collect(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Request {
|
||||
pub model: String,
|
||||
pub max_tokens: u32,
|
||||
pub messages: Vec<BedrockMessage>,
|
||||
pub tools: Vec<BedrockTool>,
|
||||
pub tool_choice: Option<BedrockToolChoice>,
|
||||
pub system: Option<String>,
|
||||
pub metadata: Option<Metadata>,
|
||||
pub stop_sequences: Vec<String>,
|
||||
pub temperature: Option<f32>,
|
||||
pub top_k: Option<u32>,
|
||||
pub top_p: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Metadata {
|
||||
pub user_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum BedrockError {
|
||||
#[error("client error: {0}")]
|
||||
ClientError(anyhow::Error),
|
||||
#[error("extension error: {0}")]
|
||||
ExtensionError(anyhow::Error),
|
||||
#[error(transparent)]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
199
crates/bedrock/src/models.rs
Normal file
199
crates/bedrock/src/models.rs
Normal file
@@ -0,0 +1,199 @@
|
||||
use anyhow::anyhow;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::EnumIter;
|
||||
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)]
|
||||
pub enum Model {
|
||||
// Anthropic models (already included)
|
||||
#[default]
|
||||
#[serde(rename = "claude-3-5-sonnet", alias = "claude-3-5-sonnet-latest")]
|
||||
Claude3_5Sonnet,
|
||||
#[serde(rename = "claude-3-opus", alias = "claude-3-opus-latest")]
|
||||
Claude3Opus,
|
||||
#[serde(rename = "claude-3-sonnet", alias = "claude-3-sonnet-latest")]
|
||||
Claude3Sonnet,
|
||||
#[serde(rename = "claude-3-5-haiku", alias = "claude-3-5-haiku-latest")]
|
||||
Claude3_5Haiku,
|
||||
// Amazon Nova Models
|
||||
AmazonNovaLite,
|
||||
AmazonNovaMicro,
|
||||
AmazonNovaPro,
|
||||
// AI21 models
|
||||
AI21J2GrandeInstruct,
|
||||
AI21J2JumboInstruct,
|
||||
AI21J2Mid,
|
||||
AI21J2MidV1,
|
||||
AI21J2Ultra,
|
||||
AI21J2UltraV1_8k,
|
||||
AI21J2UltraV1,
|
||||
AI21JambaInstructV1,
|
||||
AI21Jamba15LargeV1,
|
||||
AI21Jamba15MiniV1,
|
||||
// Cohere models
|
||||
CohereCommandTextV14_4k,
|
||||
CohereCommandRV1,
|
||||
CohereCommandRPlusV1,
|
||||
CohereCommandLightTextV14_4k,
|
||||
// Meta models
|
||||
MetaLlama38BInstructV1,
|
||||
MetaLlama370BInstructV1,
|
||||
MetaLlama318BInstructV1_128k,
|
||||
MetaLlama318BInstructV1,
|
||||
MetaLlama3170BInstructV1_128k,
|
||||
MetaLlama3170BInstructV1,
|
||||
MetaLlama3211BInstructV1,
|
||||
MetaLlama3290BInstructV1,
|
||||
MetaLlama321BInstructV1,
|
||||
MetaLlama323BInstructV1,
|
||||
// Mistral models
|
||||
MistralMistral7BInstructV0,
|
||||
MistralMixtral8x7BInstructV0,
|
||||
MistralMistralLarge2402V1,
|
||||
MistralMistralSmall2402V1,
|
||||
#[serde(rename = "custom")]
|
||||
Custom {
|
||||
name: String,
|
||||
max_tokens: usize,
|
||||
/// The name displayed in the UI, such as in the assistant panel model dropdown menu.
|
||||
display_name: Option<String>,
|
||||
max_output_tokens: Option<u32>,
|
||||
default_temperature: Option<f32>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub fn from_id(id: &str) -> anyhow::Result<Self> {
|
||||
if id.starts_with("claude-3-5-sonnet") {
|
||||
Ok(Self::Claude3_5Sonnet)
|
||||
} else if id.starts_with("claude-3-opus") {
|
||||
Ok(Self::Claude3Opus)
|
||||
} else if id.starts_with("claude-3-sonnet") {
|
||||
Ok(Self::Claude3Sonnet)
|
||||
} else if id.starts_with("claude-3-5-haiku") {
|
||||
Ok(Self::Claude3_5Haiku)
|
||||
} else {
|
||||
Err(anyhow!("invalid model id"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &str {
|
||||
match self {
|
||||
Model::Claude3_5Sonnet => "us.anthropic.claude-3-5-sonnet-20241022-v2:0",
|
||||
Model::Claude3Opus => "us.anthropic.claude-3-opus-20240229-v1:0",
|
||||
Model::Claude3Sonnet => "us.anthropic.claude-3-sonnet-20240229-v1:0",
|
||||
Model::Claude3_5Haiku => "us.anthropic.claude-3-5-haiku-20241022-v1:0",
|
||||
Model::AmazonNovaLite => "us.amazon.nova-lite-v1:0",
|
||||
Model::AmazonNovaMicro => "us.amazon.nova-micro-v1:0",
|
||||
Model::AmazonNovaPro => "us.amazon.nova-pro-v1:0",
|
||||
Model::AI21J2GrandeInstruct => "ai21.j2-grande-instruct",
|
||||
Model::AI21J2JumboInstruct => "ai21.j2-jumbo-instruct",
|
||||
Model::AI21J2Mid => "ai21.j2-mid",
|
||||
Model::AI21J2MidV1 => "ai21.j2-mid-v1",
|
||||
Model::AI21J2Ultra => "ai21.j2-ultra",
|
||||
Model::AI21J2UltraV1_8k => "ai21.j2-ultra-v1:0:8k",
|
||||
Model::AI21J2UltraV1 => "ai21.j2-ultra-v1",
|
||||
Model::AI21JambaInstructV1 => "ai21.jamba-instruct-v1:0",
|
||||
Model::AI21Jamba15LargeV1 => "ai21.jamba-1-5-large-v1:0",
|
||||
Model::AI21Jamba15MiniV1 => "ai21.jamba-1-5-mini-v1:0",
|
||||
Model::CohereCommandTextV14_4k => "cohere.command-text-v14:7:4k",
|
||||
Model::CohereCommandRV1 => "cohere.command-r-v1:0",
|
||||
Model::CohereCommandRPlusV1 => "cohere.command-r-plus-v1:0",
|
||||
Model::CohereCommandLightTextV14_4k => "cohere.command-light-text-v14:7:4k",
|
||||
Model::MetaLlama38BInstructV1 => "meta.llama3-8b-instruct-v1:0",
|
||||
Model::MetaLlama370BInstructV1 => "meta.llama3-70b-instruct-v1:0",
|
||||
Model::MetaLlama318BInstructV1_128k => "meta.llama3-1-8b-instruct-v1:0:128k",
|
||||
Model::MetaLlama318BInstructV1 => "meta.llama3-1-8b-instruct-v1:0",
|
||||
Model::MetaLlama3170BInstructV1_128k => "meta.llama3-1-70b-instruct-v1:0:128k",
|
||||
Model::MetaLlama3170BInstructV1 => "meta.llama3-1-70b-instruct-v1:0",
|
||||
Model::MetaLlama3211BInstructV1 => "meta.llama3-2-11b-instruct-v1:0",
|
||||
Model::MetaLlama3290BInstructV1 => "meta.llama3-2-90b-instruct-v1:0",
|
||||
Model::MetaLlama321BInstructV1 => "meta.llama3-2-1b-instruct-v1:0",
|
||||
Model::MetaLlama323BInstructV1 => "meta.llama3-2-3b-instruct-v1:0",
|
||||
Model::MistralMistral7BInstructV0 => "mistral.mistral-7b-instruct-v0:2",
|
||||
Model::MistralMixtral8x7BInstructV0 => "mistral.mixtral-8x7b-instruct-v0:1",
|
||||
Model::MistralMistralLarge2402V1 => "mistral.mistral-large-2402-v1:0",
|
||||
Model::MistralMistralSmall2402V1 => "mistral.mistral-small-2402-v1:0",
|
||||
Self::Custom { name, .. } => name,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn display_name(&self) -> &str {
|
||||
match self {
|
||||
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
|
||||
Self::Claude3Opus => "Claude 3 Opus",
|
||||
Self::Claude3Sonnet => "Claude 3 Sonnet",
|
||||
Self::Claude3_5Haiku => "Claude 3.5 Haiku",
|
||||
Self::AmazonNovaLite => "Amazon Nova Lite",
|
||||
Self::AmazonNovaMicro => "Amazon Nova Micro",
|
||||
Self::AmazonNovaPro => "Amazon Nova Pro",
|
||||
Self::AI21J2GrandeInstruct => "AI21 Jurassic2 Grande Instruct",
|
||||
Self::AI21J2JumboInstruct => "AI21 Jurassic2 Jumbo Instruct",
|
||||
Self::AI21J2Mid => "AI21 Jurassic2 Mid",
|
||||
Self::AI21J2MidV1 => "AI21 Jurassic2 Mid V1",
|
||||
Self::AI21J2Ultra => "AI21 Jurassic2 Ultra",
|
||||
Self::AI21J2UltraV1_8k => "AI21 Jurassic2 Ultra V1 8K",
|
||||
Self::AI21J2UltraV1 => "AI21 Jurassic2 Ultra V1",
|
||||
Self::AI21JambaInstructV1 => "AI21 Jamba Instruct",
|
||||
Self::AI21Jamba15LargeV1 => "AI21 Jamba 1.5 Large",
|
||||
Self::AI21Jamba15MiniV1 => "AI21 Jamba 1.5 Mini",
|
||||
Self::CohereCommandTextV14_4k => "Cohere Command Text V14 4K",
|
||||
Self::CohereCommandRV1 => "Cohere Command R V1",
|
||||
Self::CohereCommandRPlusV1 => "Cohere Command R Plus V1",
|
||||
Self::CohereCommandLightTextV14_4k => "Cohere Command Light Text V14 4K",
|
||||
Self::MetaLlama38BInstructV1 => "Meta Llama 3 8B Instruct V1",
|
||||
Self::MetaLlama370BInstructV1 => "Meta Llama 3 70B Instruct V1",
|
||||
Self::MetaLlama318BInstructV1_128k => "Meta Llama 3 1.8B Instruct V1 128K",
|
||||
Self::MetaLlama318BInstructV1 => "Meta Llama 3 1.8B Instruct V1",
|
||||
Self::MetaLlama3170BInstructV1_128k => "Meta Llama 3 1 70B Instruct V1 128K",
|
||||
Self::MetaLlama3170BInstructV1 => "Meta Llama 3 1 70B Instruct V1",
|
||||
Self::MetaLlama3211BInstructV1 => "Meta Llama 3 2 11B Instruct V1",
|
||||
Self::MetaLlama3290BInstructV1 => "Meta Llama 3 2 90B Instruct V1",
|
||||
Self::MetaLlama321BInstructV1 => "Meta Llama 3 2 1B Instruct V1",
|
||||
Self::MetaLlama323BInstructV1 => "Meta Llama 3 2 3B Instruct V1",
|
||||
Self::MistralMistral7BInstructV0 => "Mistral 7B Instruct V0",
|
||||
Self::MistralMixtral8x7BInstructV0 => "Mistral Mixtral 8x7B Instruct V0",
|
||||
Self::MistralMistralLarge2402V1 => "Mistral Large 2402 V1",
|
||||
Self::MistralMistralSmall2402V1 => "Mistral Small 2402 V1",
|
||||
Self::Custom {
|
||||
display_name, name, ..
|
||||
} => display_name.as_deref().unwrap_or(name),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn max_token_count(&self) -> usize {
|
||||
match self {
|
||||
Self::Claude3_5Sonnet
|
||||
| Self::Claude3Opus
|
||||
| Self::Claude3Sonnet
|
||||
| Self::Claude3_5Haiku => 200_000,
|
||||
Self::Custom { max_tokens, .. } => *max_tokens,
|
||||
_ => 200_000,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn max_output_tokens(&self) -> u32 {
|
||||
match self {
|
||||
Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3_5Haiku => 4_096,
|
||||
Self::Claude3_5Sonnet => 8_192,
|
||||
Self::Custom {
|
||||
max_output_tokens, ..
|
||||
} => max_output_tokens.unwrap_or(4_096),
|
||||
_ => 4_096,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_temperature(&self) -> f32 {
|
||||
match self {
|
||||
Self::Claude3_5Sonnet
|
||||
| Self::Claude3Opus
|
||||
| Self::Claude3Sonnet
|
||||
| Self::Claude3_5Haiku => 1.0,
|
||||
Self::Custom {
|
||||
default_temperature,
|
||||
..
|
||||
} => default_temperature.unwrap_or(1.0),
|
||||
_ => 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -256,6 +256,7 @@ async fn perform_completion(
|
||||
// so that users can use the new version, without having to update Zed.
|
||||
request.model = match model.as_str() {
|
||||
"claude-3-5-sonnet" => anthropic::Model::Claude3_5Sonnet.id().to_string(),
|
||||
"claude-3-7-sonnet" => anthropic::Model::Claude3_7Sonnet.id().to_string(),
|
||||
"claude-3-opus" => anthropic::Model::Claude3Opus.id().to_string(),
|
||||
"claude-3-haiku" => anthropic::Model::Claude3Haiku.id().to_string(),
|
||||
"claude-3-sonnet" => anthropic::Model::Claude3Sonnet.id().to_string(),
|
||||
|
||||
@@ -392,9 +392,13 @@ impl Server {
|
||||
.add_request_handler(forward_mutating_project_request::<proto::OpenContext>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::CreateContext>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::SynchronizeContexts>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::Push>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::Pull>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::Fetch>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::Stage>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::Unstage>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::Commit>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GetRemotes>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GitShow>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GitReset>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GitCheckoutFiles>)
|
||||
|
||||
@@ -38,6 +38,7 @@ gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
inline_completion.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
menu.workspace = true
|
||||
node_runtime.workspace = true
|
||||
@@ -62,7 +63,9 @@ async-std = { version = "1.12.0", features = ["unstable"] }
|
||||
client = { workspace = true, features = ["test-support"] }
|
||||
clock = { workspace = true, features = ["test-support"] }
|
||||
collections = { workspace = true, features = ["test-support"] }
|
||||
ctor.workspace = true
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
env_logger.workspace = true
|
||||
fs = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
http_client = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -16,6 +16,7 @@ use gpui::{
|
||||
};
|
||||
use http_client::github::get_release_by_tag_name;
|
||||
use http_client::HttpClient;
|
||||
use language::language_settings::CopilotSettings;
|
||||
use language::{
|
||||
language_settings::{all_language_settings, language_settings, EditPredictionProvider},
|
||||
point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, Language, PointUtf16,
|
||||
@@ -367,13 +368,13 @@ impl Copilot {
|
||||
let server_id = self.server_id;
|
||||
let http = self.http.clone();
|
||||
let node_runtime = self.node_runtime.clone();
|
||||
if all_language_settings(None, cx).edit_predictions.provider
|
||||
== EditPredictionProvider::Copilot
|
||||
{
|
||||
let language_settings = all_language_settings(None, cx);
|
||||
if language_settings.edit_predictions.provider == EditPredictionProvider::Copilot {
|
||||
if matches!(self.server, CopilotServer::Disabled) {
|
||||
let env = self.build_env(&language_settings.edit_predictions.copilot);
|
||||
let start_task = cx
|
||||
.spawn(move |this, cx| {
|
||||
Self::start_language_server(server_id, http, node_runtime, this, cx)
|
||||
Self::start_language_server(server_id, http, node_runtime, env, this, cx)
|
||||
})
|
||||
.shared();
|
||||
self.server = CopilotServer::Starting { task: start_task };
|
||||
@@ -385,6 +386,30 @@ impl Copilot {
|
||||
}
|
||||
}
|
||||
|
||||
fn build_env(&self, copilot_settings: &CopilotSettings) -> Option<HashMap<String, String>> {
|
||||
let proxy_url = copilot_settings.proxy.clone()?;
|
||||
let no_verify = copilot_settings.proxy_no_verify;
|
||||
let http_or_https_proxy = if proxy_url.starts_with("http:") {
|
||||
"HTTP_PROXY"
|
||||
} else if proxy_url.starts_with("https:") {
|
||||
"HTTPS_PROXY"
|
||||
} else {
|
||||
log::error!(
|
||||
"Unsupported protocol scheme for language server proxy (must be http or https)"
|
||||
);
|
||||
return None;
|
||||
};
|
||||
|
||||
let mut env = HashMap::default();
|
||||
env.insert(http_or_https_proxy.to_string(), proxy_url);
|
||||
|
||||
if let Some(true) = no_verify {
|
||||
env.insert("NODE_TLS_REJECT_UNAUTHORIZED".to_string(), "0".to_string());
|
||||
};
|
||||
|
||||
Some(env)
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn fake(cx: &mut gpui::TestAppContext) -> (Entity<Self>, lsp::FakeLanguageServer) {
|
||||
use lsp::FakeLanguageServer;
|
||||
@@ -422,6 +447,7 @@ impl Copilot {
|
||||
new_server_id: LanguageServerId,
|
||||
http: Arc<dyn HttpClient>,
|
||||
node_runtime: NodeRuntime,
|
||||
env: Option<HashMap<String, String>>,
|
||||
this: WeakEntity<Self>,
|
||||
mut cx: AsyncApp,
|
||||
) {
|
||||
@@ -432,8 +458,7 @@ impl Copilot {
|
||||
let binary = LanguageServerBinary {
|
||||
path: node_path,
|
||||
arguments,
|
||||
// TODO: We could set HTTP_PROXY etc here and fix the copilot issue.
|
||||
env: None,
|
||||
env,
|
||||
};
|
||||
|
||||
let root_path = if cfg!(target_os = "windows") {
|
||||
@@ -611,6 +636,8 @@ impl Copilot {
|
||||
}
|
||||
|
||||
pub fn reinstall(&mut self, cx: &mut Context<Self>) -> Task<()> {
|
||||
let language_settings = all_language_settings(None, cx);
|
||||
let env = self.build_env(&language_settings.edit_predictions.copilot);
|
||||
let start_task = cx
|
||||
.spawn({
|
||||
let http = self.http.clone();
|
||||
@@ -618,7 +645,7 @@ impl Copilot {
|
||||
let server_id = self.server_id;
|
||||
move |this, cx| async move {
|
||||
clear_copilot_dir().await;
|
||||
Self::start_language_server(server_id, http, node_runtime, this, cx).await
|
||||
Self::start_language_server(server_id, http, node_runtime, env, this, cx).await
|
||||
}
|
||||
})
|
||||
.shared();
|
||||
@@ -1279,3 +1306,11 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[ctor::ctor]
|
||||
fn init_logger() {
|
||||
if std::env::var("RUST_LOG").is_ok() {
|
||||
env_logger::init();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ pub use editor_settings::{
|
||||
CurrentLineHighlight, EditorSettings, ScrollBeyondLastLine, SearchSettings, ShowScrollbar,
|
||||
};
|
||||
pub use editor_settings_controls::*;
|
||||
use element::{AcceptEditPredictionBinding, LineWithInvisibles, PositionMap};
|
||||
use element::{layout_line, AcceptEditPredictionBinding, LineWithInvisibles, PositionMap};
|
||||
pub use element::{
|
||||
CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition,
|
||||
};
|
||||
@@ -82,7 +82,7 @@ use git::blame::GitBlame;
|
||||
use gpui::{
|
||||
div, impl_actions, point, prelude::*, pulsating_between, px, relative, size, Action, Animation,
|
||||
AnimationExt, AnyElement, App, AsyncWindowContext, AvailableSpace, Background, Bounds,
|
||||
ClipboardEntry, ClipboardItem, Context, DispatchPhase, Entity, EntityInputHandler,
|
||||
ClipboardEntry, ClipboardItem, Context, DispatchPhase, Edges, Entity, EntityInputHandler,
|
||||
EventEmitter, FocusHandle, FocusOutEvent, Focusable, FontId, FontWeight, Global,
|
||||
HighlightStyle, Hsla, KeyContext, Modifiers, MouseButton, MouseDownEvent, PaintQuad,
|
||||
ParentElement, Pixels, Render, SharedString, Size, Styled, StyledText, Subscription, Task,
|
||||
@@ -113,6 +113,7 @@ use persistence::DB;
|
||||
pub use proposed_changes_editor::{
|
||||
ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar,
|
||||
};
|
||||
use smallvec::smallvec;
|
||||
use std::iter::Peekable;
|
||||
use task::{ResolvedTask, TaskTemplate, TaskVariables};
|
||||
|
||||
@@ -676,8 +677,8 @@ pub struct Editor {
|
||||
show_inline_completions_override: Option<bool>,
|
||||
menu_inline_completions_policy: MenuInlineCompletionsPolicy,
|
||||
edit_prediction_preview: EditPredictionPreview,
|
||||
edit_prediction_cursor_on_leading_whitespace: bool,
|
||||
edit_prediction_requires_modifier_in_leading_space: bool,
|
||||
edit_prediction_indent_conflict: bool,
|
||||
edit_prediction_requires_modifier_in_indent_conflict: bool,
|
||||
inlay_hint_cache: InlayHintCache,
|
||||
next_inlay_id: usize,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
@@ -695,7 +696,6 @@ pub struct Editor {
|
||||
show_git_blame_inline: bool,
|
||||
show_git_blame_inline_delay_task: Option<Task<()>>,
|
||||
git_blame_inline_tooltip: Option<WeakEntity<crate::commit_tooltip::CommitTooltip>>,
|
||||
distinguish_unstaged_diff_hunks: bool,
|
||||
git_blame_inline_enabled: bool,
|
||||
serialize_dirty_buffers: bool,
|
||||
show_selection_menu: Option<bool>,
|
||||
@@ -1403,12 +1403,11 @@ impl Editor {
|
||||
show_inline_completions_override: None,
|
||||
menu_inline_completions_policy: MenuInlineCompletionsPolicy::ByProvider,
|
||||
edit_prediction_settings: EditPredictionSettings::Disabled,
|
||||
edit_prediction_cursor_on_leading_whitespace: false,
|
||||
edit_prediction_requires_modifier_in_leading_space: true,
|
||||
edit_prediction_indent_conflict: false,
|
||||
edit_prediction_requires_modifier_in_indent_conflict: true,
|
||||
custom_context_menu: None,
|
||||
show_git_blame_gutter: false,
|
||||
show_git_blame_inline: false,
|
||||
distinguish_unstaged_diff_hunks: false,
|
||||
show_selection_menu: None,
|
||||
show_git_blame_inline_delay_task: None,
|
||||
git_blame_inline_tooltip: None,
|
||||
@@ -1579,7 +1578,7 @@ impl Editor {
|
||||
|| self.edit_prediction_requires_modifier()
|
||||
// Require modifier key when the cursor is on leading whitespace, to allow `tab`
|
||||
// bindings to insert tab characters.
|
||||
|| (self.edit_prediction_requires_modifier_in_leading_space && self.edit_prediction_cursor_on_leading_whitespace)
|
||||
|| (self.edit_prediction_requires_modifier_in_indent_conflict && self.edit_prediction_indent_conflict)
|
||||
}
|
||||
|
||||
pub fn accept_edit_prediction_keybind(
|
||||
@@ -2151,7 +2150,7 @@ impl Editor {
|
||||
self.refresh_selected_text_highlights(window, cx);
|
||||
refresh_matching_bracket_highlights(self, window, cx);
|
||||
self.update_visible_inline_completion(window, cx);
|
||||
self.edit_prediction_requires_modifier_in_leading_space = true;
|
||||
self.edit_prediction_requires_modifier_in_indent_conflict = true;
|
||||
linked_editing_ranges::refresh_linked_ranges(self, window, cx);
|
||||
if self.git_blame_inline_enabled {
|
||||
self.start_inline_blame_timer(window, cx);
|
||||
@@ -5137,7 +5136,7 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
self.edit_prediction_requires_modifier_in_leading_space = false;
|
||||
self.edit_prediction_requires_modifier_in_indent_conflict = false;
|
||||
}
|
||||
|
||||
pub fn accept_partial_inline_completion(
|
||||
@@ -5435,8 +5434,19 @@ impl Editor {
|
||||
self.edit_prediction_settings =
|
||||
self.edit_prediction_settings_at_position(&buffer, cursor_buffer_position, cx);
|
||||
|
||||
self.edit_prediction_cursor_on_leading_whitespace =
|
||||
multibuffer.is_line_whitespace_upto(cursor);
|
||||
self.edit_prediction_indent_conflict = multibuffer.is_line_whitespace_upto(cursor);
|
||||
|
||||
if self.edit_prediction_indent_conflict {
|
||||
let cursor_point = cursor.to_point(&multibuffer);
|
||||
|
||||
let indents = multibuffer.suggested_indents(cursor_point.row..cursor_point.row + 1, cx);
|
||||
|
||||
if let Some((_, indent)) = indents.iter().next() {
|
||||
if indent.len == cursor_point.column {
|
||||
self.edit_prediction_indent_conflict = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let inline_completion = provider.suggest(&buffer, cursor_buffer_position, cx)?;
|
||||
let edits = inline_completion
|
||||
@@ -5784,6 +5794,524 @@ impl Editor {
|
||||
.map(|menu| menu.origin())
|
||||
}
|
||||
|
||||
const EDIT_PREDICTION_POPOVER_PADDING_X: Pixels = Pixels(24.);
|
||||
const EDIT_PREDICTION_POPOVER_PADDING_Y: Pixels = Pixels(2.);
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_edit_prediction_popover(
|
||||
&mut self,
|
||||
text_bounds: &Bounds<Pixels>,
|
||||
content_origin: gpui::Point<Pixels>,
|
||||
editor_snapshot: &EditorSnapshot,
|
||||
visible_row_range: Range<DisplayRow>,
|
||||
scroll_top: f32,
|
||||
scroll_bottom: f32,
|
||||
line_layouts: &[LineWithInvisibles],
|
||||
line_height: Pixels,
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
newest_selection_head: Option<DisplayPoint>,
|
||||
editor_width: Pixels,
|
||||
style: &EditorStyle,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<(AnyElement, gpui::Point<Pixels>)> {
|
||||
let active_inline_completion = self.active_inline_completion.as_ref()?;
|
||||
|
||||
if self.edit_prediction_visible_in_cursor_popover(true) {
|
||||
return None;
|
||||
}
|
||||
|
||||
match &active_inline_completion.completion {
|
||||
InlineCompletion::Move { target, .. } => {
|
||||
let target_display_point = target.to_display_point(editor_snapshot);
|
||||
|
||||
if self.edit_prediction_requires_modifier() {
|
||||
if !self.edit_prediction_preview_is_active() {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.render_edit_prediction_modifier_jump_popover(
|
||||
text_bounds,
|
||||
content_origin,
|
||||
visible_row_range,
|
||||
line_layouts,
|
||||
line_height,
|
||||
scroll_pixel_position,
|
||||
newest_selection_head,
|
||||
target_display_point,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
self.render_edit_prediction_eager_jump_popover(
|
||||
text_bounds,
|
||||
content_origin,
|
||||
editor_snapshot,
|
||||
visible_row_range,
|
||||
scroll_top,
|
||||
scroll_bottom,
|
||||
line_height,
|
||||
scroll_pixel_position,
|
||||
target_display_point,
|
||||
editor_width,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
}
|
||||
InlineCompletion::Edit {
|
||||
display_mode: EditDisplayMode::Inline,
|
||||
..
|
||||
} => None,
|
||||
InlineCompletion::Edit {
|
||||
display_mode: EditDisplayMode::TabAccept,
|
||||
edits,
|
||||
..
|
||||
} => {
|
||||
let range = &edits.first()?.0;
|
||||
let target_display_point = range.end.to_display_point(editor_snapshot);
|
||||
|
||||
self.render_edit_prediction_end_of_line_popover(
|
||||
"Accept",
|
||||
editor_snapshot,
|
||||
visible_row_range,
|
||||
target_display_point,
|
||||
line_height,
|
||||
scroll_pixel_position,
|
||||
content_origin,
|
||||
editor_width,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
InlineCompletion::Edit {
|
||||
edits,
|
||||
edit_preview,
|
||||
display_mode: EditDisplayMode::DiffPopover,
|
||||
snapshot,
|
||||
} => self.render_edit_prediction_diff_popover(
|
||||
text_bounds,
|
||||
content_origin,
|
||||
editor_snapshot,
|
||||
visible_row_range,
|
||||
line_layouts,
|
||||
line_height,
|
||||
scroll_pixel_position,
|
||||
newest_selection_head,
|
||||
editor_width,
|
||||
style,
|
||||
edits,
|
||||
edit_preview,
|
||||
snapshot,
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_edit_prediction_modifier_jump_popover(
|
||||
&mut self,
|
||||
text_bounds: &Bounds<Pixels>,
|
||||
content_origin: gpui::Point<Pixels>,
|
||||
visible_row_range: Range<DisplayRow>,
|
||||
line_layouts: &[LineWithInvisibles],
|
||||
line_height: Pixels,
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
newest_selection_head: Option<DisplayPoint>,
|
||||
target_display_point: DisplayPoint,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<(AnyElement, gpui::Point<Pixels>)> {
|
||||
let scrolled_content_origin =
|
||||
content_origin - gpui::Point::new(scroll_pixel_position.x, Pixels(0.0));
|
||||
|
||||
const SCROLL_PADDING_Y: Pixels = px(12.);
|
||||
|
||||
if target_display_point.row() < visible_row_range.start {
|
||||
return self.render_edit_prediction_scroll_popover(
|
||||
|_| SCROLL_PADDING_Y,
|
||||
IconName::ArrowUp,
|
||||
visible_row_range,
|
||||
line_layouts,
|
||||
newest_selection_head,
|
||||
scrolled_content_origin,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
} else if target_display_point.row() >= visible_row_range.end {
|
||||
return self.render_edit_prediction_scroll_popover(
|
||||
|size| text_bounds.size.height - size.height - SCROLL_PADDING_Y,
|
||||
IconName::ArrowDown,
|
||||
visible_row_range,
|
||||
line_layouts,
|
||||
newest_selection_head,
|
||||
scrolled_content_origin,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
const POLE_WIDTH: Pixels = px(2.);
|
||||
|
||||
let mut element = v_flex()
|
||||
.items_end()
|
||||
.child(
|
||||
self.render_edit_prediction_line_popover("Jump", None, window, cx)?
|
||||
.rounded_br(px(0.))
|
||||
.rounded_tr(px(0.))
|
||||
.border_r_2(),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w(POLE_WIDTH)
|
||||
.bg(Editor::edit_prediction_callout_popover_border_color(cx))
|
||||
.h(line_height),
|
||||
)
|
||||
.into_any();
|
||||
|
||||
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
|
||||
|
||||
let line_layout =
|
||||
line_layouts.get(target_display_point.row().minus(visible_row_range.start) as usize)?;
|
||||
let target_column = target_display_point.column() as usize;
|
||||
|
||||
let target_x = line_layout.x_for_index(target_column);
|
||||
let target_y =
|
||||
(target_display_point.row().as_f32() * line_height) - scroll_pixel_position.y;
|
||||
|
||||
let mut origin = scrolled_content_origin + point(target_x, target_y)
|
||||
- point(size.width - POLE_WIDTH, size.height - line_height);
|
||||
|
||||
origin.x = origin.x.max(content_origin.x);
|
||||
|
||||
element.prepaint_at(origin, window, cx);
|
||||
|
||||
Some((element, origin))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_edit_prediction_scroll_popover(
|
||||
&mut self,
|
||||
to_y: impl Fn(Size<Pixels>) -> Pixels,
|
||||
scroll_icon: IconName,
|
||||
visible_row_range: Range<DisplayRow>,
|
||||
line_layouts: &[LineWithInvisibles],
|
||||
newest_selection_head: Option<DisplayPoint>,
|
||||
scrolled_content_origin: gpui::Point<Pixels>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<(AnyElement, gpui::Point<Pixels>)> {
|
||||
let mut element = self
|
||||
.render_edit_prediction_line_popover("Scroll", Some(scroll_icon), window, cx)?
|
||||
.into_any();
|
||||
|
||||
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
|
||||
|
||||
let cursor = newest_selection_head?;
|
||||
let cursor_row_layout =
|
||||
line_layouts.get(cursor.row().minus(visible_row_range.start) as usize)?;
|
||||
let cursor_column = cursor.column() as usize;
|
||||
|
||||
let cursor_character_x = cursor_row_layout.x_for_index(cursor_column);
|
||||
|
||||
let origin = scrolled_content_origin + point(cursor_character_x, to_y(size));
|
||||
|
||||
element.prepaint_at(origin, window, cx);
|
||||
Some((element, origin))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_edit_prediction_eager_jump_popover(
|
||||
&mut self,
|
||||
text_bounds: &Bounds<Pixels>,
|
||||
content_origin: gpui::Point<Pixels>,
|
||||
editor_snapshot: &EditorSnapshot,
|
||||
visible_row_range: Range<DisplayRow>,
|
||||
scroll_top: f32,
|
||||
scroll_bottom: f32,
|
||||
line_height: Pixels,
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
target_display_point: DisplayPoint,
|
||||
editor_width: Pixels,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<(AnyElement, gpui::Point<Pixels>)> {
|
||||
if target_display_point.row().as_f32() < scroll_top {
|
||||
let mut element = self
|
||||
.render_edit_prediction_line_popover(
|
||||
"Jump to Edit",
|
||||
Some(IconName::ArrowUp),
|
||||
window,
|
||||
cx,
|
||||
)?
|
||||
.into_any();
|
||||
|
||||
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
|
||||
let offset = point(
|
||||
(text_bounds.size.width - size.width) / 2.,
|
||||
Self::EDIT_PREDICTION_POPOVER_PADDING_Y,
|
||||
);
|
||||
|
||||
let origin = text_bounds.origin + offset;
|
||||
element.prepaint_at(origin, window, cx);
|
||||
Some((element, origin))
|
||||
} else if (target_display_point.row().as_f32() + 1.) > scroll_bottom {
|
||||
let mut element = self
|
||||
.render_edit_prediction_line_popover(
|
||||
"Jump to Edit",
|
||||
Some(IconName::ArrowDown),
|
||||
window,
|
||||
cx,
|
||||
)?
|
||||
.into_any();
|
||||
|
||||
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
|
||||
let offset = point(
|
||||
(text_bounds.size.width - size.width) / 2.,
|
||||
text_bounds.size.height - size.height - Self::EDIT_PREDICTION_POPOVER_PADDING_Y,
|
||||
);
|
||||
|
||||
let origin = text_bounds.origin + offset;
|
||||
element.prepaint_at(origin, window, cx);
|
||||
Some((element, origin))
|
||||
} else {
|
||||
self.render_edit_prediction_end_of_line_popover(
|
||||
"Jump to Edit",
|
||||
editor_snapshot,
|
||||
visible_row_range,
|
||||
target_display_point,
|
||||
line_height,
|
||||
scroll_pixel_position,
|
||||
content_origin,
|
||||
editor_width,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_edit_prediction_end_of_line_popover(
|
||||
self: &mut Editor,
|
||||
label: &'static str,
|
||||
editor_snapshot: &EditorSnapshot,
|
||||
visible_row_range: Range<DisplayRow>,
|
||||
target_display_point: DisplayPoint,
|
||||
line_height: Pixels,
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
content_origin: gpui::Point<Pixels>,
|
||||
editor_width: Pixels,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<(AnyElement, gpui::Point<Pixels>)> {
|
||||
let target_line_end = DisplayPoint::new(
|
||||
target_display_point.row(),
|
||||
editor_snapshot.line_len(target_display_point.row()),
|
||||
);
|
||||
|
||||
let mut element = self
|
||||
.render_edit_prediction_line_popover(label, None, window, cx)?
|
||||
.into_any();
|
||||
|
||||
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
|
||||
|
||||
let line_origin = self.display_to_pixel_point(target_line_end, editor_snapshot, window)?;
|
||||
|
||||
let start_point = content_origin - point(scroll_pixel_position.x, Pixels::ZERO);
|
||||
let mut origin = start_point
|
||||
+ line_origin
|
||||
+ point(Self::EDIT_PREDICTION_POPOVER_PADDING_X, Pixels::ZERO);
|
||||
origin.x = origin.x.max(content_origin.x);
|
||||
|
||||
let max_x = content_origin.x + editor_width - size.width;
|
||||
|
||||
if origin.x > max_x {
|
||||
let offset = line_height + Self::EDIT_PREDICTION_POPOVER_PADDING_Y;
|
||||
|
||||
let icon = if visible_row_range.contains(&(target_display_point.row() + 2)) {
|
||||
origin.y += offset;
|
||||
IconName::ArrowUp
|
||||
} else {
|
||||
origin.y -= offset;
|
||||
IconName::ArrowDown
|
||||
};
|
||||
|
||||
element = self
|
||||
.render_edit_prediction_line_popover(label, Some(icon), window, cx)?
|
||||
.into_any();
|
||||
|
||||
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
|
||||
|
||||
origin.x = content_origin.x + editor_width - size.width - px(2.);
|
||||
}
|
||||
|
||||
element.prepaint_at(origin, window, cx);
|
||||
Some((element, origin))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_edit_prediction_diff_popover(
|
||||
self: &Editor,
|
||||
text_bounds: &Bounds<Pixels>,
|
||||
content_origin: gpui::Point<Pixels>,
|
||||
editor_snapshot: &EditorSnapshot,
|
||||
visible_row_range: Range<DisplayRow>,
|
||||
line_layouts: &[LineWithInvisibles],
|
||||
line_height: Pixels,
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
newest_selection_head: Option<DisplayPoint>,
|
||||
editor_width: Pixels,
|
||||
style: &EditorStyle,
|
||||
edits: &Vec<(Range<Anchor>, String)>,
|
||||
edit_preview: &Option<language::EditPreview>,
|
||||
snapshot: &language::BufferSnapshot,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<(AnyElement, gpui::Point<Pixels>)> {
|
||||
let edit_start = edits
|
||||
.first()
|
||||
.unwrap()
|
||||
.0
|
||||
.start
|
||||
.to_display_point(editor_snapshot);
|
||||
let edit_end = edits
|
||||
.last()
|
||||
.unwrap()
|
||||
.0
|
||||
.end
|
||||
.to_display_point(editor_snapshot);
|
||||
|
||||
let is_visible = visible_row_range.contains(&edit_start.row())
|
||||
|| visible_row_range.contains(&edit_end.row());
|
||||
if !is_visible {
|
||||
return None;
|
||||
}
|
||||
|
||||
let highlighted_edits =
|
||||
crate::inline_completion_edit_text(&snapshot, edits, edit_preview.as_ref()?, false, cx);
|
||||
|
||||
let styled_text = highlighted_edits.to_styled_text(&style.text);
|
||||
let line_count = highlighted_edits.text.lines().count();
|
||||
|
||||
const BORDER_WIDTH: Pixels = px(1.);
|
||||
|
||||
let mut element = h_flex()
|
||||
.items_start()
|
||||
.child(
|
||||
h_flex()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border(BORDER_WIDTH)
|
||||
.shadow_sm()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_l_lg()
|
||||
.when(line_count > 1, |el| el.rounded_br_lg())
|
||||
.pr_1()
|
||||
.child(styled_text),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.h(line_height + BORDER_WIDTH * px(2.))
|
||||
.px_1p5()
|
||||
.gap_1()
|
||||
// Workaround: For some reason, there's a gap if we don't do this
|
||||
.ml(-BORDER_WIDTH)
|
||||
.shadow(smallvec![gpui::BoxShadow {
|
||||
color: gpui::black().opacity(0.05),
|
||||
offset: point(px(1.), px(1.)),
|
||||
blur_radius: px(2.),
|
||||
spread_radius: px(0.),
|
||||
}])
|
||||
.bg(Editor::edit_prediction_line_popover_bg_color(cx))
|
||||
.border(BORDER_WIDTH)
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_r_lg()
|
||||
.children(self.render_edit_prediction_accept_keybind(window, cx)),
|
||||
)
|
||||
.into_any();
|
||||
|
||||
let longest_row =
|
||||
editor_snapshot.longest_row_in_range(edit_start.row()..edit_end.row() + 1);
|
||||
let longest_line_width = if visible_row_range.contains(&longest_row) {
|
||||
line_layouts[(longest_row.0 - visible_row_range.start.0) as usize].width
|
||||
} else {
|
||||
layout_line(
|
||||
longest_row,
|
||||
editor_snapshot,
|
||||
style,
|
||||
editor_width,
|
||||
|_| false,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.width
|
||||
};
|
||||
|
||||
let viewport_bounds =
|
||||
Bounds::new(Default::default(), window.viewport_size()).extend(Edges {
|
||||
right: -EditorElement::SCROLLBAR_WIDTH,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let x_after_longest =
|
||||
text_bounds.origin.x + longest_line_width + Self::EDIT_PREDICTION_POPOVER_PADDING_X
|
||||
- scroll_pixel_position.x;
|
||||
|
||||
let element_bounds = element.layout_as_root(AvailableSpace::min_size(), window, cx);
|
||||
|
||||
// Fully visible if it can be displayed within the window (allow overlapping other
|
||||
// panes). However, this is only allowed if the popover starts within text_bounds.
|
||||
let can_position_to_the_right = x_after_longest < text_bounds.right()
|
||||
&& x_after_longest + element_bounds.width < viewport_bounds.right();
|
||||
|
||||
let mut origin = if can_position_to_the_right {
|
||||
point(
|
||||
x_after_longest,
|
||||
text_bounds.origin.y + edit_start.row().as_f32() * line_height
|
||||
- scroll_pixel_position.y,
|
||||
)
|
||||
} else {
|
||||
let cursor_row = newest_selection_head.map(|head| head.row());
|
||||
let above_edit = edit_start
|
||||
.row()
|
||||
.0
|
||||
.checked_sub(line_count as u32)
|
||||
.map(DisplayRow);
|
||||
let below_edit = Some(edit_end.row() + 1);
|
||||
let above_cursor =
|
||||
cursor_row.and_then(|row| row.0.checked_sub(line_count as u32).map(DisplayRow));
|
||||
let below_cursor = cursor_row.map(|cursor_row| cursor_row + 1);
|
||||
|
||||
// Place the edit popover adjacent to the edit if there is a location
|
||||
// available that is onscreen and does not obscure the cursor. Otherwise,
|
||||
// place it adjacent to the cursor.
|
||||
let row_target = [above_edit, below_edit, above_cursor, below_cursor]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.find(|&start_row| {
|
||||
let end_row = start_row + line_count as u32;
|
||||
visible_row_range.contains(&start_row)
|
||||
&& visible_row_range.contains(&end_row)
|
||||
&& cursor_row.map_or(true, |cursor_row| {
|
||||
!((start_row..end_row).contains(&cursor_row))
|
||||
})
|
||||
})?;
|
||||
|
||||
content_origin
|
||||
+ point(
|
||||
-scroll_pixel_position.x,
|
||||
row_target.as_f32() * line_height - scroll_pixel_position.y,
|
||||
)
|
||||
};
|
||||
|
||||
origin.x -= BORDER_WIDTH;
|
||||
|
||||
window.defer_draw(element, origin, 1);
|
||||
|
||||
// Do not return an element, since it will already be drawn due to defer_draw.
|
||||
None
|
||||
}
|
||||
|
||||
fn edit_prediction_cursor_popover_height(&self) -> Pixels {
|
||||
px(30.)
|
||||
}
|
||||
@@ -9005,6 +9533,56 @@ impl Editor {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn move_to_previous_multibuffer_header(
|
||||
&mut self,
|
||||
_: &MoveToStartOfExcerpt,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if matches!(self.mode, EditorMode::SingleLine { .. }) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
|
||||
self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
selection.collapse_to(
|
||||
movement::multibuffer_header(
|
||||
map,
|
||||
selection.head(),
|
||||
workspace::searchable::Direction::Prev,
|
||||
),
|
||||
SelectionGoal::None,
|
||||
)
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub fn move_to_next_multibuffer_header(
|
||||
&mut self,
|
||||
_: &MoveToEndOfExcerpt,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if matches!(self.mode, EditorMode::SingleLine { .. }) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
|
||||
self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
selection.collapse_to(
|
||||
movement::multibuffer_header(
|
||||
map,
|
||||
selection.head(),
|
||||
workspace::searchable::Direction::Next,
|
||||
),
|
||||
SelectionGoal::None,
|
||||
)
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub fn select_to_end_of_paragraph(
|
||||
&mut self,
|
||||
_: &SelectToEndOfParagraph,
|
||||
@@ -12736,10 +13314,6 @@ impl Editor {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn set_distinguish_unstaged_diff_hunks(&mut self) {
|
||||
self.distinguish_unstaged_diff_hunks = true;
|
||||
}
|
||||
|
||||
pub fn expand_all_diff_hunks(
|
||||
&mut self,
|
||||
_: &ExpandAllDiffHunks,
|
||||
@@ -12985,7 +13559,12 @@ impl Editor {
|
||||
.update(cx, |buffer_store, cx| buffer_store.save_buffer(buffer, cx))
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
let _ = repo.read(cx).set_index_text(&path, new_index_text);
|
||||
cx.background_spawn(
|
||||
repo.read(cx)
|
||||
.set_index_text(&path, new_index_text)
|
||||
.log_err(),
|
||||
)
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn expand_selected_diff_hunks(&mut self, cx: &mut Context<Self>) {
|
||||
|
||||
@@ -3711,391 +3711,6 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_edit_prediction_popover(
|
||||
&self,
|
||||
text_bounds: &Bounds<Pixels>,
|
||||
content_origin: gpui::Point<Pixels>,
|
||||
editor_snapshot: &EditorSnapshot,
|
||||
visible_row_range: Range<DisplayRow>,
|
||||
scroll_top: f32,
|
||||
scroll_bottom: f32,
|
||||
line_layouts: &[LineWithInvisibles],
|
||||
line_height: Pixels,
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
newest_selection_head: Option<DisplayPoint>,
|
||||
editor_width: Pixels,
|
||||
style: &EditorStyle,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<(AnyElement, gpui::Point<Pixels>)> {
|
||||
const PADDING_X: Pixels = Pixels(24.);
|
||||
const PADDING_Y: Pixels = Pixels(2.);
|
||||
|
||||
let editor = self.editor.read(cx);
|
||||
let active_inline_completion = editor.active_inline_completion.as_ref()?;
|
||||
|
||||
if editor.edit_prediction_visible_in_cursor_popover(true) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Adjust text origin for horizontal scrolling (in some cases here)
|
||||
let start_point = content_origin - gpui::Point::new(scroll_pixel_position.x, Pixels(0.0));
|
||||
|
||||
// Clamp left offset after extreme scrollings
|
||||
let clamp_start = |point: gpui::Point<Pixels>| gpui::Point {
|
||||
x: point.x.max(content_origin.x),
|
||||
y: point.y,
|
||||
};
|
||||
|
||||
match &active_inline_completion.completion {
|
||||
InlineCompletion::Move { target, .. } => {
|
||||
let target_display_point = target.to_display_point(editor_snapshot);
|
||||
|
||||
if editor.edit_prediction_requires_modifier() {
|
||||
if !editor.edit_prediction_preview_is_active() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if target_display_point.row() < visible_row_range.start {
|
||||
let mut element = editor
|
||||
.render_edit_prediction_line_popover(
|
||||
"Scroll",
|
||||
Some(IconName::ArrowUp),
|
||||
window,
|
||||
cx,
|
||||
)?
|
||||
.into_any();
|
||||
|
||||
element.layout_as_root(AvailableSpace::min_size(), window, cx);
|
||||
|
||||
let cursor = newest_selection_head?;
|
||||
let cursor_row_layout = line_layouts
|
||||
.get(cursor.row().minus(visible_row_range.start) as usize)?;
|
||||
let cursor_column = cursor.column() as usize;
|
||||
|
||||
let cursor_character_x = cursor_row_layout.x_for_index(cursor_column);
|
||||
|
||||
const PADDING_Y: Pixels = px(12.);
|
||||
|
||||
let origin = start_point + point(cursor_character_x, PADDING_Y);
|
||||
|
||||
element.prepaint_at(origin, window, cx);
|
||||
return Some((element, origin));
|
||||
} else if target_display_point.row() >= visible_row_range.end {
|
||||
let mut element = editor
|
||||
.render_edit_prediction_line_popover(
|
||||
"Scroll",
|
||||
Some(IconName::ArrowDown),
|
||||
window,
|
||||
cx,
|
||||
)?
|
||||
.into_any();
|
||||
|
||||
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
|
||||
|
||||
let cursor = newest_selection_head?;
|
||||
let cursor_row_layout = line_layouts
|
||||
.get(cursor.row().minus(visible_row_range.start) as usize)?;
|
||||
let cursor_column = cursor.column() as usize;
|
||||
|
||||
let cursor_character_x = cursor_row_layout.x_for_index(cursor_column);
|
||||
const PADDING_Y: Pixels = px(12.);
|
||||
|
||||
let origin = start_point
|
||||
+ point(
|
||||
cursor_character_x,
|
||||
text_bounds.size.height - size.height - PADDING_Y,
|
||||
);
|
||||
|
||||
element.prepaint_at(origin, window, cx);
|
||||
return Some((element, origin));
|
||||
} else {
|
||||
const POLE_WIDTH: Pixels = px(2.);
|
||||
|
||||
let mut element = v_flex()
|
||||
.items_end()
|
||||
.child(
|
||||
editor
|
||||
.render_edit_prediction_line_popover("Jump", None, window, cx)?
|
||||
.rounded_br(px(0.))
|
||||
.rounded_tr(px(0.))
|
||||
.border_r_2(),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w(POLE_WIDTH)
|
||||
.bg(Editor::edit_prediction_callout_popover_border_color(cx))
|
||||
.h(line_height),
|
||||
)
|
||||
.into_any();
|
||||
|
||||
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
|
||||
|
||||
let line_layout =
|
||||
line_layouts
|
||||
.get(target_display_point.row().minus(visible_row_range.start)
|
||||
as usize)?;
|
||||
let target_column = target_display_point.column() as usize;
|
||||
|
||||
let target_x = line_layout.x_for_index(target_column);
|
||||
let target_y = (target_display_point.row().as_f32() * line_height)
|
||||
- scroll_pixel_position.y;
|
||||
|
||||
let origin = clamp_start(
|
||||
start_point + point(target_x, target_y)
|
||||
- point(size.width - POLE_WIDTH, size.height - line_height),
|
||||
);
|
||||
|
||||
element.prepaint_at(origin, window, cx);
|
||||
|
||||
return Some((element, origin));
|
||||
}
|
||||
}
|
||||
|
||||
if target_display_point.row().as_f32() < scroll_top {
|
||||
let mut element = editor
|
||||
.render_edit_prediction_line_popover(
|
||||
"Jump to Edit",
|
||||
Some(IconName::ArrowUp),
|
||||
window,
|
||||
cx,
|
||||
)?
|
||||
.into_any();
|
||||
|
||||
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
|
||||
let offset = point((text_bounds.size.width - size.width) / 2., PADDING_Y);
|
||||
|
||||
let origin = text_bounds.origin + offset;
|
||||
element.prepaint_at(origin, window, cx);
|
||||
Some((element, origin))
|
||||
} else if (target_display_point.row().as_f32() + 1.) > scroll_bottom {
|
||||
let mut element = editor
|
||||
.render_edit_prediction_line_popover(
|
||||
"Jump to Edit",
|
||||
Some(IconName::ArrowDown),
|
||||
window,
|
||||
cx,
|
||||
)?
|
||||
.into_any();
|
||||
|
||||
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
|
||||
let offset = point(
|
||||
(text_bounds.size.width - size.width) / 2.,
|
||||
text_bounds.size.height - size.height - PADDING_Y,
|
||||
);
|
||||
|
||||
let origin = text_bounds.origin + offset;
|
||||
element.prepaint_at(origin, window, cx);
|
||||
Some((element, origin))
|
||||
} else {
|
||||
let mut element = editor
|
||||
.render_edit_prediction_line_popover("Jump to Edit", None, window, cx)?
|
||||
.into_any();
|
||||
let target_line_end = DisplayPoint::new(
|
||||
target_display_point.row(),
|
||||
editor_snapshot.line_len(target_display_point.row()),
|
||||
);
|
||||
let origin = self.editor.update(cx, |editor, _cx| {
|
||||
editor.display_to_pixel_point(target_line_end, editor_snapshot, window)
|
||||
})?;
|
||||
|
||||
let origin = clamp_start(start_point + origin + point(PADDING_X, px(0.)));
|
||||
element.prepaint_as_root(origin, AvailableSpace::min_size(), window, cx);
|
||||
Some((element, origin))
|
||||
}
|
||||
}
|
||||
InlineCompletion::Edit {
|
||||
edits,
|
||||
edit_preview,
|
||||
display_mode,
|
||||
snapshot,
|
||||
} => {
|
||||
if self.editor.read(cx).has_visible_completions_menu() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let edit_start = edits
|
||||
.first()
|
||||
.unwrap()
|
||||
.0
|
||||
.start
|
||||
.to_display_point(editor_snapshot);
|
||||
let edit_end = edits
|
||||
.last()
|
||||
.unwrap()
|
||||
.0
|
||||
.end
|
||||
.to_display_point(editor_snapshot);
|
||||
|
||||
let is_visible = visible_row_range.contains(&edit_start.row())
|
||||
|| visible_row_range.contains(&edit_end.row());
|
||||
if !is_visible {
|
||||
return None;
|
||||
}
|
||||
|
||||
match display_mode {
|
||||
EditDisplayMode::TabAccept => {
|
||||
let range = &edits.first()?.0;
|
||||
let target_display_point = range.end.to_display_point(editor_snapshot);
|
||||
|
||||
let target_line_end = DisplayPoint::new(
|
||||
target_display_point.row(),
|
||||
editor_snapshot.line_len(target_display_point.row()),
|
||||
);
|
||||
let (mut element, origin) = self.editor.update(cx, |editor, cx| {
|
||||
Some((
|
||||
editor
|
||||
.render_edit_prediction_line_popover(
|
||||
"Accept", None, window, cx,
|
||||
)?
|
||||
.into_any(),
|
||||
editor.display_to_pixel_point(
|
||||
target_line_end,
|
||||
editor_snapshot,
|
||||
window,
|
||||
)?,
|
||||
))
|
||||
})?;
|
||||
|
||||
let origin = clamp_start(start_point + origin + point(PADDING_X, px(0.)));
|
||||
element.prepaint_as_root(origin, AvailableSpace::min_size(), window, cx);
|
||||
return Some((element, origin));
|
||||
}
|
||||
EditDisplayMode::Inline => return None,
|
||||
EditDisplayMode::DiffPopover => {}
|
||||
}
|
||||
|
||||
let highlighted_edits = crate::inline_completion_edit_text(
|
||||
&snapshot,
|
||||
edits,
|
||||
edit_preview.as_ref()?,
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
|
||||
let styled_text = highlighted_edits.to_styled_text(&style.text);
|
||||
let line_count = highlighted_edits.text.lines().count();
|
||||
|
||||
const BORDER_WIDTH: Pixels = px(1.);
|
||||
|
||||
let mut element = h_flex()
|
||||
.items_start()
|
||||
.child(
|
||||
h_flex()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border(BORDER_WIDTH)
|
||||
.shadow_sm()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_l_lg()
|
||||
.when(line_count > 1, |el| el.rounded_br_lg())
|
||||
.pr_1()
|
||||
.child(styled_text),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.h(line_height + BORDER_WIDTH * px(2.))
|
||||
.px_1p5()
|
||||
.gap_1()
|
||||
// Workaround: For some reason, there's a gap if we don't do this
|
||||
.ml(-BORDER_WIDTH)
|
||||
.shadow(smallvec![gpui::BoxShadow {
|
||||
color: gpui::black().opacity(0.05),
|
||||
offset: point(px(1.), px(1.)),
|
||||
blur_radius: px(2.),
|
||||
spread_radius: px(0.),
|
||||
}])
|
||||
.bg(Editor::edit_prediction_line_popover_bg_color(cx))
|
||||
.border(BORDER_WIDTH)
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_r_lg()
|
||||
.children(editor.render_edit_prediction_accept_keybind(window, cx)),
|
||||
)
|
||||
.into_any();
|
||||
|
||||
let longest_row =
|
||||
editor_snapshot.longest_row_in_range(edit_start.row()..edit_end.row() + 1);
|
||||
let longest_line_width = if visible_row_range.contains(&longest_row) {
|
||||
line_layouts[(longest_row.0 - visible_row_range.start.0) as usize].width
|
||||
} else {
|
||||
layout_line(
|
||||
longest_row,
|
||||
editor_snapshot,
|
||||
style,
|
||||
editor_width,
|
||||
|_| false,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.width
|
||||
};
|
||||
|
||||
let viewport_bounds = Bounds::new(Default::default(), window.viewport_size())
|
||||
.extend(Edges {
|
||||
right: -Self::SCROLLBAR_WIDTH,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let x_after_longest =
|
||||
text_bounds.origin.x + longest_line_width + PADDING_X - scroll_pixel_position.x;
|
||||
|
||||
let element_bounds = element.layout_as_root(AvailableSpace::min_size(), window, cx);
|
||||
|
||||
// Fully visible if it can be displayed within the window (allow overlapping other
|
||||
// panes). However, this is only allowed if the popover starts within text_bounds.
|
||||
let can_position_to_the_right = x_after_longest < text_bounds.right()
|
||||
&& x_after_longest + element_bounds.width < viewport_bounds.right();
|
||||
|
||||
let mut origin = if can_position_to_the_right {
|
||||
point(
|
||||
x_after_longest,
|
||||
text_bounds.origin.y + edit_start.row().as_f32() * line_height
|
||||
- scroll_pixel_position.y,
|
||||
)
|
||||
} else {
|
||||
let cursor_row = newest_selection_head.map(|head| head.row());
|
||||
let above_edit = edit_start
|
||||
.row()
|
||||
.0
|
||||
.checked_sub(line_count as u32)
|
||||
.map(DisplayRow);
|
||||
let below_edit = Some(edit_end.row() + 1);
|
||||
let above_cursor = cursor_row
|
||||
.and_then(|row| row.0.checked_sub(line_count as u32).map(DisplayRow));
|
||||
let below_cursor = cursor_row.map(|cursor_row| cursor_row + 1);
|
||||
|
||||
// Place the edit popover adjacent to the edit if there is a location
|
||||
// available that is onscreen and does not obscure the cursor. Otherwise,
|
||||
// place it adjacent to the cursor.
|
||||
let row_target = [above_edit, below_edit, above_cursor, below_cursor]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.find(|&start_row| {
|
||||
let end_row = start_row + line_count as u32;
|
||||
visible_row_range.contains(&start_row)
|
||||
&& visible_row_range.contains(&end_row)
|
||||
&& cursor_row.map_or(true, |cursor_row| {
|
||||
!((start_row..end_row).contains(&cursor_row))
|
||||
})
|
||||
})?;
|
||||
|
||||
content_origin
|
||||
+ point(
|
||||
-scroll_pixel_position.x,
|
||||
row_target.as_f32() * line_height - scroll_pixel_position.y,
|
||||
)
|
||||
};
|
||||
|
||||
origin.x -= BORDER_WIDTH;
|
||||
|
||||
window.defer_draw(element, origin, 1);
|
||||
|
||||
// Do not return an element, since it will already be drawn due to defer_draw.
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn layout_mouse_context_menu(
|
||||
&self,
|
||||
editor_snapshot: &EditorSnapshot,
|
||||
@@ -6238,7 +5853,7 @@ pub(crate) struct LineWithInvisibles {
|
||||
fragments: SmallVec<[LineFragment; 1]>,
|
||||
invisibles: Vec<Invisible>,
|
||||
len: usize,
|
||||
width: Pixels,
|
||||
pub(crate) width: Pixels,
|
||||
font_size: Pixels,
|
||||
}
|
||||
|
||||
@@ -7386,22 +7001,25 @@ impl Element for EditorElement {
|
||||
});
|
||||
|
||||
let (inline_completion_popover, inline_completion_popover_origin) = self
|
||||
.layout_edit_prediction_popover(
|
||||
&text_hitbox.bounds,
|
||||
content_origin,
|
||||
&snapshot,
|
||||
start_row..end_row,
|
||||
scroll_position.y,
|
||||
scroll_position.y + height_in_lines,
|
||||
&line_layouts,
|
||||
line_height,
|
||||
scroll_pixel_position,
|
||||
newest_selection_head,
|
||||
editor_width,
|
||||
&style,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.render_edit_prediction_popover(
|
||||
&text_hitbox.bounds,
|
||||
content_origin,
|
||||
&snapshot,
|
||||
start_row..end_row,
|
||||
scroll_position.y,
|
||||
scroll_position.y + height_in_lines,
|
||||
&line_layouts,
|
||||
line_height,
|
||||
scroll_pixel_position,
|
||||
newest_selection_head,
|
||||
editor_width,
|
||||
&style,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.unzip();
|
||||
|
||||
let mut inline_diagnostics = self.layout_inline_diagnostics(
|
||||
@@ -8207,7 +7825,7 @@ struct BlockLayout {
|
||||
style: BlockStyle,
|
||||
}
|
||||
|
||||
fn layout_line(
|
||||
pub fn layout_line(
|
||||
row: DisplayRow,
|
||||
snapshot: &EditorSnapshot,
|
||||
style: &EditorStyle,
|
||||
|
||||
@@ -467,6 +467,31 @@ pub fn end_of_excerpt(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn multibuffer_header(
|
||||
map: &DisplaySnapshot,
|
||||
display_point: DisplayPoint,
|
||||
direction: Direction,
|
||||
) -> DisplayPoint {
|
||||
let point = map.display_point_to_point(display_point, Bias::Left);
|
||||
// It seems likely that the better way to implement this is to reuse block logic via
|
||||
// `map.blocks_in_range` and add support for `reversed_blocks_in_range`. I haven't evaluated in
|
||||
// depth whether this will work.
|
||||
//
|
||||
// Before thinking of that, implementation plan was to:
|
||||
//
|
||||
// * For `Direction::Prev`, use `reversed_excerpts_at` to iterate over the excerpts in reverse
|
||||
// order.
|
||||
//
|
||||
// * For `Direction::Next`, use `excerpts_at` to iterate over the excerpts.
|
||||
//
|
||||
// * Find boundaries by checking when `buffer_id` changes similar to the block_map logic
|
||||
// [here](https://github.com/zed-industries/zed/blob/e5b61949148cd87e08ae38e80949bce9b4ede9f7/crates/editor/src/display_map/block_map.rs#L845).
|
||||
//
|
||||
// Another alternative might be to use `excerpt_before` and `excerpt_after` methods to walk the
|
||||
// excerpts.
|
||||
todo!();
|
||||
}
|
||||
|
||||
/// Scans for a boundary preceding the given start point `from` until a boundary is found,
|
||||
/// indicated by the given predicate returning true.
|
||||
/// The predicate is called with the character to the left and right of the candidate boundary location.
|
||||
|
||||
@@ -298,6 +298,7 @@ impl EditorTestContext {
|
||||
self.cx.run_until_parked();
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn assert_index_text(&mut self, expected: Option<&str>) {
|
||||
let fs = self.update_editor(|editor, _, cx| {
|
||||
editor.project.as_ref().unwrap().read(cx).fs().as_fake()
|
||||
|
||||
@@ -53,10 +53,7 @@ impl RenderOnce for ExtensionCard {
|
||||
.size_full()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.bg(theme::color_alpha(
|
||||
cx.theme().colors().elevated_surface_background,
|
||||
0.8,
|
||||
))
|
||||
.bg(cx.theme().colors().elevated_surface_background.alpha(0.8))
|
||||
.child(Label::new("Overridden by dev extension.")),
|
||||
)
|
||||
}),
|
||||
|
||||
@@ -1379,7 +1379,10 @@ impl FakeFs {
|
||||
pub fn files(&self) -> Vec<PathBuf> {
|
||||
let mut result = Vec::new();
|
||||
let mut queue = collections::VecDeque::new();
|
||||
queue.push_back((PathBuf::from("/"), self.state.lock().root.clone()));
|
||||
queue.push_back((
|
||||
PathBuf::from(util::path!("/")),
|
||||
self.state.lock().root.clone(),
|
||||
));
|
||||
while let Some((path, entry)) = queue.pop_front() {
|
||||
let e = entry.lock();
|
||||
match &*e {
|
||||
@@ -2007,11 +2010,52 @@ pub fn normalize_path(path: &Path) -> PathBuf {
|
||||
ret
|
||||
}
|
||||
|
||||
pub fn copy_recursive<'a>(
|
||||
pub async fn copy_recursive<'a>(
|
||||
fs: &'a dyn Fs,
|
||||
source: &'a Path,
|
||||
target: &'a Path,
|
||||
options: CopyOptions,
|
||||
) -> Result<()> {
|
||||
for (is_dir, item) in read_dir_items(fs, source).await? {
|
||||
let Ok(item_relative_path) = item.strip_prefix(source) else {
|
||||
continue;
|
||||
};
|
||||
let target_item = target.join(item_relative_path);
|
||||
if is_dir {
|
||||
if !options.overwrite && fs.metadata(&target_item).await.is_ok_and(|m| m.is_some()) {
|
||||
if options.ignore_if_exists {
|
||||
continue;
|
||||
} else {
|
||||
return Err(anyhow!("{target_item:?} already exists"));
|
||||
}
|
||||
}
|
||||
let _ = fs
|
||||
.remove_dir(
|
||||
&target_item,
|
||||
RemoveOptions {
|
||||
recursive: true,
|
||||
ignore_if_not_exists: true,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
fs.create_dir(&target_item).await?;
|
||||
} else {
|
||||
fs.copy_file(&item, &target_item, options).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn read_dir_items<'a>(fs: &'a dyn Fs, source: &'a Path) -> Result<Vec<(bool, PathBuf)>> {
|
||||
let mut items = Vec::new();
|
||||
read_recursive(fs, source, &mut items).await?;
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
fn read_recursive<'a>(
|
||||
fs: &'a dyn Fs,
|
||||
source: &'a Path,
|
||||
output: &'a mut Vec<(bool, PathBuf)>,
|
||||
) -> BoxFuture<'a, Result<()>> {
|
||||
use futures::future::FutureExt;
|
||||
|
||||
@@ -2020,39 +2064,19 @@ pub fn copy_recursive<'a>(
|
||||
.metadata(source)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("path does not exist: {}", source.display()))?;
|
||||
if metadata.is_dir {
|
||||
if !options.overwrite && fs.metadata(target).await.is_ok_and(|m| m.is_some()) {
|
||||
if options.ignore_if_exists {
|
||||
return Ok(());
|
||||
} else {
|
||||
return Err(anyhow!("{target:?} already exists"));
|
||||
}
|
||||
}
|
||||
|
||||
let _ = fs
|
||||
.remove_dir(
|
||||
target,
|
||||
RemoveOptions {
|
||||
recursive: true,
|
||||
ignore_if_not_exists: true,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
fs.create_dir(target).await?;
|
||||
if metadata.is_dir {
|
||||
output.push((true, source.to_path_buf()));
|
||||
let mut children = fs.read_dir(source).await?;
|
||||
while let Some(child_path) = children.next().await {
|
||||
if let Ok(child_path) = child_path {
|
||||
if let Some(file_name) = child_path.file_name() {
|
||||
let child_target_path = target.join(file_name);
|
||||
copy_recursive(fs, &child_path, &child_target_path, options).await?;
|
||||
}
|
||||
read_recursive(fs, &child_path, output).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
fs.copy_file(source, target, options).await
|
||||
output.push((false, source.to_path_buf()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
@@ -2094,12 +2118,13 @@ mod tests {
|
||||
use super::*;
|
||||
use gpui::BackgroundExecutor;
|
||||
use serde_json::json;
|
||||
use util::path;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_fake_fs(executor: BackgroundExecutor) {
|
||||
let fs = FakeFs::new(executor.clone());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
path!("/root"),
|
||||
json!({
|
||||
"dir1": {
|
||||
"a": "A",
|
||||
@@ -2118,32 +2143,229 @@ mod tests {
|
||||
assert_eq!(
|
||||
fs.files(),
|
||||
vec![
|
||||
PathBuf::from("/root/dir1/a"),
|
||||
PathBuf::from("/root/dir1/b"),
|
||||
PathBuf::from("/root/dir2/c"),
|
||||
PathBuf::from("/root/dir2/dir3/d"),
|
||||
PathBuf::from(path!("/root/dir1/a")),
|
||||
PathBuf::from(path!("/root/dir1/b")),
|
||||
PathBuf::from(path!("/root/dir2/c")),
|
||||
PathBuf::from(path!("/root/dir2/dir3/d")),
|
||||
]
|
||||
);
|
||||
|
||||
fs.create_symlink("/root/dir2/link-to-dir3".as_ref(), "./dir3".into())
|
||||
fs.create_symlink(path!("/root/dir2/link-to-dir3").as_ref(), "./dir3".into())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
fs.canonicalize("/root/dir2/link-to-dir3".as_ref())
|
||||
fs.canonicalize(path!("/root/dir2/link-to-dir3").as_ref())
|
||||
.await
|
||||
.unwrap(),
|
||||
PathBuf::from("/root/dir2/dir3"),
|
||||
PathBuf::from(path!("/root/dir2/dir3")),
|
||||
);
|
||||
assert_eq!(
|
||||
fs.canonicalize("/root/dir2/link-to-dir3/d".as_ref())
|
||||
fs.canonicalize(path!("/root/dir2/link-to-dir3/d").as_ref())
|
||||
.await
|
||||
.unwrap(),
|
||||
PathBuf::from("/root/dir2/dir3/d"),
|
||||
PathBuf::from(path!("/root/dir2/dir3/d")),
|
||||
);
|
||||
assert_eq!(
|
||||
fs.load("/root/dir2/link-to-dir3/d".as_ref()).await.unwrap(),
|
||||
fs.load(path!("/root/dir2/link-to-dir3/d").as_ref())
|
||||
.await
|
||||
.unwrap(),
|
||||
"D",
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_copy_recursive(executor: BackgroundExecutor) {
|
||||
let fs = FakeFs::new(executor.clone());
|
||||
fs.insert_tree(
|
||||
path!("/outer"),
|
||||
json!({
|
||||
"inner1": {
|
||||
"a": "A",
|
||||
"b": "B",
|
||||
"inner3": {
|
||||
"d": "D",
|
||||
}
|
||||
},
|
||||
"inner2": {
|
||||
"c": "C",
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
fs.files(),
|
||||
vec![
|
||||
PathBuf::from(path!("/outer/inner1/a")),
|
||||
PathBuf::from(path!("/outer/inner1/b")),
|
||||
PathBuf::from(path!("/outer/inner2/c")),
|
||||
PathBuf::from(path!("/outer/inner1/inner3/d")),
|
||||
]
|
||||
);
|
||||
|
||||
let source = Path::new(path!("/outer"));
|
||||
let target = Path::new(path!("/outer/inner1/outer"));
|
||||
copy_recursive(fs.as_ref(), source, target, Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
fs.files(),
|
||||
vec![
|
||||
PathBuf::from(path!("/outer/inner1/a")),
|
||||
PathBuf::from(path!("/outer/inner1/b")),
|
||||
PathBuf::from(path!("/outer/inner2/c")),
|
||||
PathBuf::from(path!("/outer/inner1/inner3/d")),
|
||||
PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
|
||||
PathBuf::from(path!("/outer/inner1/outer/inner1/b")),
|
||||
PathBuf::from(path!("/outer/inner1/outer/inner2/c")),
|
||||
PathBuf::from(path!("/outer/inner1/outer/inner1/inner3/d")),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_copy_recursive_with_overwriting(executor: BackgroundExecutor) {
|
||||
let fs = FakeFs::new(executor.clone());
|
||||
fs.insert_tree(
|
||||
path!("/outer"),
|
||||
json!({
|
||||
"inner1": {
|
||||
"a": "A",
|
||||
"b": "B",
|
||||
"outer": {
|
||||
"inner1": {
|
||||
"a": "B"
|
||||
}
|
||||
}
|
||||
},
|
||||
"inner2": {
|
||||
"c": "C",
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
fs.files(),
|
||||
vec![
|
||||
PathBuf::from(path!("/outer/inner1/a")),
|
||||
PathBuf::from(path!("/outer/inner1/b")),
|
||||
PathBuf::from(path!("/outer/inner2/c")),
|
||||
PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
fs.load(path!("/outer/inner1/outer/inner1/a").as_ref())
|
||||
.await
|
||||
.unwrap(),
|
||||
"B",
|
||||
);
|
||||
|
||||
let source = Path::new(path!("/outer"));
|
||||
let target = Path::new(path!("/outer/inner1/outer"));
|
||||
copy_recursive(
|
||||
fs.as_ref(),
|
||||
source,
|
||||
target,
|
||||
CopyOptions {
|
||||
overwrite: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
fs.files(),
|
||||
vec![
|
||||
PathBuf::from(path!("/outer/inner1/a")),
|
||||
PathBuf::from(path!("/outer/inner1/b")),
|
||||
PathBuf::from(path!("/outer/inner2/c")),
|
||||
PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
|
||||
PathBuf::from(path!("/outer/inner1/outer/inner1/b")),
|
||||
PathBuf::from(path!("/outer/inner1/outer/inner2/c")),
|
||||
PathBuf::from(path!("/outer/inner1/outer/inner1/outer/inner1/a")),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
fs.load(path!("/outer/inner1/outer/inner1/a").as_ref())
|
||||
.await
|
||||
.unwrap(),
|
||||
"A"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_copy_recursive_with_ignoring(executor: BackgroundExecutor) {
|
||||
let fs = FakeFs::new(executor.clone());
|
||||
fs.insert_tree(
|
||||
path!("/outer"),
|
||||
json!({
|
||||
"inner1": {
|
||||
"a": "A",
|
||||
"b": "B",
|
||||
"outer": {
|
||||
"inner1": {
|
||||
"a": "B"
|
||||
}
|
||||
}
|
||||
},
|
||||
"inner2": {
|
||||
"c": "C",
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
fs.files(),
|
||||
vec![
|
||||
PathBuf::from(path!("/outer/inner1/a")),
|
||||
PathBuf::from(path!("/outer/inner1/b")),
|
||||
PathBuf::from(path!("/outer/inner2/c")),
|
||||
PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
fs.load(path!("/outer/inner1/outer/inner1/a").as_ref())
|
||||
.await
|
||||
.unwrap(),
|
||||
"B",
|
||||
);
|
||||
|
||||
let source = Path::new(path!("/outer"));
|
||||
let target = Path::new(path!("/outer/inner1/outer"));
|
||||
copy_recursive(
|
||||
fs.as_ref(),
|
||||
source,
|
||||
target,
|
||||
CopyOptions {
|
||||
ignore_if_exists: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
fs.files(),
|
||||
vec![
|
||||
PathBuf::from(path!("/outer/inner1/a")),
|
||||
PathBuf::from(path!("/outer/inner1/b")),
|
||||
PathBuf::from(path!("/outer/inner2/c")),
|
||||
PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
|
||||
PathBuf::from(path!("/outer/inner1/outer/inner1/b")),
|
||||
PathBuf::from(path!("/outer/inner1/outer/inner2/c")),
|
||||
PathBuf::from(path!("/outer/inner1/outer/inner1/outer/inner1/a")),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
fs.load(path!("/outer/inner1/outer/inner1/a").as_ref())
|
||||
.await
|
||||
.unwrap(),
|
||||
"B"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ log.workspace = true
|
||||
parking_lot.workspace = true
|
||||
regex.workspace = true
|
||||
rope.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
smol.workspace = true
|
||||
sum_tree.workspace = true
|
||||
|
||||
@@ -8,6 +8,9 @@ pub mod status;
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use gpui::action_with_deprecated_aliases;
|
||||
use gpui::actions;
|
||||
use gpui::impl_actions;
|
||||
use repository::PushOptions;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ffi::OsStr;
|
||||
use std::fmt;
|
||||
@@ -27,6 +30,13 @@ pub static COMMIT_MESSAGE: LazyLock<&'static OsStr> =
|
||||
LazyLock::new(|| OsStr::new("COMMIT_EDITMSG"));
|
||||
pub static INDEX_LOCK: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new("index.lock"));
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Deserialize, JsonSchema)]
|
||||
pub struct Push {
|
||||
pub options: Option<PushOptions>,
|
||||
}
|
||||
|
||||
impl_actions!(git, [Push]);
|
||||
|
||||
actions!(
|
||||
git,
|
||||
[
|
||||
@@ -43,6 +53,8 @@ actions!(
|
||||
RestoreTrackedFiles,
|
||||
TrashUntrackedFiles,
|
||||
Uncommit,
|
||||
Pull,
|
||||
Fetch,
|
||||
Commit,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -7,6 +7,8 @@ use git2::BranchType;
|
||||
use gpui::SharedString;
|
||||
use parking_lot::Mutex;
|
||||
use rope::Rope;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use std::borrow::Borrow;
|
||||
use std::io::Write as _;
|
||||
use std::process::Stdio;
|
||||
@@ -29,6 +31,12 @@ pub struct Branch {
|
||||
}
|
||||
|
||||
impl Branch {
|
||||
pub fn tracking_status(&self) -> Option<UpstreamTrackingStatus> {
|
||||
self.upstream
|
||||
.as_ref()
|
||||
.and_then(|upstream| upstream.tracking.status())
|
||||
}
|
||||
|
||||
pub fn priority_key(&self) -> (bool, Option<i64>) {
|
||||
(
|
||||
self.is_head,
|
||||
@@ -42,11 +50,32 @@ impl Branch {
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
||||
pub struct Upstream {
|
||||
pub ref_name: SharedString,
|
||||
pub tracking: Option<UpstreamTracking>,
|
||||
pub tracking: UpstreamTracking,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
||||
pub struct UpstreamTracking {
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||
pub enum UpstreamTracking {
|
||||
/// Remote ref not present in local repository.
|
||||
Gone,
|
||||
/// Remote ref present in local repository (fetched from remote).
|
||||
Tracked(UpstreamTrackingStatus),
|
||||
}
|
||||
|
||||
impl UpstreamTracking {
|
||||
pub fn is_gone(&self) -> bool {
|
||||
matches!(self, UpstreamTracking::Gone)
|
||||
}
|
||||
|
||||
pub fn status(&self) -> Option<UpstreamTrackingStatus> {
|
||||
match self {
|
||||
UpstreamTracking::Gone => None,
|
||||
UpstreamTracking::Tracked(status) => Some(*status),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||
pub struct UpstreamTrackingStatus {
|
||||
pub ahead: u32,
|
||||
pub behind: u32,
|
||||
}
|
||||
@@ -68,6 +97,11 @@ pub struct CommitDetails {
|
||||
pub committer_name: SharedString,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||||
pub struct Remote {
|
||||
pub name: SharedString,
|
||||
}
|
||||
|
||||
pub enum ResetMode {
|
||||
// reset the branch pointer, leave index and worktree unchanged
|
||||
// (this will make it look like things that were committed are now
|
||||
@@ -139,6 +173,22 @@ pub trait GitRepository: Send + Sync {
|
||||
fn unstage_paths(&self, paths: &[RepoPath]) -> Result<()>;
|
||||
|
||||
fn commit(&self, message: &str, name_and_email: Option<(&str, &str)>) -> Result<()>;
|
||||
|
||||
fn push(
|
||||
&self,
|
||||
branch_name: &str,
|
||||
upstream_name: &str,
|
||||
options: Option<PushOptions>,
|
||||
) -> Result<()>;
|
||||
fn pull(&self, branch_name: &str, upstream_name: &str) -> Result<()>;
|
||||
fn get_remotes(&self, branch_name: Option<&str>) -> Result<Vec<Remote>>;
|
||||
fn fetch(&self) -> Result<()>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, JsonSchema)]
|
||||
pub enum PushOptions {
|
||||
SetUpstream,
|
||||
Force,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for dyn GitRepository {
|
||||
@@ -165,6 +215,14 @@ impl RealGitRepository {
|
||||
hosting_provider_registry,
|
||||
}
|
||||
}
|
||||
|
||||
fn working_directory(&self) -> Result<PathBuf> {
|
||||
self.repository
|
||||
.lock()
|
||||
.workdir()
|
||||
.context("failed to read git work directory")
|
||||
.map(Path::to_path_buf)
|
||||
}
|
||||
}
|
||||
|
||||
// https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
|
||||
@@ -209,12 +267,7 @@ impl GitRepository for RealGitRepository {
|
||||
}
|
||||
|
||||
fn reset(&self, commit: &str, mode: ResetMode) -> Result<()> {
|
||||
let working_directory = self
|
||||
.repository
|
||||
.lock()
|
||||
.workdir()
|
||||
.context("failed to read git work directory")?
|
||||
.to_path_buf();
|
||||
let working_directory = self.working_directory()?;
|
||||
|
||||
let mode_flag = match mode {
|
||||
ResetMode::Mixed => "--mixed",
|
||||
@@ -238,12 +291,7 @@ impl GitRepository for RealGitRepository {
|
||||
if paths.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let working_directory = self
|
||||
.repository
|
||||
.lock()
|
||||
.workdir()
|
||||
.context("failed to read git work directory")?
|
||||
.to_path_buf();
|
||||
let working_directory = self.working_directory()?;
|
||||
|
||||
let output = new_std_command(&self.git_binary_path)
|
||||
.current_dir(&working_directory)
|
||||
@@ -296,12 +344,7 @@ impl GitRepository for RealGitRepository {
|
||||
}
|
||||
|
||||
fn set_index_text(&self, path: &RepoPath, content: Option<String>) -> anyhow::Result<()> {
|
||||
let working_directory = self
|
||||
.repository
|
||||
.lock()
|
||||
.workdir()
|
||||
.context("failed to read git work directory")?
|
||||
.to_path_buf();
|
||||
let working_directory = self.working_directory()?;
|
||||
if let Some(content) = content {
|
||||
let mut child = new_std_command(&self.git_binary_path)
|
||||
.current_dir(&working_directory)
|
||||
@@ -485,12 +528,7 @@ impl GitRepository for RealGitRepository {
|
||||
}
|
||||
|
||||
fn stage_paths(&self, paths: &[RepoPath]) -> Result<()> {
|
||||
let working_directory = self
|
||||
.repository
|
||||
.lock()
|
||||
.workdir()
|
||||
.context("failed to read git work directory")?
|
||||
.to_path_buf();
|
||||
let working_directory = self.working_directory()?;
|
||||
|
||||
if !paths.is_empty() {
|
||||
let output = new_std_command(&self.git_binary_path)
|
||||
@@ -498,6 +536,8 @@ impl GitRepository for RealGitRepository {
|
||||
.args(["update-index", "--add", "--remove", "--"])
|
||||
.args(paths.iter().map(|p| p.as_ref()))
|
||||
.output()?;
|
||||
|
||||
// TODO: Get remote response out of this and show it to the user
|
||||
if !output.status.success() {
|
||||
return Err(anyhow!(
|
||||
"Failed to stage paths:\n{}",
|
||||
@@ -509,12 +549,7 @@ impl GitRepository for RealGitRepository {
|
||||
}
|
||||
|
||||
fn unstage_paths(&self, paths: &[RepoPath]) -> Result<()> {
|
||||
let working_directory = self
|
||||
.repository
|
||||
.lock()
|
||||
.workdir()
|
||||
.context("failed to read git work directory")?
|
||||
.to_path_buf();
|
||||
let working_directory = self.working_directory()?;
|
||||
|
||||
if !paths.is_empty() {
|
||||
let output = new_std_command(&self.git_binary_path)
|
||||
@@ -522,6 +557,8 @@ impl GitRepository for RealGitRepository {
|
||||
.args(["reset", "--quiet", "--"])
|
||||
.args(paths.iter().map(|p| p.as_ref()))
|
||||
.output()?;
|
||||
|
||||
// TODO: Get remote response out of this and show it to the user
|
||||
if !output.status.success() {
|
||||
return Err(anyhow!(
|
||||
"Failed to unstage:\n{}",
|
||||
@@ -533,24 +570,21 @@ impl GitRepository for RealGitRepository {
|
||||
}
|
||||
|
||||
fn commit(&self, message: &str, name_and_email: Option<(&str, &str)>) -> Result<()> {
|
||||
let working_directory = self
|
||||
.repository
|
||||
.lock()
|
||||
.workdir()
|
||||
.context("failed to read git work directory")?
|
||||
.to_path_buf();
|
||||
let mut args = vec!["commit", "--quiet", "-m", message, "--cleanup=strip"];
|
||||
let author = name_and_email.map(|(name, email)| format!("{name} <{email}>"));
|
||||
if let Some(author) = author.as_deref() {
|
||||
args.push("--author");
|
||||
args.push(author);
|
||||
let working_directory = self.working_directory()?;
|
||||
|
||||
let mut cmd = new_std_command(&self.git_binary_path);
|
||||
cmd.current_dir(&working_directory)
|
||||
.args(["commit", "--quiet", "-m"])
|
||||
.arg(message)
|
||||
.arg("--cleanup=strip");
|
||||
|
||||
if let Some((name, email)) = name_and_email {
|
||||
cmd.arg("--author").arg(&format!("{name} <{email}>"));
|
||||
}
|
||||
|
||||
let output = new_std_command(&self.git_binary_path)
|
||||
.current_dir(&working_directory)
|
||||
.args(args)
|
||||
.output()?;
|
||||
let output = cmd.output()?;
|
||||
|
||||
// TODO: Get remote response out of this and show it to the user
|
||||
if !output.status.success() {
|
||||
return Err(anyhow!(
|
||||
"Failed to commit:\n{}",
|
||||
@@ -559,6 +593,118 @@ impl GitRepository for RealGitRepository {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn push(
|
||||
&self,
|
||||
branch_name: &str,
|
||||
remote_name: &str,
|
||||
options: Option<PushOptions>,
|
||||
) -> Result<()> {
|
||||
let working_directory = self.working_directory()?;
|
||||
|
||||
let output = new_std_command(&self.git_binary_path)
|
||||
.current_dir(&working_directory)
|
||||
.args(["push", "--quiet"])
|
||||
.args(options.map(|option| match option {
|
||||
PushOptions::SetUpstream => "--set-upstream",
|
||||
PushOptions::Force => "--force-with-lease",
|
||||
}))
|
||||
.arg(remote_name)
|
||||
.arg(format!("{}:{}", branch_name, branch_name))
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(anyhow!(
|
||||
"Failed to push:\n{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
}
|
||||
|
||||
// TODO: Get remote response out of this and show it to the user
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pull(&self, branch_name: &str, remote_name: &str) -> Result<()> {
|
||||
let working_directory = self.working_directory()?;
|
||||
|
||||
let output = new_std_command(&self.git_binary_path)
|
||||
.current_dir(&working_directory)
|
||||
.args(["pull", "--quiet"])
|
||||
.arg(remote_name)
|
||||
.arg(branch_name)
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(anyhow!(
|
||||
"Failed to pull:\n{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
}
|
||||
|
||||
// TODO: Get remote response out of this and show it to the user
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fetch(&self) -> Result<()> {
|
||||
let working_directory = self.working_directory()?;
|
||||
|
||||
let output = new_std_command(&self.git_binary_path)
|
||||
.current_dir(&working_directory)
|
||||
.args(["fetch", "--quiet", "--all"])
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(anyhow!(
|
||||
"Failed to fetch:\n{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
}
|
||||
|
||||
// TODO: Get remote response out of this and show it to the user
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_remotes(&self, branch_name: Option<&str>) -> Result<Vec<Remote>> {
|
||||
let working_directory = self.working_directory()?;
|
||||
|
||||
if let Some(branch_name) = branch_name {
|
||||
let output = new_std_command(&self.git_binary_path)
|
||||
.current_dir(&working_directory)
|
||||
.args(["config", "--get"])
|
||||
.arg(format!("branch.{}.remote", branch_name))
|
||||
.output()?;
|
||||
|
||||
if output.status.success() {
|
||||
let remote_name = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
return Ok(vec![Remote {
|
||||
name: remote_name.trim().to_string().into(),
|
||||
}]);
|
||||
}
|
||||
}
|
||||
|
||||
let output = new_std_command(&self.git_binary_path)
|
||||
.current_dir(&working_directory)
|
||||
.args(["remote"])
|
||||
.output()?;
|
||||
|
||||
if output.status.success() {
|
||||
let remote_names = String::from_utf8_lossy(&output.stdout)
|
||||
.split('\n')
|
||||
.filter(|name| !name.is_empty())
|
||||
.map(|name| Remote {
|
||||
name: name.trim().to_string().into(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
return Ok(remote_names);
|
||||
} else {
|
||||
return Err(anyhow!(
|
||||
"Failed to get remotes:\n{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -743,6 +889,22 @@ impl GitRepository for FakeGitRepository {
|
||||
fn commit(&self, _message: &str, _name_and_email: Option<(&str, &str)>) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn push(&self, _branch: &str, _remote: &str, _options: Option<PushOptions>) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn pull(&self, _branch: &str, _remote: &str) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn fetch(&self) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn get_remotes(&self, _branch: Option<&str>) -> Result<Vec<Remote>> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {
|
||||
@@ -911,9 +1073,9 @@ fn parse_branch_input(input: &str) -> Result<Vec<Branch>> {
|
||||
Ok(branches)
|
||||
}
|
||||
|
||||
fn parse_upstream_track(upstream_track: &str) -> Result<Option<UpstreamTracking>> {
|
||||
fn parse_upstream_track(upstream_track: &str) -> Result<UpstreamTracking> {
|
||||
if upstream_track == "" {
|
||||
return Ok(Some(UpstreamTracking {
|
||||
return Ok(UpstreamTracking::Tracked(UpstreamTrackingStatus {
|
||||
ahead: 0,
|
||||
behind: 0,
|
||||
}));
|
||||
@@ -929,7 +1091,7 @@ fn parse_upstream_track(upstream_track: &str) -> Result<Option<UpstreamTracking>
|
||||
let mut behind: u32 = 0;
|
||||
for component in upstream_track.split(", ") {
|
||||
if component == "gone" {
|
||||
return Ok(None);
|
||||
return Ok(UpstreamTracking::Gone);
|
||||
}
|
||||
if let Some(ahead_num) = component.strip_prefix("ahead ") {
|
||||
ahead = ahead_num.parse::<u32>()?;
|
||||
@@ -938,7 +1100,10 @@ fn parse_upstream_track(upstream_track: &str) -> Result<Option<UpstreamTracking>
|
||||
behind = behind_num.parse::<u32>()?;
|
||||
}
|
||||
}
|
||||
Ok(Some(UpstreamTracking { ahead, behind }))
|
||||
Ok(UpstreamTracking::Tracked(UpstreamTrackingStatus {
|
||||
ahead,
|
||||
behind,
|
||||
}))
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -953,7 +1118,7 @@ fn test_branches_parsing() {
|
||||
name: "zed-patches".into(),
|
||||
upstream: Some(Upstream {
|
||||
ref_name: "refs/remotes/origin/zed-patches".into(),
|
||||
tracking: Some(UpstreamTracking {
|
||||
tracking: UpstreamTracking::Tracked(UpstreamTrackingStatus {
|
||||
ahead: 0,
|
||||
behind: 0
|
||||
})
|
||||
|
||||
@@ -261,7 +261,7 @@ impl PickerDelegate for BranchListDelegate {
|
||||
.project()
|
||||
.read(cx)
|
||||
.active_repository(cx)
|
||||
.and_then(|repo| repo.read(cx).branch())
|
||||
.and_then(|repo| repo.read(cx).current_branch())
|
||||
.map(|branch| branch.name.to_string())
|
||||
})
|
||||
.ok()
|
||||
|
||||
@@ -4,13 +4,17 @@ use crate::git_panel::{commit_message_editor, GitPanel};
|
||||
use crate::repository_selector::RepositorySelector;
|
||||
use anyhow::Result;
|
||||
use git::Commit;
|
||||
use language::language_settings::LanguageSettings;
|
||||
use language::Buffer;
|
||||
use panel::{panel_editor_container, panel_editor_style, panel_filled_button, panel_icon_button};
|
||||
use panel::{
|
||||
panel_button, panel_editor_container, panel_editor_style, panel_filled_button,
|
||||
panel_icon_button,
|
||||
};
|
||||
use settings::Settings;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{prelude::*, Tooltip};
|
||||
use ui::{prelude::*, KeybindingHint, Tooltip};
|
||||
|
||||
use editor::{Editor, EditorElement, EditorMode, MultiBuffer};
|
||||
use editor::{Direction, Editor, EditorElement, EditorMode, EditorSettings, MultiBuffer};
|
||||
use gpui::*;
|
||||
use project::git::Repository;
|
||||
use project::{Fs, Project};
|
||||
@@ -18,6 +22,8 @@ use std::sync::Arc;
|
||||
use workspace::dock::{Dock, DockPosition, PanelHandle};
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
// actions!(commit_modal, [NextSuggestion, PrevSuggestion]);
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
cx.observe_new(|workspace: &mut Workspace, window, cx| {
|
||||
let Some(window) = window else {
|
||||
@@ -32,6 +38,8 @@ pub struct CommitModal {
|
||||
git_panel: Entity<GitPanel>,
|
||||
commit_editor: Entity<Editor>,
|
||||
restore_dock: RestoreDock,
|
||||
current_suggestion: Option<usize>,
|
||||
suggested_messages: Vec<SharedString>,
|
||||
}
|
||||
|
||||
impl Focusable for CommitModal {
|
||||
@@ -114,6 +122,7 @@ impl CommitModal {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let panel = git_panel.read(cx);
|
||||
let suggested_message = panel.suggest_commit_message();
|
||||
|
||||
let commit_editor = git_panel.update(cx, |git_panel, cx| {
|
||||
git_panel.set_modal_open(true, cx);
|
||||
@@ -122,36 +131,276 @@ impl CommitModal {
|
||||
cx.new(|cx| commit_message_editor(buffer, project.clone(), false, window, cx))
|
||||
});
|
||||
|
||||
let commit_message = commit_editor.read(cx).text(cx);
|
||||
|
||||
if let Some(suggested_message) = suggested_message {
|
||||
if commit_message.is_empty() {
|
||||
commit_editor.update(cx, |editor, cx| {
|
||||
editor.set_text(suggested_message, window, cx);
|
||||
editor.select_all(&Default::default(), window, cx);
|
||||
});
|
||||
} else {
|
||||
if commit_message.as_str().trim() == suggested_message.trim() {
|
||||
commit_editor.update(cx, |editor, cx| {
|
||||
// select the message to make it easy to delete
|
||||
editor.select_all(&Default::default(), window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
git_panel,
|
||||
commit_editor,
|
||||
restore_dock,
|
||||
current_suggestion: None,
|
||||
suggested_messages: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns container `(width, x padding, border radius)`
|
||||
fn container_properties(&self, window: &mut Window, cx: &mut Context<Self>) -> (f32, f32, f32) {
|
||||
// TODO: Let's set the width based on your set wrap guide if possible
|
||||
|
||||
// let settings = EditorSettings::get_global(cx);
|
||||
|
||||
// let first_wrap_guide = self
|
||||
// .commit_editor
|
||||
// .read(cx)
|
||||
// .wrap_guides(cx)
|
||||
// .iter()
|
||||
// .next()
|
||||
// .map(|(guide, active)| if *active { Some(*guide) } else { None })
|
||||
// .flatten();
|
||||
|
||||
// let preferred_width = if let Some(guide) = first_wrap_guide {
|
||||
// guide
|
||||
// } else {
|
||||
// 80
|
||||
// };
|
||||
|
||||
let border_radius = 16.0;
|
||||
|
||||
let preferred_width = 50; // (chars wide)
|
||||
|
||||
let mut width = 460.0;
|
||||
let padding_x = 16.0;
|
||||
|
||||
let mut snapshot = self
|
||||
.commit_editor
|
||||
.update(cx, |editor, cx| editor.snapshot(window, cx));
|
||||
let style = window.text_style().clone();
|
||||
|
||||
let font_id = window.text_system().resolve_font(&style.font());
|
||||
let font_size = style.font_size.to_pixels(window.rem_size());
|
||||
let line_height = style.line_height_in_pixels(window.rem_size());
|
||||
if let Ok(em_width) = window.text_system().em_width(font_id, font_size) {
|
||||
width = preferred_width as f32 * em_width.0 + (padding_x * 2.0);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
// cx.notify();
|
||||
|
||||
(width, padding_x, border_radius)
|
||||
}
|
||||
|
||||
// fn cycle_suggested_messages(&mut self, direction: Direction, cx: &mut Context<Self>) {
|
||||
// let new_index = match direction {
|
||||
// Direction::Next => {
|
||||
// (self.current_suggestion.unwrap_or(0) + 1).rem_euclid(self.suggested_messages.len())
|
||||
// }
|
||||
// Direction::Prev => {
|
||||
// (self.current_suggestion.unwrap_or(0) + self.suggested_messages.len() - 1)
|
||||
// .rem_euclid(self.suggested_messages.len())
|
||||
// }
|
||||
// };
|
||||
// self.current_suggestion = Some(new_index);
|
||||
|
||||
// cx.notify();
|
||||
// }
|
||||
|
||||
// fn next_suggestion(&mut self, _: &NextSuggestion, window: &mut Window, cx: &mut Context<Self>) {
|
||||
// self.current_suggestion = Some(1);
|
||||
// self.apply_suggestion(window, cx);
|
||||
// }
|
||||
|
||||
// fn prev_suggestion(&mut self, _: &PrevSuggestion, window: &mut Window, cx: &mut Context<Self>) {
|
||||
// self.current_suggestion = Some(0);
|
||||
// self.apply_suggestion(window, cx);
|
||||
// }
|
||||
|
||||
// fn set_commit_message(&mut self, message: &str, window: &mut Window, cx: &mut Context<Self>) {
|
||||
// self.commit_editor.update(cx, |editor, cx| {
|
||||
// editor.set_text(message.to_string(), window, cx)
|
||||
// });
|
||||
// self.current_suggestion = Some(0);
|
||||
// cx.notify();
|
||||
// }
|
||||
|
||||
// fn apply_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
// let suggested_messages = self.suggested_messages.clone();
|
||||
|
||||
// if let Some(suggestion) = self.current_suggestion {
|
||||
// let suggested_message = &suggested_messages[suggestion];
|
||||
|
||||
// self.set_commit_message(suggested_message, window, cx);
|
||||
// }
|
||||
|
||||
// cx.notify();
|
||||
// }
|
||||
|
||||
fn commit_editor_element(&self, window: &mut Window, cx: &mut Context<Self>) -> EditorElement {
|
||||
let mut editor = self.commit_editor.clone();
|
||||
|
||||
let editor_style = panel_editor_style(true, window, cx);
|
||||
|
||||
EditorElement::new(&self.commit_editor, editor_style)
|
||||
}
|
||||
|
||||
pub fn render_commit_editor(
|
||||
&self,
|
||||
name_and_email: Option<(SharedString, SharedString)>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let editor = self.commit_editor.clone();
|
||||
let (width, padding_x, modal_border_radius) = self.container_properties(window, cx);
|
||||
|
||||
let panel_editor_style = panel_editor_style(true, window, cx);
|
||||
let border_radius = modal_border_radius - padding_x / 2.0;
|
||||
|
||||
let editor = self.commit_editor.clone();
|
||||
let editor_focus_handle = editor.focus_handle(cx);
|
||||
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let line_height = relative(settings.buffer_line_height.value())
|
||||
.to_pixels(settings.buffer_font_size(cx).into(), window.rem_size());
|
||||
|
||||
let mut snapshot = self
|
||||
.commit_editor
|
||||
.update(cx, |editor, cx| editor.snapshot(window, cx));
|
||||
let style = window.text_style().clone();
|
||||
|
||||
let font_id = window.text_system().resolve_font(&style.font());
|
||||
let font_size = style.font_size.to_pixels(window.rem_size());
|
||||
let line_height = style.line_height_in_pixels(window.rem_size());
|
||||
let em_width = window.text_system().em_width(font_id, font_size);
|
||||
|
||||
let (branch, tooltip, commit_label, co_authors) =
|
||||
self.git_panel.update(cx, |git_panel, cx| {
|
||||
let branch = git_panel
|
||||
.active_repository
|
||||
.as_ref()
|
||||
.and_then(|repo| repo.read(cx).current_branch().map(|b| b.name.clone()))
|
||||
.unwrap_or_else(|| "<no branch>".into());
|
||||
let tooltip = if git_panel.has_staged_changes() {
|
||||
"Commit staged changes"
|
||||
} else {
|
||||
"Commit changes to tracked files"
|
||||
};
|
||||
let title = if git_panel.has_staged_changes() {
|
||||
"Commit"
|
||||
} else {
|
||||
"Commit Tracked"
|
||||
};
|
||||
let co_authors = git_panel.render_co_authors(cx);
|
||||
(branch, tooltip, title, co_authors)
|
||||
});
|
||||
|
||||
let branch_selector = panel_button(branch)
|
||||
.icon(IconName::GitBranch)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Placeholder)
|
||||
.color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.tooltip(Tooltip::for_action_title(
|
||||
"Switch Branch",
|
||||
&zed_actions::git::Branch,
|
||||
))
|
||||
.on_click(cx.listener(|_, _, window, cx| {
|
||||
window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx);
|
||||
}))
|
||||
.style(ButtonStyle::Transparent);
|
||||
|
||||
let changes_count = self.git_panel.read(cx).total_staged_count();
|
||||
|
||||
let close_kb_hint =
|
||||
if let Some(close_kb) = ui::KeyBinding::for_action(&menu::Cancel, window, cx) {
|
||||
Some(
|
||||
KeybindingHint::new(close_kb, cx.theme().colors().editor_background)
|
||||
.suffix("Cancel"),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let fake_commit_kb =
|
||||
ui::KeyBinding::new(gpui::KeyBinding::new("cmd-enter", gpui::NoAction, None), cx);
|
||||
|
||||
let commit_hint =
|
||||
KeybindingHint::new(fake_commit_kb, cx.theme().colors().editor_background)
|
||||
.suffix(commit_label);
|
||||
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
|
||||
// let next_suggestion_kb =
|
||||
// ui::KeyBinding::for_action_in(&NextSuggestion, &focus_handle.clone(), window, cx);
|
||||
// let next_suggestion_hint = next_suggestion_kb.map(|kb| {
|
||||
// KeybindingHint::new(kb, cx.theme().colors().editor_background).suffix("Next Suggestion")
|
||||
// });
|
||||
|
||||
// let prev_suggestion_kb =
|
||||
// ui::KeyBinding::for_action_in(&PrevSuggestion, &focus_handle.clone(), window, cx);
|
||||
// let prev_suggestion_hint = prev_suggestion_kb.map(|kb| {
|
||||
// KeybindingHint::new(kb, cx.theme().colors().editor_background)
|
||||
// .suffix("Previous Suggestion")
|
||||
// });
|
||||
|
||||
v_flex()
|
||||
.justify_between()
|
||||
.relative()
|
||||
.w_full()
|
||||
.h_full()
|
||||
.pt_2()
|
||||
.id("editor-container")
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(EditorElement::new(&self.commit_editor, panel_editor_style))
|
||||
.child(self.render_footer(window, cx))
|
||||
.flex_1()
|
||||
.size_full()
|
||||
.rounded(px(border_radius))
|
||||
.overflow_hidden()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.py_2()
|
||||
.px_3()
|
||||
.on_click(cx.listener(move |_, _: &ClickEvent, window, _cx| {
|
||||
window.focus(&editor_focus_handle);
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
.size_full()
|
||||
.flex_1()
|
||||
.child(self.commit_editor_element(window, cx)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.group("commit_editor_footer")
|
||||
.flex_none()
|
||||
.w_full()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.w_full()
|
||||
.pt_2()
|
||||
.pb_0p5()
|
||||
.gap_1()
|
||||
.child(h_flex().gap_1().child(branch_selector).children(co_authors))
|
||||
.child(div().flex_1())
|
||||
.child(
|
||||
h_flex()
|
||||
.opacity(0.7)
|
||||
.group_hover("commit_editor_footer", |this| this.opacity(1.0))
|
||||
.items_center()
|
||||
.justify_end()
|
||||
.flex_none()
|
||||
.px_1()
|
||||
.gap_4()
|
||||
.children(close_kb_hint)
|
||||
// .children(next_suggestion_hint)
|
||||
.child(commit_hint),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn render_footer(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
@@ -159,7 +408,12 @@ impl CommitModal {
|
||||
let branch = git_panel
|
||||
.active_repository
|
||||
.as_ref()
|
||||
.and_then(|repo| repo.read(cx).branch().map(|b| b.name.clone()))
|
||||
.and_then(|repo| {
|
||||
repo.read(cx)
|
||||
.repository_entry
|
||||
.branch()
|
||||
.map(|b| b.name.clone())
|
||||
})
|
||||
.unwrap_or_else(|| "<no branch>".into());
|
||||
let tooltip = if git_panel.has_staged_changes() {
|
||||
"Commit staged changes"
|
||||
@@ -175,13 +429,10 @@ impl CommitModal {
|
||||
(branch, tooltip, title, co_authors)
|
||||
});
|
||||
|
||||
let branch_selector = Button::new("branch-selector", branch)
|
||||
.color(Color::Muted)
|
||||
.style(ButtonStyle::Subtle)
|
||||
let branch_selector = panel_button(branch)
|
||||
.icon(IconName::GitBranch)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.size(ButtonSize::Compact)
|
||||
.icon_position(IconPosition::Start)
|
||||
.tooltip(Tooltip::for_action_title(
|
||||
"Switch Branch",
|
||||
@@ -191,13 +442,29 @@ impl CommitModal {
|
||||
window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx);
|
||||
}))
|
||||
.style(ButtonStyle::Transparent);
|
||||
|
||||
let changes_count = self.git_panel.read(cx).total_staged_count();
|
||||
|
||||
let close_kb_hint =
|
||||
if let Some(close_kb) = ui::KeyBinding::for_action(&menu::Cancel, window, cx) {
|
||||
Some(
|
||||
KeybindingHint::new(close_kb, cx.theme().colors().editor_background)
|
||||
.suffix("Cancel"),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.items_center()
|
||||
.h(px(36.0))
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(branch_selector)
|
||||
.px_3()
|
||||
.child(h_flex().child(branch_selector))
|
||||
.child(
|
||||
h_flex().children(co_authors).child(
|
||||
panel_filled_button(title)
|
||||
h_flex().gap_1p5().children(co_authors).child(
|
||||
Button::new("stage-button", title)
|
||||
.tooltip(Tooltip::for_action_title(tooltip, &git::Commit))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.commit(&Default::default(), window, cx);
|
||||
@@ -206,6 +473,10 @@ impl CommitModal {
|
||||
)
|
||||
}
|
||||
|
||||
fn border_radius(&self) -> f32 {
|
||||
8.0
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
@@ -218,27 +489,33 @@ impl CommitModal {
|
||||
|
||||
impl Render for CommitModal {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
|
||||
let (width, _, border_radius) = self.container_properties(window, cx);
|
||||
|
||||
v_flex()
|
||||
.id("commit-modal")
|
||||
.key_context("GitCommit")
|
||||
.elevation_3(cx)
|
||||
.overflow_hidden()
|
||||
.on_action(cx.listener(Self::dismiss))
|
||||
.on_action(cx.listener(Self::commit))
|
||||
// .on_action(cx.listener(Self::next_suggestion))
|
||||
// .on_action(cx.listener(Self::prev_suggestion))
|
||||
.relative()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.rounded(px(16.))
|
||||
.justify_between()
|
||||
.bg(cx.theme().colors().elevated_surface_background)
|
||||
.rounded(px(border_radius))
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.py_2()
|
||||
.px_4()
|
||||
.w(px(480.))
|
||||
.min_h(rems(18.))
|
||||
.w(px(width))
|
||||
.h(px(360.))
|
||||
.flex_1()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.p_2()
|
||||
.child(self.render_commit_editor(None, window, cx)),
|
||||
)
|
||||
// .child(self.render_footer(window, cx))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::repository_selector::RepositorySelectorPopoverMenu;
|
||||
use crate::{
|
||||
git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector,
|
||||
};
|
||||
use crate::{project_diff, ProjectDiff};
|
||||
use crate::{picker_prompt, project_diff, ProjectDiff};
|
||||
use collections::HashMap;
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use editor::commit_tooltip::CommitTooltip;
|
||||
@@ -12,9 +12,9 @@ use editor::{
|
||||
scroll::ScrollbarAutoHide, Editor, EditorElement, EditorMode, EditorSettings, MultiBuffer,
|
||||
ShowScrollbar,
|
||||
};
|
||||
use git::repository::{CommitDetails, ResetMode};
|
||||
use git::repository::{Branch, CommitDetails, PushOptions, Remote, ResetMode, UpstreamTracking};
|
||||
use git::{repository::RepoPath, status::FileStatus, Commit, ToggleStaged};
|
||||
use git::{RestoreTrackedFiles, StageAll, TrashUntrackedFiles, UnstageAll};
|
||||
use git::{Push, RestoreTrackedFiles, StageAll, TrashUntrackedFiles, UnstageAll};
|
||||
use gpui::*;
|
||||
use itertools::Itertools;
|
||||
use language::{Buffer, File};
|
||||
@@ -27,6 +27,9 @@ use project::{
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings as _;
|
||||
use std::cell::RefCell;
|
||||
use std::future::Future;
|
||||
use std::rc::Rc;
|
||||
use std::{collections::HashSet, path::PathBuf, sync::Arc, time::Duration, usize};
|
||||
use strum::{IntoEnumIterator, VariantNames};
|
||||
use time::OffsetDateTime;
|
||||
@@ -34,7 +37,7 @@ use ui::{
|
||||
prelude::*, ButtonLike, Checkbox, ContextMenu, Divider, DividerColor, ElevationIndex, ListItem,
|
||||
ListItemSpacing, Scrollbar, ScrollbarState, Tooltip,
|
||||
};
|
||||
use util::{maybe, ResultExt, TryFutureExt};
|
||||
use util::{maybe, post_inc, ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
notifications::{DetachAndPromptErr, NotificationId},
|
||||
@@ -174,9 +177,14 @@ struct PendingOperation {
|
||||
op_id: usize,
|
||||
}
|
||||
|
||||
type RemoteOperations = Rc<RefCell<HashSet<u32>>>;
|
||||
|
||||
pub struct GitPanel {
|
||||
remote_operation_id: u32,
|
||||
pending_remote_operations: RemoteOperations,
|
||||
pub(crate) active_repository: Option<Entity<Repository>>,
|
||||
commit_editor: Entity<Editor>,
|
||||
suggested_commit_message: Option<String>,
|
||||
conflicted_count: usize,
|
||||
conflicted_staged_count: usize,
|
||||
current_modifiers: Modifiers,
|
||||
@@ -206,6 +214,17 @@ pub struct GitPanel {
|
||||
modal_open: bool,
|
||||
}
|
||||
|
||||
struct RemoteOperationGuard {
|
||||
id: u32,
|
||||
pending_remote_operations: RemoteOperations,
|
||||
}
|
||||
|
||||
impl Drop for RemoteOperationGuard {
|
||||
fn drop(&mut self) {
|
||||
self.pending_remote_operations.borrow_mut().remove(&self.id);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn commit_message_editor(
|
||||
commit_message_buffer: Entity<Buffer>,
|
||||
project: Entity<Project>,
|
||||
@@ -286,8 +305,11 @@ impl GitPanel {
|
||||
cx.new(|cx| RepositorySelector::new(project.clone(), window, cx));
|
||||
|
||||
let mut git_panel = Self {
|
||||
pending_remote_operations: Default::default(),
|
||||
remote_operation_id: 0,
|
||||
active_repository,
|
||||
commit_editor,
|
||||
suggested_commit_message: None,
|
||||
conflicted_count: 0,
|
||||
conflicted_staged_count: 0,
|
||||
current_modifiers: window.modifiers(),
|
||||
@@ -341,6 +363,16 @@ impl GitPanel {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn start_remote_operation(&mut self) -> RemoteOperationGuard {
|
||||
let id = post_inc(&mut self.remote_operation_id);
|
||||
self.pending_remote_operations.borrow_mut().insert(id);
|
||||
|
||||
RemoteOperationGuard {
|
||||
id,
|
||||
pending_remote_operations: self.pending_remote_operations.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize(&mut self, cx: &mut Context<Self>) {
|
||||
let width = self.width;
|
||||
self.pending_serialization = cx.background_spawn(
|
||||
@@ -1008,6 +1040,10 @@ impl GitPanel {
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn total_staged_count(&self) -> usize {
|
||||
self.tracked_staged_count + self.new_staged_count + self.conflicted_staged_count
|
||||
}
|
||||
|
||||
pub fn commit_message_buffer(&self, cx: &App) -> Entity<Buffer> {
|
||||
self.commit_editor
|
||||
.read(cx)
|
||||
@@ -1029,17 +1065,15 @@ impl GitPanel {
|
||||
}
|
||||
}
|
||||
|
||||
/// Commit all staged changes
|
||||
fn commit(&mut self, _: &git::Commit, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let editor = self.commit_editor.read(cx);
|
||||
if editor.is_empty(cx) {
|
||||
if !editor.focus_handle(cx).contains_focused(window, cx) {
|
||||
editor.focus_handle(cx).focus(window);
|
||||
return;
|
||||
}
|
||||
if self
|
||||
.commit_editor
|
||||
.focus_handle(cx)
|
||||
.contains_focused(window, cx)
|
||||
{
|
||||
self.commit_changes(window, cx)
|
||||
}
|
||||
|
||||
self.commit_changes(window, cx)
|
||||
cx.propagate();
|
||||
}
|
||||
|
||||
pub(crate) fn commit_changes(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
@@ -1121,23 +1155,44 @@ impl GitPanel {
|
||||
let Some(repo) = self.active_repository.clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// TODO: Use git merge-base to find the upstream and main branch split
|
||||
let confirmation = Task::ready(true);
|
||||
// let confirmation = if self.commit_editor.read(cx).is_empty(cx) {
|
||||
// Task::ready(true)
|
||||
// } else {
|
||||
// let prompt = window.prompt(
|
||||
// PromptLevel::Warning,
|
||||
// "Uncomitting will replace the current commit message with the previous commit's message",
|
||||
// None,
|
||||
// &["Ok", "Cancel"],
|
||||
// cx,
|
||||
// );
|
||||
// cx.spawn(|_, _| async move { prompt.await.is_ok_and(|i| i == 0) })
|
||||
// };
|
||||
|
||||
let prior_head = self.load_commit_details("HEAD", cx);
|
||||
|
||||
let task = cx.spawn(|_, mut cx| async move {
|
||||
let prior_head = prior_head.await?;
|
||||
|
||||
repo.update(&mut cx, |repo, _| repo.reset("HEAD^", ResetMode::Soft))?
|
||||
.await??;
|
||||
|
||||
Ok(prior_head)
|
||||
});
|
||||
|
||||
let task = cx.spawn_in(window, |this, mut cx| async move {
|
||||
let result = task.await;
|
||||
let result = maybe!(async {
|
||||
if !confirmation.await {
|
||||
Ok(None)
|
||||
} else {
|
||||
let prior_head = prior_head.await?;
|
||||
|
||||
repo.update(&mut cx, |repo, _| repo.reset("HEAD^", ResetMode::Soft))?
|
||||
.await??;
|
||||
|
||||
Ok(Some(prior_head))
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
this.update_in(&mut cx, |this, window, cx| {
|
||||
this.pending_commit.take();
|
||||
match result {
|
||||
Ok(prior_commit) => {
|
||||
Ok(None) => {}
|
||||
Ok(Some(prior_commit)) => {
|
||||
this.commit_editor.update(cx, |editor, cx| {
|
||||
editor.set_text(prior_commit.message, window, cx)
|
||||
});
|
||||
@@ -1151,6 +1206,176 @@ impl GitPanel {
|
||||
self.pending_commit = Some(task);
|
||||
}
|
||||
|
||||
/// Suggests a commit message based on the changed files and their statuses
|
||||
pub fn suggest_commit_message(&self) -> Option<String> {
|
||||
let entries = self
|
||||
.entries
|
||||
.iter()
|
||||
.filter_map(|entry| {
|
||||
if let GitListEntry::GitStatusEntry(status_entry) = entry {
|
||||
Some(status_entry)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<&GitStatusEntry>>();
|
||||
|
||||
if entries.is_empty() {
|
||||
None
|
||||
} else if entries.len() == 1 {
|
||||
let entry = &entries[0];
|
||||
let file_name = entry
|
||||
.repo_path
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy();
|
||||
|
||||
if entry.status.is_deleted() {
|
||||
Some(format!("Delete {}", file_name))
|
||||
} else if entry.status.is_created() {
|
||||
Some(format!("Create {}", file_name))
|
||||
} else if entry.status.is_modified() {
|
||||
Some(format!("Update {}", file_name))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn update_editor_placeholder(&mut self, cx: &mut Context<Self>) {
|
||||
let suggested_commit_message = self.suggest_commit_message();
|
||||
self.suggested_commit_message = suggested_commit_message.clone();
|
||||
|
||||
if let Some(suggested_commit_message) = suggested_commit_message {
|
||||
self.commit_editor.update(cx, |editor, cx| {
|
||||
editor.set_placeholder_text(Arc::from(suggested_commit_message), cx)
|
||||
});
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn fetch(&mut self, _: &git::Fetch, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(repo) = self.active_repository.clone() else {
|
||||
return;
|
||||
};
|
||||
let guard = self.start_remote_operation();
|
||||
let fetch = repo.read(cx).fetch();
|
||||
cx.spawn(|_, _| async move {
|
||||
fetch.await??;
|
||||
drop(guard);
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn pull(&mut self, _: &git::Pull, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let guard = self.start_remote_operation();
|
||||
let remote = self.get_current_remote(window, cx);
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
let remote = remote.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let Some(repo) = this.active_repository.clone() else {
|
||||
return Err(anyhow::anyhow!("No active repository"));
|
||||
};
|
||||
|
||||
let Some(branch) = repo.read(cx).current_branch() else {
|
||||
return Err(anyhow::anyhow!("No active branch"));
|
||||
};
|
||||
|
||||
Ok(repo.read(cx).pull(branch.name.clone(), remote.name))
|
||||
})??
|
||||
.await??;
|
||||
|
||||
drop(guard);
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn push(&mut self, action: &git::Push, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let guard = self.start_remote_operation();
|
||||
let options = action.options;
|
||||
let remote = self.get_current_remote(window, cx);
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
let remote = remote.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let Some(repo) = this.active_repository.clone() else {
|
||||
return Err(anyhow::anyhow!("No active repository"));
|
||||
};
|
||||
|
||||
let Some(branch) = repo.read(cx).current_branch() else {
|
||||
return Err(anyhow::anyhow!("No active branch"));
|
||||
};
|
||||
|
||||
Ok(repo
|
||||
.read(cx)
|
||||
.push(branch.name.clone(), remote.name, options))
|
||||
})??
|
||||
.await??;
|
||||
|
||||
drop(guard);
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn get_current_remote(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl Future<Output = Result<Remote>> {
|
||||
let repo = self.active_repository.clone();
|
||||
let workspace = self.workspace.clone();
|
||||
let mut cx = window.to_async(cx);
|
||||
|
||||
async move {
|
||||
let Some(repo) = repo else {
|
||||
return Err(anyhow::anyhow!("No active repository"));
|
||||
};
|
||||
|
||||
let mut current_remotes: Vec<Remote> = repo
|
||||
.update(&mut cx, |repo, cx| {
|
||||
let Some(current_branch) = repo.current_branch() else {
|
||||
return Err(anyhow::anyhow!("No active branch"));
|
||||
};
|
||||
|
||||
Ok(repo.get_remotes(Some(current_branch.name.to_string()), cx))
|
||||
})??
|
||||
.await?;
|
||||
|
||||
if current_remotes.len() == 0 {
|
||||
return Err(anyhow::anyhow!("No active remote"));
|
||||
} else if current_remotes.len() == 1 {
|
||||
return Ok(current_remotes.pop().unwrap());
|
||||
} else {
|
||||
let current_remotes: Vec<_> = current_remotes
|
||||
.into_iter()
|
||||
.map(|remotes| remotes.name)
|
||||
.collect();
|
||||
let selection = cx
|
||||
.update(|window, cx| {
|
||||
picker_prompt::prompt(
|
||||
"Pick which remote to push to",
|
||||
current_remotes.clone(),
|
||||
workspace,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
return Ok(Remote {
|
||||
name: current_remotes[selection].clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn potential_co_authors(&self, cx: &App) -> Vec<(String, String)> {
|
||||
let mut new_co_authors = Vec::new();
|
||||
let project = self.project.read(cx);
|
||||
@@ -1276,6 +1501,7 @@ impl GitPanel {
|
||||
git_panel.clear_pending();
|
||||
}
|
||||
git_panel.update_visible_entries(cx);
|
||||
git_panel.update_editor_placeholder(cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
@@ -1591,15 +1817,23 @@ impl GitPanel {
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(self.render_repository_selector(cx))
|
||||
.child(div().flex_grow())
|
||||
.child(div().flex_grow()) // spacer
|
||||
.child(
|
||||
Button::new("diff", "+/-")
|
||||
.tooltip(Tooltip::for_action_title("Open diff", &Diff))
|
||||
.on_click(|_, _, cx| {
|
||||
cx.defer(|cx| {
|
||||
cx.dispatch_action(&Diff);
|
||||
})
|
||||
}),
|
||||
div()
|
||||
.h_flex()
|
||||
.gap_1()
|
||||
.children(self.render_spinner(cx))
|
||||
.children(self.render_sync_button(cx))
|
||||
.children(self.render_pull_button(cx))
|
||||
.child(
|
||||
Button::new("diff", "+/-")
|
||||
.tooltip(Tooltip::for_action_title("Open diff", &Diff))
|
||||
.on_click(|_, _, cx| {
|
||||
cx.defer(|cx| {
|
||||
cx.dispatch_action(&Diff);
|
||||
})
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
@@ -1607,6 +1841,74 @@ impl GitPanel {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_spinner(&self, _cx: &mut Context<Self>) -> Option<impl IntoElement> {
|
||||
(!self.pending_remote_operations.borrow().is_empty()).then(|| {
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Info)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn render_sync_button(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
|
||||
let active_repository = self.project.read(cx).active_repository(cx);
|
||||
active_repository.as_ref().map(|_| {
|
||||
panel_filled_button("Fetch")
|
||||
.icon(IconName::ArrowCircle)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.tooltip(Tooltip::for_action_title("git fetch", &git::Fetch))
|
||||
.on_click(
|
||||
cx.listener(move |this, _, window, cx| this.fetch(&git::Fetch, window, cx)),
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn render_pull_button(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
|
||||
let active_repository = self.project.read(cx).active_repository(cx);
|
||||
active_repository
|
||||
.as_ref()
|
||||
.and_then(|repo| repo.read(cx).current_branch())
|
||||
.and_then(|branch| {
|
||||
branch.upstream.as_ref().map(|upstream| {
|
||||
let status = &upstream.tracking;
|
||||
|
||||
let disabled = status.is_gone();
|
||||
|
||||
panel_filled_button(match status {
|
||||
git::repository::UpstreamTracking::Tracked(status) if status.behind > 0 => {
|
||||
format!("Pull ({})", status.behind)
|
||||
}
|
||||
_ => "Pull".to_string(),
|
||||
})
|
||||
.icon(IconName::ArrowDown)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.disabled(status.is_gone())
|
||||
.tooltip(move |window, cx| {
|
||||
if disabled {
|
||||
Tooltip::simple("Upstream is gone", cx)
|
||||
} else {
|
||||
// TODO: Add <origin> and <branch> argument substitutions to this
|
||||
Tooltip::for_action("git pull", &git::Pull, window, cx)
|
||||
}
|
||||
})
|
||||
.on_click(
|
||||
cx.listener(move |this, _, window, cx| this.pull(&git::Pull, window, cx)),
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn render_repository_selector(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let active_repository = self.project.read(cx).active_repository(cx);
|
||||
let repository_display_name = active_repository
|
||||
@@ -1679,27 +1981,25 @@ impl GitPanel {
|
||||
&& self.pending_commit.is_none()
|
||||
&& !editor.read(cx).is_empty(cx)
|
||||
&& self.has_write_access(cx);
|
||||
|
||||
let panel_editor_style = panel_editor_style(true, window, cx);
|
||||
let enable_coauthors = self.render_co_authors(cx);
|
||||
|
||||
let editor_focus_handle = editor.read(cx).focus_handle(cx).clone();
|
||||
|
||||
let focus_handle_1 = self.focus_handle(cx).clone();
|
||||
let tooltip = if self.has_staged_changes() {
|
||||
"Commit staged changes"
|
||||
"git commit"
|
||||
} else {
|
||||
"Commit changes to tracked files"
|
||||
"git commit --all"
|
||||
};
|
||||
let title = if self.has_staged_changes() {
|
||||
"Commit"
|
||||
} else {
|
||||
"Commit All"
|
||||
"Commit Tracked"
|
||||
};
|
||||
let editor_focus_handle = self.commit_editor.focus_handle(cx);
|
||||
|
||||
let commit_button = panel_filled_button(title)
|
||||
.tooltip(move |window, cx| {
|
||||
let focus_handle = focus_handle_1.clone();
|
||||
Tooltip::for_action_in(tooltip, &Commit, &focus_handle, window, cx)
|
||||
Tooltip::for_action_in(tooltip, &Commit, &editor_focus_handle, window, cx)
|
||||
})
|
||||
.disabled(!can_commit)
|
||||
.on_click({
|
||||
@@ -1709,7 +2009,7 @@ impl GitPanel {
|
||||
let branch = self
|
||||
.active_repository
|
||||
.as_ref()
|
||||
.and_then(|repo| repo.read(cx).branch().map(|b| b.name.clone()))
|
||||
.and_then(|repo| repo.read(cx).current_branch().map(|b| b.name.clone()))
|
||||
.unwrap_or_else(|| "<no branch>".into());
|
||||
|
||||
let branch_selector = Button::new("branch-selector", branch)
|
||||
@@ -1743,8 +2043,8 @@ impl GitPanel {
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.cursor_text()
|
||||
.on_click(cx.listener(move |_, _: &ClickEvent, window, _cx| {
|
||||
window.focus(&editor_focus_handle);
|
||||
.on_click(cx.listener(move |this, _: &ClickEvent, window, cx| {
|
||||
window.focus(&this.commit_editor.focus_handle(cx));
|
||||
}))
|
||||
.when(!self.modal_open, |el| {
|
||||
el.child(EditorElement::new(&self.commit_editor, panel_editor_style))
|
||||
@@ -1772,24 +2072,9 @@ impl GitPanel {
|
||||
|
||||
fn render_previous_commit(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
|
||||
let active_repository = self.active_repository.as_ref()?;
|
||||
let branch = active_repository.read(cx).branch()?;
|
||||
let branch = active_repository.read(cx).current_branch()?;
|
||||
let commit = branch.most_recent_commit.as_ref()?.clone();
|
||||
|
||||
if branch.upstream.as_ref().is_some_and(|upstream| {
|
||||
if let Some(tracking) = &upstream.tracking {
|
||||
tracking.ahead == 0
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}) {
|
||||
return None;
|
||||
}
|
||||
let tooltip = if self.has_staged_changes() {
|
||||
"git reset HEAD^ --soft"
|
||||
} else {
|
||||
"git reset HEAD^"
|
||||
};
|
||||
|
||||
let this = cx.entity();
|
||||
Some(
|
||||
h_flex()
|
||||
@@ -1829,9 +2114,17 @@ impl GitPanel {
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.tooltip(Tooltip::for_action_title(tooltip, &git::Uncommit))
|
||||
.tooltip(Tooltip::for_action_title(
|
||||
if self.has_staged_changes() {
|
||||
"git reset HEAD^ --soft"
|
||||
} else {
|
||||
"git reset HEAD^"
|
||||
},
|
||||
&git::Uncommit,
|
||||
))
|
||||
.on_click(cx.listener(|this, _, window, cx| this.uncommit(window, cx))),
|
||||
),
|
||||
)
|
||||
.child(self.render_push_button(branch, cx)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2252,6 +2545,69 @@ impl GitPanel {
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_push_button(&self, branch: &Branch, cx: &Context<Self>) -> AnyElement {
|
||||
let mut disabled = false;
|
||||
|
||||
// TODO: Add <origin> and <branch> argument substitutions to this
|
||||
let button: SharedString;
|
||||
let tooltip: SharedString;
|
||||
let action: Option<Push>;
|
||||
if let Some(upstream) = &branch.upstream {
|
||||
match upstream.tracking {
|
||||
UpstreamTracking::Gone => {
|
||||
button = "Republish".into();
|
||||
tooltip = "git push --set-upstream".into();
|
||||
action = Some(git::Push {
|
||||
options: Some(PushOptions::SetUpstream),
|
||||
});
|
||||
}
|
||||
UpstreamTracking::Tracked(tracking) => {
|
||||
if tracking.behind > 0 {
|
||||
disabled = true;
|
||||
button = "Push".into();
|
||||
tooltip = "Upstream is ahead of local branch".into();
|
||||
action = None;
|
||||
} else if tracking.ahead > 0 {
|
||||
button = format!("Push ({})", tracking.ahead).into();
|
||||
tooltip = "git push".into();
|
||||
action = Some(git::Push { options: None });
|
||||
} else {
|
||||
disabled = true;
|
||||
button = "Push".into();
|
||||
tooltip = "Upstream matches local branch".into();
|
||||
action = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
button = "Publish".into();
|
||||
tooltip = "git push --set-upstream".into();
|
||||
action = Some(git::Push {
|
||||
options: Some(PushOptions::SetUpstream),
|
||||
});
|
||||
};
|
||||
|
||||
panel_filled_button(button)
|
||||
.icon(IconName::ArrowUp)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.disabled(disabled)
|
||||
.when_some(action, |this, action| {
|
||||
this.on_click(
|
||||
cx.listener(move |this, _, window, cx| this.push(&action, window, cx)),
|
||||
)
|
||||
})
|
||||
.tooltip(move |window, cx| {
|
||||
if let Some(action) = action.as_ref() {
|
||||
Tooltip::for_action(tooltip.clone(), action, window, cx)
|
||||
} else {
|
||||
Tooltip::simple(tooltip.clone(), cx)
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn has_write_access(&self, cx: &App) -> bool {
|
||||
!self.project.read(cx).is_read_only(cx)
|
||||
}
|
||||
@@ -2301,6 +2657,9 @@ impl Render for GitPanel {
|
||||
.on_action(cx.listener(Self::unstage_all))
|
||||
.on_action(cx.listener(Self::discard_tracked_changes))
|
||||
.on_action(cx.listener(Self::clean_all))
|
||||
.on_action(cx.listener(Self::fetch))
|
||||
.on_action(cx.listener(Self::pull))
|
||||
.on_action(cx.listener(Self::push))
|
||||
.when(has_write_access && has_co_authors, |git_panel| {
|
||||
git_panel.on_action(cx.listener(Self::toggle_fill_co_authors))
|
||||
})
|
||||
@@ -2317,17 +2676,21 @@ impl Render for GitPanel {
|
||||
.size_full()
|
||||
.overflow_hidden()
|
||||
.bg(ElevationIndex::Surface.bg(cx))
|
||||
.child(if has_entries {
|
||||
.child(
|
||||
v_flex()
|
||||
.size_full()
|
||||
.children(self.render_panel_header(window, cx))
|
||||
.child(self.render_entries(has_write_access, window, cx))
|
||||
.map(|this| {
|
||||
if has_entries {
|
||||
this.child(self.render_entries(has_write_access, window, cx))
|
||||
} else {
|
||||
this.child(self.render_empty_state(cx).into_any_element())
|
||||
}
|
||||
})
|
||||
.children(self.render_previous_commit(cx))
|
||||
.child(self.render_commit_editor(window, cx))
|
||||
.into_any_element()
|
||||
} else {
|
||||
self.render_empty_state(cx).into_any_element()
|
||||
})
|
||||
.into_any_element(),
|
||||
)
|
||||
.children(self.context_menu.as_ref().map(|(menu, position, _)| {
|
||||
deferred(
|
||||
anchored()
|
||||
|
||||
@@ -9,6 +9,7 @@ pub mod branch_picker;
|
||||
mod commit_modal;
|
||||
pub mod git_panel;
|
||||
mod git_panel_settings;
|
||||
pub mod picker_prompt;
|
||||
pub mod project_diff;
|
||||
pub mod repository_selector;
|
||||
|
||||
|
||||
235
crates/git_ui/src/picker_prompt.rs
Normal file
235
crates/git_ui/src/picker_prompt.rs
Normal file
@@ -0,0 +1,235 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use futures::channel::oneshot;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
|
||||
use core::cmp;
|
||||
use gpui::{
|
||||
rems, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
|
||||
Task, WeakEntity, Window,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use std::sync::Arc;
|
||||
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
|
||||
use util::ResultExt;
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
pub struct PickerPrompt {
|
||||
pub picker: Entity<Picker<PickerPromptDelegate>>,
|
||||
rem_width: f32,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
pub fn prompt(
|
||||
prompt: &str,
|
||||
options: Vec<SharedString>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<usize>> {
|
||||
if options.is_empty() {
|
||||
return Task::ready(Err(anyhow!("No options")));
|
||||
}
|
||||
let prompt = prompt.to_string().into();
|
||||
|
||||
window.spawn(cx, |mut cx| async move {
|
||||
// Modal branch picker has a longer trailoff than a popover one.
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let delegate = PickerPromptDelegate::new(prompt, options, tx, 70);
|
||||
|
||||
workspace.update_in(&mut cx, |workspace, window, cx| {
|
||||
workspace.toggle_modal(window, cx, |window, cx| {
|
||||
PickerPrompt::new(delegate, 34., window, cx)
|
||||
})
|
||||
})?;
|
||||
|
||||
rx.await?
|
||||
})
|
||||
}
|
||||
|
||||
impl PickerPrompt {
|
||||
fn new(
|
||||
delegate: PickerPromptDelegate,
|
||||
rem_width: f32,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
|
||||
let _subscription = cx.subscribe(&picker, |_, _, _, cx| cx.emit(DismissEvent));
|
||||
Self {
|
||||
picker,
|
||||
rem_width,
|
||||
_subscription,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl ModalView for PickerPrompt {}
|
||||
impl EventEmitter<DismissEvent> for PickerPrompt {}
|
||||
|
||||
impl Focusable for PickerPrompt {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for PickerPrompt {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.w(rems(self.rem_width))
|
||||
.child(self.picker.clone())
|
||||
.on_mouse_down_out(cx.listener(|this, _, window, cx| {
|
||||
this.picker.update(cx, |this, cx| {
|
||||
this.cancel(&Default::default(), window, cx);
|
||||
})
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PickerPromptDelegate {
|
||||
prompt: Arc<str>,
|
||||
matches: Vec<StringMatch>,
|
||||
all_options: Vec<SharedString>,
|
||||
selected_index: usize,
|
||||
max_match_length: usize,
|
||||
tx: Option<oneshot::Sender<Result<usize>>>,
|
||||
}
|
||||
|
||||
impl PickerPromptDelegate {
|
||||
pub fn new(
|
||||
prompt: Arc<str>,
|
||||
options: Vec<SharedString>,
|
||||
tx: oneshot::Sender<Result<usize>>,
|
||||
max_chars: usize,
|
||||
) -> Self {
|
||||
Self {
|
||||
prompt,
|
||||
all_options: options,
|
||||
matches: vec![],
|
||||
selected_index: 0,
|
||||
max_match_length: max_chars,
|
||||
tx: Some(tx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for PickerPromptDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
||||
self.prompt.clone()
|
||||
}
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(
|
||||
&mut self,
|
||||
ix: usize,
|
||||
_window: &mut Window,
|
||||
_: &mut Context<Picker<Self>>,
|
||||
) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
cx.spawn_in(window, move |picker, mut cx| async move {
|
||||
let candidates = picker.update(&mut cx, |picker, _| {
|
||||
picker
|
||||
.delegate
|
||||
.all_options
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, option)| StringMatchCandidate::new(ix, &option))
|
||||
.collect::<Vec<StringMatchCandidate>>()
|
||||
});
|
||||
let Some(candidates) = candidates.log_err() else {
|
||||
return;
|
||||
};
|
||||
let matches: Vec<StringMatch> = if query.is_empty() {
|
||||
candidates
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, candidate)| StringMatch {
|
||||
candidate_id: index,
|
||||
string: candidate.string,
|
||||
positions: Vec::new(),
|
||||
score: 0.0,
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
fuzzy::match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
true,
|
||||
10000,
|
||||
&Default::default(),
|
||||
cx.background_executor().clone(),
|
||||
)
|
||||
.await
|
||||
};
|
||||
picker
|
||||
.update(&mut cx, |picker, _| {
|
||||
let delegate = &mut picker.delegate;
|
||||
delegate.matches = matches;
|
||||
if delegate.matches.is_empty() {
|
||||
delegate.selected_index = 0;
|
||||
} else {
|
||||
delegate.selected_index =
|
||||
cmp::min(delegate.selected_index, delegate.matches.len() - 1);
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
let Some(option) = self.matches.get(self.selected_index()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.tx.take().map(|tx| tx.send(Ok(option.candidate_id)));
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let hit = &self.matches[ix];
|
||||
let shortened_option = util::truncate_and_trailoff(&hit.string, self.max_match_length);
|
||||
|
||||
Some(
|
||||
ListItem::new(SharedString::from(format!("picker-prompt-menu-{ix}")))
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.map(|el| {
|
||||
let highlights: Vec<_> = hit
|
||||
.positions
|
||||
.iter()
|
||||
.filter(|index| index < &&self.max_match_length)
|
||||
.copied()
|
||||
.collect();
|
||||
|
||||
el.child(HighlightedLabel::new(shortened_option, highlights))
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ use editor::{
|
||||
};
|
||||
use feature_flags::FeatureFlagViewExt;
|
||||
use futures::StreamExt;
|
||||
use git::{Commit, StageAll, StageAndNext, ToggleStaged, UnstageAll};
|
||||
use git::{status::FileStatus, Commit, StageAll, StageAndNext, ToggleStaged, UnstageAll};
|
||||
use gpui::{
|
||||
actions, Action, AnyElement, AnyView, App, AppContext as _, AsyncWindowContext, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, Render, Subscription, Task, WeakEntity,
|
||||
@@ -51,6 +51,7 @@ struct DiffBuffer {
|
||||
path_key: PathKey,
|
||||
buffer: Entity<Buffer>,
|
||||
diff: Entity<BufferDiff>,
|
||||
file_status: FileStatus,
|
||||
}
|
||||
|
||||
const CONFLICT_NAMESPACE: &'static str = "0";
|
||||
@@ -127,7 +128,6 @@ impl ProjectDiff {
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
diff_display_editor.set_distinguish_unstaged_diff_hunks();
|
||||
diff_display_editor.set_expand_all_diff_hunks(cx);
|
||||
diff_display_editor.register_addon(GitPanelAddon {
|
||||
workspace: workspace.downgrade(),
|
||||
@@ -352,6 +352,7 @@ impl ProjectDiff {
|
||||
path_key,
|
||||
buffer,
|
||||
diff: changes,
|
||||
file_status: entry.status,
|
||||
})
|
||||
}));
|
||||
}
|
||||
@@ -384,15 +385,22 @@ impl ProjectDiff {
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
self.multibuffer.update(cx, |multibuffer, cx| {
|
||||
let is_excerpt_newly_added = self.multibuffer.update(cx, |multibuffer, cx| {
|
||||
multibuffer.set_excerpts_for_path(
|
||||
path_key.clone(),
|
||||
buffer,
|
||||
diff_hunk_ranges,
|
||||
editor::DEFAULT_MULTIBUFFER_CONTEXT,
|
||||
cx,
|
||||
);
|
||||
)
|
||||
});
|
||||
|
||||
if is_excerpt_newly_added && diff_buffer.file_status.is_deleted() {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.fold_buffer(snapshot.text.remote_id(), cx)
|
||||
});
|
||||
}
|
||||
|
||||
if self.multibuffer.read(cx).is_empty()
|
||||
&& self
|
||||
.editor
|
||||
|
||||
@@ -299,7 +299,7 @@ pub struct CountTokensResponse {
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, strum::EnumIter)]
|
||||
#[derive(Clone, Default, Debug, Deserialize, Serialize, PartialEq, Eq, strum::EnumIter)]
|
||||
pub enum Model {
|
||||
#[serde(rename = "gemini-1.5-pro")]
|
||||
Gemini15Pro,
|
||||
@@ -308,6 +308,7 @@ pub enum Model {
|
||||
#[serde(rename = "gemini-2.0-pro-exp")]
|
||||
Gemini20Pro,
|
||||
#[serde(rename = "gemini-2.0-flash")]
|
||||
#[default]
|
||||
Gemini20Flash,
|
||||
#[serde(rename = "gemini-2.0-flash-thinking-exp")]
|
||||
Gemini20FlashThinking,
|
||||
|
||||
@@ -486,7 +486,31 @@ impl Hsla {
|
||||
self.a *= 1.0 - factor.clamp(0., 1.);
|
||||
}
|
||||
|
||||
/// Returns a new HSLA color with the same hue, saturation, and lightness, but with a modified alpha value.
|
||||
/// Multiplies the alpha value of the color by a given factor
|
||||
/// and returns a new HSLA color.
|
||||
///
|
||||
/// Useful for transforming colors with dynamic opacity,
|
||||
/// like a color from an external source.
|
||||
///
|
||||
/// Example:
|
||||
/// ```
|
||||
/// let color = gpui::red();
|
||||
/// let faded_color = color.opacity(0.5);
|
||||
/// assert_eq!(faded_color.a, 0.5);
|
||||
/// ```
|
||||
///
|
||||
/// This will return a red color with half the opacity.
|
||||
///
|
||||
/// Example:
|
||||
/// ```
|
||||
/// let color = hlsa(0.7, 1.0, 0.5, 0.7); // A saturated blue
|
||||
/// let faded_color = color.opacity(0.16);
|
||||
/// assert_eq!(faded_color.a, 0.112);
|
||||
/// ```
|
||||
///
|
||||
/// This will return a blue color with around ~10% opacity,
|
||||
/// suitable for an element's hover or selected state.
|
||||
///
|
||||
pub fn opacity(&self, factor: f32) -> Self {
|
||||
Hsla {
|
||||
h: self.h,
|
||||
@@ -495,6 +519,35 @@ impl Hsla {
|
||||
a: self.a * factor.clamp(0., 1.),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a new HSLA color with the same hue, saturation,
|
||||
/// and lightness, but with a new alpha value.
|
||||
///
|
||||
/// Example:
|
||||
/// ```
|
||||
/// let color = gpui::red();
|
||||
/// let red_color = color.alpha(0.25);
|
||||
/// assert_eq!(red_color.a, 0.25);
|
||||
/// ```
|
||||
///
|
||||
/// This will return a red color with half the opacity.
|
||||
///
|
||||
/// Example:
|
||||
/// ```
|
||||
/// let color = hsla(0.7, 1.0, 0.5, 0.7); // A saturated blue
|
||||
/// let faded_color = color.alpha(0.25);
|
||||
/// assert_eq!(faded_color.a, 0.25);
|
||||
/// ```
|
||||
///
|
||||
/// This will return a blue color with 25% opacity.
|
||||
pub fn alpha(&self, a: f32) -> Self {
|
||||
Hsla {
|
||||
h: self.h,
|
||||
s: self.s,
|
||||
l: self.l,
|
||||
a: a.clamp(0., 1.),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Rgba> for Hsla {
|
||||
|
||||
@@ -35,6 +35,11 @@ pub struct BackgroundExecutor {
|
||||
|
||||
/// A pointer to the executor that is currently running,
|
||||
/// for spawning tasks on the main thread.
|
||||
///
|
||||
/// This is intentionally `!Send` via the `not_send` marker field. This is because
|
||||
/// `ForegroundExecutor::spawn` does not require `Send` but checks at runtime that the future is
|
||||
/// only polled from the same thread it was spawned from. These checks would fail when spawning
|
||||
/// foreground tasks from from background threads.
|
||||
#[derive(Clone)]
|
||||
pub struct ForegroundExecutor {
|
||||
#[doc(hidden)]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
self as gpui, px, relative, rems, AbsoluteLength, AlignItems, CursorStyle, DefiniteLength,
|
||||
Fill, FlexDirection, FlexWrap, Font, FontStyle, FontWeight, Hsla, JustifyContent, Length,
|
||||
SharedString, StrikethroughStyle, StyleRefinement, TextOverflow, WhiteSpace,
|
||||
SharedString, StrikethroughStyle, StyleRefinement, TextOverflow, UnderlineStyle, WhiteSpace,
|
||||
};
|
||||
use crate::{TextAlign, TextStyleRefinement};
|
||||
pub use gpui_macros::{
|
||||
@@ -486,6 +486,17 @@ pub trait Styled: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the text decoration to underline.
|
||||
/// [Docs](https://tailwindcss.com/docs/text-decoration-line#underling-text)
|
||||
fn underline(mut self) -> Self {
|
||||
let style = self.text_style().get_or_insert_with(Default::default);
|
||||
style.underline = Some(UnderlineStyle {
|
||||
thickness: px(1.),
|
||||
..Default::default()
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the decoration of the text to have a line through it.
|
||||
/// [Docs](https://tailwindcss.com/docs/text-decoration#setting-the-text-decoration)
|
||||
fn line_through(mut self) -> Self {
|
||||
|
||||
@@ -593,8 +593,7 @@ impl Frame {
|
||||
}
|
||||
}
|
||||
|
||||
// Holds the state for a specific window.
|
||||
#[doc(hidden)]
|
||||
/// Holds the state for a specific window.
|
||||
pub struct Window {
|
||||
pub(crate) handle: AnyWindowHandle,
|
||||
pub(crate) invalidator: WindowInvalidator,
|
||||
@@ -1007,6 +1006,7 @@ impl Window {
|
||||
subscription
|
||||
}
|
||||
|
||||
/// Replaces the root entity of the window with a new one.
|
||||
pub fn replace_root<E>(
|
||||
&mut self,
|
||||
cx: &mut App,
|
||||
@@ -1021,6 +1021,7 @@ impl Window {
|
||||
view
|
||||
}
|
||||
|
||||
/// Returns the root entity of the window, if it has one.
|
||||
pub fn root<E>(&self) -> Option<Option<Entity<E>>>
|
||||
where
|
||||
E: 'static + Render,
|
||||
|
||||
@@ -234,6 +234,8 @@ pub struct EditPredictionSettings {
|
||||
pub disabled_globs: Vec<GlobMatcher>,
|
||||
/// Configures how edit predictions are displayed in the buffer.
|
||||
pub mode: EditPredictionsMode,
|
||||
/// Settings specific to GitHub Copilot.
|
||||
pub copilot: CopilotSettings,
|
||||
}
|
||||
|
||||
/// The mode in which edit predictions should be displayed.
|
||||
@@ -248,6 +250,14 @@ pub enum EditPredictionsMode {
|
||||
EagerPreview,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct CopilotSettings {
|
||||
/// HTTP/HTTPS proxy to use for Copilot.
|
||||
pub proxy: Option<String>,
|
||||
/// Disable certificate verification for proxy (not recommended).
|
||||
pub proxy_no_verify: Option<bool>,
|
||||
}
|
||||
|
||||
/// The settings for all languages.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct AllLanguageSettingsContent {
|
||||
@@ -465,6 +475,23 @@ pub struct EditPredictionSettingsContent {
|
||||
/// Provider support required.
|
||||
#[serde(default)]
|
||||
pub mode: EditPredictionsMode,
|
||||
/// Settings specific to GitHub Copilot.
|
||||
#[serde(default)]
|
||||
pub copilot: CopilotSettingsContent,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct CopilotSettingsContent {
|
||||
/// HTTP/HTTPS proxy to use for Copilot.
|
||||
///
|
||||
/// Default: none
|
||||
#[serde(default)]
|
||||
pub proxy: Option<String>,
|
||||
/// Disable certificate verification for the proxy (not recommended).
|
||||
///
|
||||
/// Default: false
|
||||
#[serde(default)]
|
||||
pub proxy_no_verify: Option<bool>,
|
||||
}
|
||||
|
||||
/// The settings for enabling/disabling features.
|
||||
@@ -1064,6 +1091,16 @@ impl settings::Settings for AllLanguageSettings {
|
||||
.map(|globs| globs.iter().collect())
|
||||
.ok_or_else(Self::missing_default)?;
|
||||
|
||||
let mut copilot_settings = default_value
|
||||
.edit_predictions
|
||||
.as_ref()
|
||||
.map(|settings| settings.copilot.clone())
|
||||
.map(|copilot| CopilotSettings {
|
||||
proxy: copilot.proxy,
|
||||
proxy_no_verify: copilot.proxy_no_verify,
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut file_types: HashMap<Arc<str>, GlobSet> = HashMap::default();
|
||||
|
||||
for (language, suffixes) in &default_value.file_types {
|
||||
@@ -1096,6 +1133,22 @@ impl settings::Settings for AllLanguageSettings {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(proxy) = user_settings
|
||||
.edit_predictions
|
||||
.as_ref()
|
||||
.and_then(|settings| settings.copilot.proxy.clone())
|
||||
{
|
||||
copilot_settings.proxy = Some(proxy);
|
||||
}
|
||||
|
||||
if let Some(proxy_no_verify) = user_settings
|
||||
.edit_predictions
|
||||
.as_ref()
|
||||
.and_then(|settings| settings.copilot.proxy_no_verify)
|
||||
{
|
||||
copilot_settings.proxy_no_verify = Some(proxy_no_verify);
|
||||
}
|
||||
|
||||
// A user's global settings override the default global settings and
|
||||
// all default language-specific settings.
|
||||
merge_settings(&mut defaults, &user_settings.defaults);
|
||||
@@ -1147,6 +1200,7 @@ impl settings::Settings for AllLanguageSettings {
|
||||
.filter_map(|g| Some(globset::Glob::new(g).ok()?.compile_matcher()))
|
||||
.collect(),
|
||||
mode: edit_predictions_mode,
|
||||
copilot: copilot_settings,
|
||||
},
|
||||
defaults,
|
||||
languages,
|
||||
|
||||
@@ -20,16 +20,12 @@ anthropic = { workspace = true, features = ["schemars"] }
|
||||
anyhow.workspace = true
|
||||
base64.workspace = true
|
||||
collections.workspace = true
|
||||
deepseek = { workspace = true, features = ["schemars"] }
|
||||
futures.workspace = true
|
||||
google_ai = { workspace = true, features = ["schemars"] }
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
image.workspace = true
|
||||
lmstudio = { workspace = true, features = ["schemars"] }
|
||||
log.workspace = true
|
||||
mistral = { workspace = true, features = ["schemars"] }
|
||||
ollama = { workspace = true, features = ["schemars"] }
|
||||
open_ai = { workspace = true, features = ["schemars"] }
|
||||
parking_lot.workspace = true
|
||||
proto.workspace = true
|
||||
|
||||
@@ -46,6 +46,10 @@ impl LanguageModelProvider for FakeLanguageModelProvider {
|
||||
provider_name()
|
||||
}
|
||||
|
||||
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
|
||||
Some(Arc::new(FakeLanguageModel::default()))
|
||||
}
|
||||
|
||||
fn provided_models(&self, _: &App) -> Vec<Arc<dyn LanguageModel>> {
|
||||
vec![Arc::new(FakeLanguageModel::default())]
|
||||
}
|
||||
|
||||
@@ -247,6 +247,7 @@ pub trait LanguageModelProvider: 'static {
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::ZedAssistant
|
||||
}
|
||||
fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>>;
|
||||
fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>>;
|
||||
fn load_model(&self, _model: Arc<dyn LanguageModel>, _cx: &App) {}
|
||||
fn is_authenticated(&self, cx: &App) -> bool;
|
||||
|
||||
@@ -69,6 +69,7 @@ impl CloudModel {
|
||||
| anthropic::Model::Claude3Sonnet
|
||||
| anthropic::Model::Claude3Haiku
|
||||
| anthropic::Model::Claude3_5Haiku
|
||||
| anthropic::Model::Claude3_7Sonnet
|
||||
| anthropic::Model::Custom { .. } => {
|
||||
LanguageModelAvailability::RequiresPlan(Plan::ZedPro)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
pub mod cloud_model;
|
||||
|
||||
pub use anthropic::Model as AnthropicModel;
|
||||
pub use cloud_model::*;
|
||||
pub use lmstudio::Model as LmStudioModel;
|
||||
pub use ollama::Model as OllamaModel;
|
||||
pub use open_ai::Model as OpenAiModel;
|
||||
|
||||
@@ -241,298 +241,6 @@ pub struct LanguageModelRequest {
|
||||
pub temperature: Option<f32>,
|
||||
}
|
||||
|
||||
impl LanguageModelRequest {
|
||||
pub fn into_open_ai(self, model: String, max_output_tokens: Option<u32>) -> open_ai::Request {
|
||||
let stream = !model.starts_with("o1-");
|
||||
open_ai::Request {
|
||||
model,
|
||||
messages: self
|
||||
.messages
|
||||
.into_iter()
|
||||
.map(|msg| match msg.role {
|
||||
Role::User => open_ai::RequestMessage::User {
|
||||
content: msg.string_contents(),
|
||||
},
|
||||
Role::Assistant => open_ai::RequestMessage::Assistant {
|
||||
content: Some(msg.string_contents()),
|
||||
tool_calls: Vec::new(),
|
||||
},
|
||||
Role::System => open_ai::RequestMessage::System {
|
||||
content: msg.string_contents(),
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
stream,
|
||||
stop: self.stop,
|
||||
temperature: self.temperature.unwrap_or(1.0),
|
||||
max_tokens: max_output_tokens,
|
||||
tools: Vec::new(),
|
||||
tool_choice: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_mistral(self, model: String, max_output_tokens: Option<u32>) -> mistral::Request {
|
||||
let len = self.messages.len();
|
||||
let merged_messages =
|
||||
self.messages
|
||||
.into_iter()
|
||||
.fold(Vec::with_capacity(len), |mut acc, msg| {
|
||||
let role = msg.role;
|
||||
let content = msg.string_contents();
|
||||
|
||||
acc.push(match role {
|
||||
Role::User => mistral::RequestMessage::User { content },
|
||||
Role::Assistant => mistral::RequestMessage::Assistant {
|
||||
content: Some(content),
|
||||
tool_calls: Vec::new(),
|
||||
},
|
||||
Role::System => mistral::RequestMessage::System { content },
|
||||
});
|
||||
acc
|
||||
});
|
||||
|
||||
mistral::Request {
|
||||
model,
|
||||
messages: merged_messages,
|
||||
stream: true,
|
||||
max_tokens: max_output_tokens,
|
||||
temperature: self.temperature,
|
||||
response_format: None,
|
||||
tools: self
|
||||
.tools
|
||||
.into_iter()
|
||||
.map(|tool| mistral::ToolDefinition::Function {
|
||||
function: mistral::FunctionDefinition {
|
||||
name: tool.name,
|
||||
description: Some(tool.description),
|
||||
parameters: Some(tool.input_schema),
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_google(self, model: String) -> google_ai::GenerateContentRequest {
|
||||
google_ai::GenerateContentRequest {
|
||||
model,
|
||||
contents: self
|
||||
.messages
|
||||
.into_iter()
|
||||
.map(|msg| google_ai::Content {
|
||||
parts: vec![google_ai::Part::TextPart(google_ai::TextPart {
|
||||
text: msg.string_contents(),
|
||||
})],
|
||||
role: match msg.role {
|
||||
Role::User => google_ai::Role::User,
|
||||
Role::Assistant => google_ai::Role::Model,
|
||||
Role::System => google_ai::Role::User, // Google AI doesn't have a system role
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
generation_config: Some(google_ai::GenerationConfig {
|
||||
candidate_count: Some(1),
|
||||
stop_sequences: Some(self.stop),
|
||||
max_output_tokens: None,
|
||||
temperature: self.temperature.map(|t| t as f64).or(Some(1.0)),
|
||||
top_p: None,
|
||||
top_k: None,
|
||||
}),
|
||||
safety_settings: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_anthropic(
|
||||
self,
|
||||
model: String,
|
||||
default_temperature: f32,
|
||||
max_output_tokens: u32,
|
||||
) -> anthropic::Request {
|
||||
let mut new_messages: Vec<anthropic::Message> = Vec::new();
|
||||
let mut system_message = String::new();
|
||||
|
||||
for message in self.messages {
|
||||
if message.contents_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
match message.role {
|
||||
Role::User | Role::Assistant => {
|
||||
let cache_control = if message.cache {
|
||||
Some(anthropic::CacheControl {
|
||||
cache_type: anthropic::CacheControlType::Ephemeral,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let anthropic_message_content: Vec<anthropic::RequestContent> = message
|
||||
.content
|
||||
.into_iter()
|
||||
.filter_map(|content| match content {
|
||||
MessageContent::Text(text) => {
|
||||
if !text.is_empty() {
|
||||
Some(anthropic::RequestContent::Text {
|
||||
text,
|
||||
cache_control,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
MessageContent::Image(image) => {
|
||||
Some(anthropic::RequestContent::Image {
|
||||
source: anthropic::ImageSource {
|
||||
source_type: "base64".to_string(),
|
||||
media_type: "image/png".to_string(),
|
||||
data: image.source.to_string(),
|
||||
},
|
||||
cache_control,
|
||||
})
|
||||
}
|
||||
MessageContent::ToolUse(tool_use) => {
|
||||
Some(anthropic::RequestContent::ToolUse {
|
||||
id: tool_use.id.to_string(),
|
||||
name: tool_use.name,
|
||||
input: tool_use.input,
|
||||
cache_control,
|
||||
})
|
||||
}
|
||||
MessageContent::ToolResult(tool_result) => {
|
||||
Some(anthropic::RequestContent::ToolResult {
|
||||
tool_use_id: tool_result.tool_use_id,
|
||||
is_error: tool_result.is_error,
|
||||
content: tool_result.content,
|
||||
cache_control,
|
||||
})
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let anthropic_role = match message.role {
|
||||
Role::User => anthropic::Role::User,
|
||||
Role::Assistant => anthropic::Role::Assistant,
|
||||
Role::System => unreachable!("System role should never occur here"),
|
||||
};
|
||||
if let Some(last_message) = new_messages.last_mut() {
|
||||
if last_message.role == anthropic_role {
|
||||
last_message.content.extend(anthropic_message_content);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
new_messages.push(anthropic::Message {
|
||||
role: anthropic_role,
|
||||
content: anthropic_message_content,
|
||||
});
|
||||
}
|
||||
Role::System => {
|
||||
if !system_message.is_empty() {
|
||||
system_message.push_str("\n\n");
|
||||
}
|
||||
system_message.push_str(&message.string_contents());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
anthropic::Request {
|
||||
model,
|
||||
messages: new_messages,
|
||||
max_tokens: max_output_tokens,
|
||||
system: Some(system_message),
|
||||
tools: self
|
||||
.tools
|
||||
.into_iter()
|
||||
.map(|tool| anthropic::Tool {
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
input_schema: tool.input_schema,
|
||||
})
|
||||
.collect(),
|
||||
tool_choice: None,
|
||||
metadata: None,
|
||||
stop_sequences: Vec::new(),
|
||||
temperature: self.temperature.or(Some(default_temperature)),
|
||||
top_k: None,
|
||||
top_p: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_deepseek(self, model: String, max_output_tokens: Option<u32>) -> deepseek::Request {
|
||||
let is_reasoner = model == "deepseek-reasoner";
|
||||
|
||||
let len = self.messages.len();
|
||||
let merged_messages =
|
||||
self.messages
|
||||
.into_iter()
|
||||
.fold(Vec::with_capacity(len), |mut acc, msg| {
|
||||
let role = msg.role;
|
||||
let content = msg.string_contents();
|
||||
|
||||
if is_reasoner {
|
||||
if let Some(last_msg) = acc.last_mut() {
|
||||
match (last_msg, role) {
|
||||
(deepseek::RequestMessage::User { content: last }, Role::User) => {
|
||||
last.push(' ');
|
||||
last.push_str(&content);
|
||||
return acc;
|
||||
}
|
||||
|
||||
(
|
||||
deepseek::RequestMessage::Assistant {
|
||||
content: last_content,
|
||||
..
|
||||
},
|
||||
Role::Assistant,
|
||||
) => {
|
||||
*last_content = last_content
|
||||
.take()
|
||||
.map(|c| {
|
||||
let mut s =
|
||||
String::with_capacity(c.len() + content.len() + 1);
|
||||
s.push_str(&c);
|
||||
s.push(' ');
|
||||
s.push_str(&content);
|
||||
s
|
||||
})
|
||||
.or(Some(content));
|
||||
|
||||
return acc;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
acc.push(match role {
|
||||
Role::User => deepseek::RequestMessage::User { content },
|
||||
Role::Assistant => deepseek::RequestMessage::Assistant {
|
||||
content: Some(content),
|
||||
tool_calls: Vec::new(),
|
||||
},
|
||||
Role::System => deepseek::RequestMessage::System { content },
|
||||
});
|
||||
acc
|
||||
});
|
||||
|
||||
deepseek::Request {
|
||||
model,
|
||||
messages: merged_messages,
|
||||
stream: true,
|
||||
max_tokens: max_output_tokens,
|
||||
temperature: if is_reasoner { None } else { self.temperature },
|
||||
response_format: None,
|
||||
tools: self
|
||||
.tools
|
||||
.into_iter()
|
||||
.map(|tool| deepseek::ToolDefinition::Function {
|
||||
function: deepseek::FunctionDefinition {
|
||||
name: tool.name,
|
||||
description: Some(tool.description),
|
||||
parameters: Some(tool.input_schema),
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
pub struct LanguageModelResponseMessage {
|
||||
pub role: Option<Role>,
|
||||
|
||||
@@ -45,43 +45,3 @@ impl Display for Role {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Role> for ollama::Role {
|
||||
fn from(val: Role) -> Self {
|
||||
match val {
|
||||
Role::User => ollama::Role::User,
|
||||
Role::Assistant => ollama::Role::Assistant,
|
||||
Role::System => ollama::Role::System,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Role> for open_ai::Role {
|
||||
fn from(val: Role) -> Self {
|
||||
match val {
|
||||
Role::User => open_ai::Role::User,
|
||||
Role::Assistant => open_ai::Role::Assistant,
|
||||
Role::System => open_ai::Role::System,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Role> for deepseek::Role {
|
||||
fn from(val: Role) -> Self {
|
||||
match val {
|
||||
Role::User => deepseek::Role::User,
|
||||
Role::Assistant => deepseek::Role::Assistant,
|
||||
Role::System => deepseek::Role::System,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Role> for lmstudio::Role {
|
||||
fn from(val: Role) -> Self {
|
||||
match val {
|
||||
Role::User => lmstudio::Role::User,
|
||||
Role::Assistant => lmstudio::Role::Assistant,
|
||||
Role::System => lmstudio::Role::System,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ use http_client::HttpClient;
|
||||
use language_model::{
|
||||
AuthenticateError, LanguageModel, LanguageModelCacheConfiguration, LanguageModelId,
|
||||
LanguageModelName, LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
|
||||
LanguageModelProviderState, LanguageModelRequest, RateLimiter, Role,
|
||||
LanguageModelProviderState, LanguageModelRequest, MessageContent, RateLimiter, Role,
|
||||
};
|
||||
use language_model::{LanguageModelCompletionEvent, LanguageModelToolUse, StopReason};
|
||||
use schemars::JsonSchema;
|
||||
@@ -183,6 +183,17 @@ impl LanguageModelProvider for AnthropicLanguageModelProvider {
|
||||
IconName::AiAnthropic
|
||||
}
|
||||
|
||||
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
|
||||
let model = anthropic::Model::default();
|
||||
Some(Arc::new(AnthropicModel {
|
||||
id: LanguageModelId::from(model.id().to_string()),
|
||||
model,
|
||||
state: self.state.clone(),
|
||||
http_client: self.http_client.clone(),
|
||||
request_limiter: RateLimiter::new(4),
|
||||
}))
|
||||
}
|
||||
|
||||
fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
|
||||
let mut models = BTreeMap::default();
|
||||
|
||||
@@ -385,7 +396,8 @@ impl LanguageModel for AnthropicModel {
|
||||
request: LanguageModelRequest,
|
||||
cx: &AsyncApp,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<LanguageModelCompletionEvent>>>> {
|
||||
let request = request.into_anthropic(
|
||||
let request = into_anthropic(
|
||||
request,
|
||||
self.model.id().into(),
|
||||
self.model.default_temperature(),
|
||||
self.model.max_output_tokens(),
|
||||
@@ -416,7 +428,8 @@ impl LanguageModel for AnthropicModel {
|
||||
input_schema: serde_json::Value,
|
||||
cx: &AsyncApp,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
|
||||
let mut request = request.into_anthropic(
|
||||
let mut request = into_anthropic(
|
||||
request,
|
||||
self.model.tool_model_id().into(),
|
||||
self.model.default_temperature(),
|
||||
self.model.max_output_tokens(),
|
||||
@@ -445,6 +458,117 @@ impl LanguageModel for AnthropicModel {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_anthropic(
|
||||
request: LanguageModelRequest,
|
||||
model: String,
|
||||
default_temperature: f32,
|
||||
max_output_tokens: u32,
|
||||
) -> anthropic::Request {
|
||||
let mut new_messages: Vec<anthropic::Message> = Vec::new();
|
||||
let mut system_message = String::new();
|
||||
|
||||
for message in request.messages {
|
||||
if message.contents_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
match message.role {
|
||||
Role::User | Role::Assistant => {
|
||||
let cache_control = if message.cache {
|
||||
Some(anthropic::CacheControl {
|
||||
cache_type: anthropic::CacheControlType::Ephemeral,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let anthropic_message_content: Vec<anthropic::RequestContent> = message
|
||||
.content
|
||||
.into_iter()
|
||||
.filter_map(|content| match content {
|
||||
MessageContent::Text(text) => {
|
||||
if !text.is_empty() {
|
||||
Some(anthropic::RequestContent::Text {
|
||||
text,
|
||||
cache_control,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
MessageContent::Image(image) => Some(anthropic::RequestContent::Image {
|
||||
source: anthropic::ImageSource {
|
||||
source_type: "base64".to_string(),
|
||||
media_type: "image/png".to_string(),
|
||||
data: image.source.to_string(),
|
||||
},
|
||||
cache_control,
|
||||
}),
|
||||
MessageContent::ToolUse(tool_use) => {
|
||||
Some(anthropic::RequestContent::ToolUse {
|
||||
id: tool_use.id.to_string(),
|
||||
name: tool_use.name,
|
||||
input: tool_use.input,
|
||||
cache_control,
|
||||
})
|
||||
}
|
||||
MessageContent::ToolResult(tool_result) => {
|
||||
Some(anthropic::RequestContent::ToolResult {
|
||||
tool_use_id: tool_result.tool_use_id,
|
||||
is_error: tool_result.is_error,
|
||||
content: tool_result.content,
|
||||
cache_control,
|
||||
})
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let anthropic_role = match message.role {
|
||||
Role::User => anthropic::Role::User,
|
||||
Role::Assistant => anthropic::Role::Assistant,
|
||||
Role::System => unreachable!("System role should never occur here"),
|
||||
};
|
||||
if let Some(last_message) = new_messages.last_mut() {
|
||||
if last_message.role == anthropic_role {
|
||||
last_message.content.extend(anthropic_message_content);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
new_messages.push(anthropic::Message {
|
||||
role: anthropic_role,
|
||||
content: anthropic_message_content,
|
||||
});
|
||||
}
|
||||
Role::System => {
|
||||
if !system_message.is_empty() {
|
||||
system_message.push_str("\n\n");
|
||||
}
|
||||
system_message.push_str(&message.string_contents());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
anthropic::Request {
|
||||
model,
|
||||
messages: new_messages,
|
||||
max_tokens: max_output_tokens,
|
||||
system: Some(system_message),
|
||||
tools: request
|
||||
.tools
|
||||
.into_iter()
|
||||
.map(|tool| anthropic::Tool {
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
input_schema: tool.input_schema,
|
||||
})
|
||||
.collect(),
|
||||
tool_choice: None,
|
||||
metadata: None,
|
||||
stop_sequences: Vec::new(),
|
||||
temperature: request.temperature.or(Some(default_temperature)),
|
||||
top_k: None,
|
||||
top_p: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn map_to_language_model_completion_events(
|
||||
events: Pin<Box<dyn Send + Stream<Item = Result<Event, AnthropicError>>>>,
|
||||
) -> impl Stream<Item = Result<LanguageModelCompletionEvent>> {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use super::open_ai::count_open_ai_tokens;
|
||||
use anthropic::AnthropicError;
|
||||
use anyhow::{anyhow, Result};
|
||||
use client::{
|
||||
@@ -43,11 +42,13 @@ use strum::IntoEnumIterator;
|
||||
use thiserror::Error;
|
||||
use ui::{prelude::*, TintColor};
|
||||
|
||||
use crate::provider::anthropic::map_to_language_model_completion_events;
|
||||
use crate::provider::anthropic::{
|
||||
count_anthropic_tokens, into_anthropic, map_to_language_model_completion_events,
|
||||
};
|
||||
use crate::provider::google::into_google;
|
||||
use crate::provider::open_ai::{count_open_ai_tokens, into_open_ai};
|
||||
use crate::AllLanguageModelSettings;
|
||||
|
||||
use super::anthropic::count_anthropic_tokens;
|
||||
|
||||
pub const PROVIDER_NAME: &str = "Zed";
|
||||
|
||||
const ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: Option<&str> =
|
||||
@@ -272,6 +273,18 @@ impl LanguageModelProvider for CloudLanguageModelProvider {
|
||||
IconName::AiZed
|
||||
}
|
||||
|
||||
fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {
|
||||
let llm_api_token = self.state.read(cx).llm_api_token.clone();
|
||||
let model = CloudModel::Anthropic(anthropic::Model::default());
|
||||
Some(Arc::new(CloudLanguageModel {
|
||||
id: LanguageModelId::from(model.id().to_string()),
|
||||
model,
|
||||
llm_api_token: llm_api_token.clone(),
|
||||
client: self.client.clone(),
|
||||
request_limiter: RateLimiter::new(4),
|
||||
}))
|
||||
}
|
||||
|
||||
fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
|
||||
let mut models = BTreeMap::default();
|
||||
|
||||
@@ -600,7 +613,7 @@ impl LanguageModel for CloudLanguageModel {
|
||||
CloudModel::OpenAi(model) => count_open_ai_tokens(request, model, cx),
|
||||
CloudModel::Google(model) => {
|
||||
let client = self.client.clone();
|
||||
let request = request.into_google(model.id().into());
|
||||
let request = into_google(request, model.id().into());
|
||||
let request = google_ai::CountTokensRequest {
|
||||
contents: request.contents,
|
||||
};
|
||||
@@ -626,7 +639,8 @@ impl LanguageModel for CloudLanguageModel {
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<LanguageModelCompletionEvent>>>> {
|
||||
match &self.model {
|
||||
CloudModel::Anthropic(model) => {
|
||||
let request = request.into_anthropic(
|
||||
let request = into_anthropic(
|
||||
request,
|
||||
model.id().into(),
|
||||
model.default_temperature(),
|
||||
model.max_output_tokens(),
|
||||
@@ -654,7 +668,7 @@ impl LanguageModel for CloudLanguageModel {
|
||||
}
|
||||
CloudModel::OpenAi(model) => {
|
||||
let client = self.client.clone();
|
||||
let request = request.into_open_ai(model.id().into(), model.max_output_tokens());
|
||||
let request = into_open_ai(request, model.id().into(), model.max_output_tokens());
|
||||
let llm_api_token = self.llm_api_token.clone();
|
||||
let future = self.request_limiter.stream(async move {
|
||||
let response = Self::perform_llm_completion(
|
||||
@@ -681,7 +695,7 @@ impl LanguageModel for CloudLanguageModel {
|
||||
}
|
||||
CloudModel::Google(model) => {
|
||||
let client = self.client.clone();
|
||||
let request = request.into_google(model.id().into());
|
||||
let request = into_google(request, model.id().into());
|
||||
let llm_api_token = self.llm_api_token.clone();
|
||||
let future = self.request_limiter.stream(async move {
|
||||
let response = Self::perform_llm_completion(
|
||||
@@ -724,7 +738,8 @@ impl LanguageModel for CloudLanguageModel {
|
||||
|
||||
match &self.model {
|
||||
CloudModel::Anthropic(model) => {
|
||||
let mut request = request.into_anthropic(
|
||||
let mut request = into_anthropic(
|
||||
request,
|
||||
model.tool_model_id().into(),
|
||||
model.default_temperature(),
|
||||
model.max_output_tokens(),
|
||||
@@ -764,7 +779,7 @@ impl LanguageModel for CloudLanguageModel {
|
||||
}
|
||||
CloudModel::OpenAi(model) => {
|
||||
let mut request =
|
||||
request.into_open_ai(model.id().into(), model.max_output_tokens());
|
||||
into_open_ai(request, model.id().into(), model.max_output_tokens());
|
||||
request.tool_choice = Some(open_ai::ToolChoice::Other(
|
||||
open_ai::ToolDefinition::Function {
|
||||
function: open_ai::FunctionDefinition {
|
||||
|
||||
@@ -89,6 +89,14 @@ impl LanguageModelProvider for CopilotChatLanguageModelProvider {
|
||||
IconName::Copilot
|
||||
}
|
||||
|
||||
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
|
||||
let model = CopilotChatModel::default();
|
||||
Some(Arc::new(CopilotChatLanguageModel {
|
||||
model,
|
||||
request_limiter: RateLimiter::new(4),
|
||||
}) as Arc<dyn LanguageModel>)
|
||||
}
|
||||
|
||||
fn provided_models(&self, _cx: &App) -> Vec<Arc<dyn LanguageModel>> {
|
||||
CopilotChatModel::iter()
|
||||
.map(|model| {
|
||||
|
||||
@@ -163,6 +163,17 @@ impl LanguageModelProvider for DeepSeekLanguageModelProvider {
|
||||
IconName::AiDeepSeek
|
||||
}
|
||||
|
||||
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
|
||||
let model = deepseek::Model::Chat;
|
||||
Some(Arc::new(DeepSeekLanguageModel {
|
||||
id: LanguageModelId::from(model.id().to_string()),
|
||||
model,
|
||||
state: self.state.clone(),
|
||||
http_client: self.http_client.clone(),
|
||||
request_limiter: RateLimiter::new(4),
|
||||
}))
|
||||
}
|
||||
|
||||
fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
|
||||
let mut models = BTreeMap::default();
|
||||
|
||||
@@ -311,7 +322,11 @@ impl LanguageModel for DeepSeekLanguageModel {
|
||||
request: LanguageModelRequest,
|
||||
cx: &AsyncApp,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<LanguageModelCompletionEvent>>>> {
|
||||
let request = request.into_deepseek(self.model.id().to_string(), self.max_output_tokens());
|
||||
let request = into_deepseek(
|
||||
request,
|
||||
self.model.id().to_string(),
|
||||
self.max_output_tokens(),
|
||||
);
|
||||
let stream = self.stream_completion(request, cx);
|
||||
|
||||
async move {
|
||||
@@ -346,8 +361,11 @@ impl LanguageModel for DeepSeekLanguageModel {
|
||||
schema: serde_json::Value,
|
||||
cx: &AsyncApp,
|
||||
) -> BoxFuture<'static, Result<futures::stream::BoxStream<'static, Result<String>>>> {
|
||||
let mut deepseek_request =
|
||||
request.into_deepseek(self.model.id().to_string(), self.max_output_tokens());
|
||||
let mut deepseek_request = into_deepseek(
|
||||
request,
|
||||
self.model.id().to_string(),
|
||||
self.max_output_tokens(),
|
||||
);
|
||||
|
||||
deepseek_request.tools = vec![deepseek::ToolDefinition::Function {
|
||||
function: deepseek::FunctionDefinition {
|
||||
@@ -391,6 +409,93 @@ impl LanguageModel for DeepSeekLanguageModel {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_deepseek(
|
||||
request: LanguageModelRequest,
|
||||
model: String,
|
||||
max_output_tokens: Option<u32>,
|
||||
) -> deepseek::Request {
|
||||
let is_reasoner = model == "deepseek-reasoner";
|
||||
|
||||
let len = request.messages.len();
|
||||
let merged_messages =
|
||||
request
|
||||
.messages
|
||||
.into_iter()
|
||||
.fold(Vec::with_capacity(len), |mut acc, msg| {
|
||||
let role = msg.role;
|
||||
let content = msg.string_contents();
|
||||
|
||||
if is_reasoner {
|
||||
if let Some(last_msg) = acc.last_mut() {
|
||||
match (last_msg, role) {
|
||||
(deepseek::RequestMessage::User { content: last }, Role::User) => {
|
||||
last.push(' ');
|
||||
last.push_str(&content);
|
||||
return acc;
|
||||
}
|
||||
|
||||
(
|
||||
deepseek::RequestMessage::Assistant {
|
||||
content: last_content,
|
||||
..
|
||||
},
|
||||
Role::Assistant,
|
||||
) => {
|
||||
*last_content = last_content
|
||||
.take()
|
||||
.map(|c| {
|
||||
let mut s =
|
||||
String::with_capacity(c.len() + content.len() + 1);
|
||||
s.push_str(&c);
|
||||
s.push(' ');
|
||||
s.push_str(&content);
|
||||
s
|
||||
})
|
||||
.or(Some(content));
|
||||
|
||||
return acc;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
acc.push(match role {
|
||||
Role::User => deepseek::RequestMessage::User { content },
|
||||
Role::Assistant => deepseek::RequestMessage::Assistant {
|
||||
content: Some(content),
|
||||
tool_calls: Vec::new(),
|
||||
},
|
||||
Role::System => deepseek::RequestMessage::System { content },
|
||||
});
|
||||
acc
|
||||
});
|
||||
|
||||
deepseek::Request {
|
||||
model,
|
||||
messages: merged_messages,
|
||||
stream: true,
|
||||
max_tokens: max_output_tokens,
|
||||
temperature: if is_reasoner {
|
||||
None
|
||||
} else {
|
||||
request.temperature
|
||||
},
|
||||
response_format: None,
|
||||
tools: request
|
||||
.tools
|
||||
.into_iter()
|
||||
.map(|tool| deepseek::ToolDefinition::Function {
|
||||
function: deepseek::FunctionDefinition {
|
||||
name: tool.name,
|
||||
description: Some(tool.description),
|
||||
parameters: Some(tool.input_schema),
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
struct ConfigurationView {
|
||||
api_key_editor: Entity<Editor>,
|
||||
state: Entity<State>,
|
||||
|
||||
@@ -166,6 +166,17 @@ impl LanguageModelProvider for GoogleLanguageModelProvider {
|
||||
IconName::AiGoogle
|
||||
}
|
||||
|
||||
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
|
||||
let model = google_ai::Model::default();
|
||||
Some(Arc::new(GoogleLanguageModel {
|
||||
id: LanguageModelId::from(model.id().to_string()),
|
||||
model,
|
||||
state: self.state.clone(),
|
||||
http_client: self.http_client.clone(),
|
||||
rate_limiter: RateLimiter::new(4),
|
||||
}))
|
||||
}
|
||||
|
||||
fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
|
||||
let mut models = BTreeMap::default();
|
||||
|
||||
@@ -261,7 +272,7 @@ impl LanguageModel for GoogleLanguageModel {
|
||||
request: LanguageModelRequest,
|
||||
cx: &App,
|
||||
) -> BoxFuture<'static, Result<usize>> {
|
||||
let request = request.into_google(self.model.id().to_string());
|
||||
let request = into_google(request, self.model.id().to_string());
|
||||
let http_client = self.http_client.clone();
|
||||
let api_key = self.state.read(cx).api_key.clone();
|
||||
|
||||
@@ -292,7 +303,7 @@ impl LanguageModel for GoogleLanguageModel {
|
||||
'static,
|
||||
Result<futures::stream::BoxStream<'static, Result<LanguageModelCompletionEvent>>>,
|
||||
> {
|
||||
let request = request.into_google(self.model.id().to_string());
|
||||
let request = into_google(request, self.model.id().to_string());
|
||||
|
||||
let http_client = self.http_client.clone();
|
||||
let Ok((api_key, api_url)) = cx.read_entity(&self.state, |state, cx| {
|
||||
@@ -330,6 +341,38 @@ impl LanguageModel for GoogleLanguageModel {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_google(
|
||||
request: LanguageModelRequest,
|
||||
model: String,
|
||||
) -> google_ai::GenerateContentRequest {
|
||||
google_ai::GenerateContentRequest {
|
||||
model,
|
||||
contents: request
|
||||
.messages
|
||||
.into_iter()
|
||||
.map(|msg| google_ai::Content {
|
||||
parts: vec![google_ai::Part::TextPart(google_ai::TextPart {
|
||||
text: msg.string_contents(),
|
||||
})],
|
||||
role: match msg.role {
|
||||
Role::User => google_ai::Role::User,
|
||||
Role::Assistant => google_ai::Role::Model,
|
||||
Role::System => google_ai::Role::User, // Google AI doesn't have a system role
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
generation_config: Some(google_ai::GenerationConfig {
|
||||
candidate_count: Some(1),
|
||||
stop_sequences: Some(request.stop),
|
||||
max_output_tokens: None,
|
||||
temperature: request.temperature.map(|t| t as f64).or(Some(1.0)),
|
||||
top_p: None,
|
||||
top_k: None,
|
||||
}),
|
||||
safety_settings: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn count_google_tokens(
|
||||
request: LanguageModelRequest,
|
||||
cx: &App,
|
||||
|
||||
@@ -152,6 +152,10 @@ impl LanguageModelProvider for LmStudioLanguageModelProvider {
|
||||
IconName::AiLmStudio
|
||||
}
|
||||
|
||||
fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {
|
||||
self.provided_models(cx).into_iter().next()
|
||||
}
|
||||
|
||||
fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
|
||||
let mut models: BTreeMap<String, lmstudio::Model> = BTreeMap::default();
|
||||
|
||||
|
||||
@@ -167,6 +167,17 @@ impl LanguageModelProvider for MistralLanguageModelProvider {
|
||||
IconName::AiMistral
|
||||
}
|
||||
|
||||
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
|
||||
let model = mistral::Model::default();
|
||||
Some(Arc::new(MistralLanguageModel {
|
||||
id: LanguageModelId::from(model.id().to_string()),
|
||||
model,
|
||||
state: self.state.clone(),
|
||||
http_client: self.http_client.clone(),
|
||||
request_limiter: RateLimiter::new(4),
|
||||
}))
|
||||
}
|
||||
|
||||
fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
|
||||
let mut models = BTreeMap::default();
|
||||
|
||||
@@ -323,7 +334,11 @@ impl LanguageModel for MistralLanguageModel {
|
||||
request: LanguageModelRequest,
|
||||
cx: &AsyncApp,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<LanguageModelCompletionEvent>>>> {
|
||||
let request = request.into_mistral(self.model.id().to_string(), self.max_output_tokens());
|
||||
let request = into_mistral(
|
||||
request,
|
||||
self.model.id().to_string(),
|
||||
self.max_output_tokens(),
|
||||
);
|
||||
let stream = self.stream_completion(request, cx);
|
||||
|
||||
async move {
|
||||
@@ -358,7 +373,7 @@ impl LanguageModel for MistralLanguageModel {
|
||||
schema: serde_json::Value,
|
||||
cx: &AsyncApp,
|
||||
) -> BoxFuture<'static, Result<futures::stream::BoxStream<'static, Result<String>>>> {
|
||||
let mut request = request.into_mistral(self.model.id().into(), self.max_output_tokens());
|
||||
let mut request = into_mistral(request, self.model.id().into(), self.max_output_tokens());
|
||||
request.tools = vec![mistral::ToolDefinition::Function {
|
||||
function: mistral::FunctionDefinition {
|
||||
name: tool_name.clone(),
|
||||
@@ -400,6 +415,52 @@ impl LanguageModel for MistralLanguageModel {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_mistral(
|
||||
request: LanguageModelRequest,
|
||||
model: String,
|
||||
max_output_tokens: Option<u32>,
|
||||
) -> mistral::Request {
|
||||
let len = request.messages.len();
|
||||
let merged_messages =
|
||||
request
|
||||
.messages
|
||||
.into_iter()
|
||||
.fold(Vec::with_capacity(len), |mut acc, msg| {
|
||||
let role = msg.role;
|
||||
let content = msg.string_contents();
|
||||
|
||||
acc.push(match role {
|
||||
Role::User => mistral::RequestMessage::User { content },
|
||||
Role::Assistant => mistral::RequestMessage::Assistant {
|
||||
content: Some(content),
|
||||
tool_calls: Vec::new(),
|
||||
},
|
||||
Role::System => mistral::RequestMessage::System { content },
|
||||
});
|
||||
acc
|
||||
});
|
||||
|
||||
mistral::Request {
|
||||
model,
|
||||
messages: merged_messages,
|
||||
stream: true,
|
||||
max_tokens: max_output_tokens,
|
||||
temperature: request.temperature,
|
||||
response_format: None,
|
||||
tools: request
|
||||
.tools
|
||||
.into_iter()
|
||||
.map(|tool| mistral::ToolDefinition::Function {
|
||||
function: mistral::FunctionDefinition {
|
||||
name: tool.name,
|
||||
description: Some(tool.description),
|
||||
parameters: Some(tool.input_schema),
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
struct ConfigurationView {
|
||||
api_key_editor: Entity<Editor>,
|
||||
state: gpui::Entity<State>,
|
||||
|
||||
@@ -157,6 +157,10 @@ impl LanguageModelProvider for OllamaLanguageModelProvider {
|
||||
IconName::AiOllama
|
||||
}
|
||||
|
||||
fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {
|
||||
self.provided_models(cx).into_iter().next()
|
||||
}
|
||||
|
||||
fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
|
||||
let mut models: BTreeMap<String, ollama::Model> = BTreeMap::default();
|
||||
|
||||
|
||||
@@ -169,6 +169,17 @@ impl LanguageModelProvider for OpenAiLanguageModelProvider {
|
||||
IconName::AiOpenAi
|
||||
}
|
||||
|
||||
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
|
||||
let model = open_ai::Model::default();
|
||||
Some(Arc::new(OpenAiLanguageModel {
|
||||
id: LanguageModelId::from(model.id().to_string()),
|
||||
model,
|
||||
state: self.state.clone(),
|
||||
http_client: self.http_client.clone(),
|
||||
request_limiter: RateLimiter::new(4),
|
||||
}))
|
||||
}
|
||||
|
||||
fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
|
||||
let mut models = BTreeMap::default();
|
||||
|
||||
@@ -307,7 +318,7 @@ impl LanguageModel for OpenAiLanguageModel {
|
||||
'static,
|
||||
Result<futures::stream::BoxStream<'static, Result<LanguageModelCompletionEvent>>>,
|
||||
> {
|
||||
let request = request.into_open_ai(self.model.id().into(), self.max_output_tokens());
|
||||
let request = into_open_ai(request, self.model.id().into(), self.max_output_tokens());
|
||||
let completions = self.stream_completion(request, cx);
|
||||
async move {
|
||||
Ok(open_ai::extract_text_from_events(completions.await?)
|
||||
@@ -325,7 +336,7 @@ impl LanguageModel for OpenAiLanguageModel {
|
||||
schema: serde_json::Value,
|
||||
cx: &AsyncApp,
|
||||
) -> BoxFuture<'static, Result<futures::stream::BoxStream<'static, Result<String>>>> {
|
||||
let mut request = request.into_open_ai(self.model.id().into(), self.max_output_tokens());
|
||||
let mut request = into_open_ai(request, self.model.id().into(), self.max_output_tokens());
|
||||
request.tool_choice = Some(ToolChoice::Other(ToolDefinition::Function {
|
||||
function: FunctionDefinition {
|
||||
name: tool_name.clone(),
|
||||
@@ -355,6 +366,39 @@ impl LanguageModel for OpenAiLanguageModel {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_open_ai(
|
||||
request: LanguageModelRequest,
|
||||
model: String,
|
||||
max_output_tokens: Option<u32>,
|
||||
) -> open_ai::Request {
|
||||
let stream = !model.starts_with("o1-");
|
||||
open_ai::Request {
|
||||
model,
|
||||
messages: request
|
||||
.messages
|
||||
.into_iter()
|
||||
.map(|msg| match msg.role {
|
||||
Role::User => open_ai::RequestMessage::User {
|
||||
content: msg.string_contents(),
|
||||
},
|
||||
Role::Assistant => open_ai::RequestMessage::Assistant {
|
||||
content: Some(msg.string_contents()),
|
||||
tool_calls: Vec::new(),
|
||||
},
|
||||
Role::System => open_ai::RequestMessage::System {
|
||||
content: msg.string_contents(),
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
stream,
|
||||
stop: request.stop,
|
||||
temperature: request.temperature.unwrap_or(1.0),
|
||||
max_tokens: max_output_tokens,
|
||||
tools: Vec::new(),
|
||||
tool_choice: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn count_open_ai_tokens(
|
||||
request: LanguageModelRequest,
|
||||
model: open_ai::Model,
|
||||
|
||||
@@ -15,5 +15,5 @@ brackets = [
|
||||
]
|
||||
|
||||
auto_indent_using_last_non_empty_line = false
|
||||
increase_indent_pattern = ":\\s*$"
|
||||
increase_indent_pattern = "^[^#].*:\\s*$"
|
||||
decrease_indent_pattern = "^\\s*(else|elif|except|finally)\\b.*:"
|
||||
|
||||
@@ -526,17 +526,25 @@ impl ContextProvider for RustContextProvider {
|
||||
cx: &App,
|
||||
) -> Option<TaskTemplates> {
|
||||
const DEFAULT_RUN_NAME_STR: &str = "RUST_DEFAULT_PACKAGE_RUN";
|
||||
let package_to_run = language_settings(Some("Rust".into()), file.as_ref(), cx)
|
||||
const CUSTOM_TARGET_DIR: &str = "RUST_TARGET_DIR";
|
||||
|
||||
let language_sets = language_settings(Some("Rust".into()), file.as_ref(), cx);
|
||||
let package_to_run = language_sets
|
||||
.tasks
|
||||
.variables
|
||||
.get(DEFAULT_RUN_NAME_STR)
|
||||
.cloned();
|
||||
let custom_target_dir = language_sets
|
||||
.tasks
|
||||
.variables
|
||||
.get(CUSTOM_TARGET_DIR)
|
||||
.cloned();
|
||||
let run_task_args = if let Some(package_to_run) = package_to_run {
|
||||
vec!["run".into(), "-p".into(), package_to_run]
|
||||
} else {
|
||||
vec!["run".into()]
|
||||
};
|
||||
Some(TaskTemplates(vec![
|
||||
let mut task_templates = vec![
|
||||
TaskTemplate {
|
||||
label: format!(
|
||||
"Check (package: {})",
|
||||
@@ -661,7 +669,25 @@ impl ContextProvider for RustContextProvider {
|
||||
cwd: Some("$ZED_DIRNAME".to_owned()),
|
||||
..TaskTemplate::default()
|
||||
},
|
||||
]))
|
||||
];
|
||||
|
||||
if let Some(custom_target_dir) = custom_target_dir {
|
||||
task_templates = task_templates
|
||||
.into_iter()
|
||||
.map(|mut task_template| {
|
||||
let mut args = task_template.args.split_off(1);
|
||||
task_template.args.append(&mut vec![
|
||||
"--target-dir".to_string(),
|
||||
custom_target_dir.clone(),
|
||||
]);
|
||||
task_template.args.append(&mut args);
|
||||
|
||||
task_template
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
Some(TaskTemplates(task_templates))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1448,7 +1448,7 @@ impl MultiBuffer {
|
||||
excerpt.range.context.start,
|
||||
))
|
||||
}
|
||||
|
||||
/// Sets excerpts, returns `true` if at least one new excerpt was added.
|
||||
pub fn set_excerpts_for_path(
|
||||
&mut self,
|
||||
path: PathKey,
|
||||
@@ -1456,7 +1456,7 @@ impl MultiBuffer {
|
||||
ranges: Vec<Range<Point>>,
|
||||
context_line_count: u32,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
) -> bool {
|
||||
let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
|
||||
|
||||
let mut insert_after = self
|
||||
@@ -1475,6 +1475,7 @@ impl MultiBuffer {
|
||||
let mut new_excerpt_ids = Vec::new();
|
||||
let mut to_remove = Vec::new();
|
||||
let mut to_insert = Vec::new();
|
||||
let mut added_a_new_excerpt = false;
|
||||
let snapshot = self.snapshot(cx);
|
||||
|
||||
let mut excerpts_cursor = snapshot.excerpts.cursor::<Option<&Locator>>(&());
|
||||
@@ -1489,6 +1490,7 @@ impl MultiBuffer {
|
||||
continue;
|
||||
}
|
||||
(Some(_), None) => {
|
||||
added_a_new_excerpt = true;
|
||||
to_insert.push(new_iter.next().unwrap());
|
||||
continue;
|
||||
}
|
||||
@@ -1552,6 +1554,8 @@ impl MultiBuffer {
|
||||
} else {
|
||||
self.buffers_by_path.insert(path, new_excerpt_ids);
|
||||
}
|
||||
|
||||
added_a_new_excerpt
|
||||
}
|
||||
|
||||
pub fn paths(&self) -> impl Iterator<Item = PathKey> + '_ {
|
||||
@@ -4964,6 +4968,23 @@ impl MultiBufferSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn excerpts_at<T>(&self, position: T) -> impl Iterator<Item = MultiBufferExcerpt<'_>>
|
||||
where
|
||||
T: ToOffset,
|
||||
{
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn reversed_excerpts_at<T>(
|
||||
&self,
|
||||
position: T,
|
||||
) -> impl Iterator<Item = MultiBufferExcerpt<'_>>
|
||||
where
|
||||
T: ToOffset,
|
||||
{
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn excerpt_before(&self, id: ExcerptId) -> Option<MultiBufferExcerpt<'_>> {
|
||||
let start_locator = self.excerpt_locator_for_id(id);
|
||||
let mut excerpts = self
|
||||
|
||||
@@ -15,7 +15,7 @@ use language::{Outline, OutlineItem};
|
||||
use ordered_float::OrderedFloat;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use settings::Settings;
|
||||
use theme::{color_alpha, ActiveTheme, ThemeSettings};
|
||||
use theme::{ActiveTheme, ThemeSettings};
|
||||
use ui::{prelude::*, ListItem, ListItemSpacing};
|
||||
use util::ResultExt;
|
||||
use workspace::{DismissDecision, ModalView};
|
||||
@@ -332,7 +332,7 @@ pub fn render_item<T>(
|
||||
cx: &App,
|
||||
) -> StyledText {
|
||||
let highlight_style = HighlightStyle {
|
||||
background_color: Some(color_alpha(cx.theme().colors().text_accent, 0.3)),
|
||||
background_color: Some(cx.theme().colors().text_accent.alpha(0.3)),
|
||||
..Default::default()
|
||||
};
|
||||
let custom_highlights = match_ranges
|
||||
|
||||
@@ -49,6 +49,7 @@ pub fn panel_button(label: impl Into<SharedString>) -> ui::Button {
|
||||
let id = ElementId::Name(label.clone().to_lowercase().replace(' ', "_").into());
|
||||
ui::Button::new(id, label)
|
||||
.label_size(ui::LabelSize::Small)
|
||||
.icon_size(ui::IconSize::Small)
|
||||
// TODO: Change this once we use on_surface_bg in button_like
|
||||
.layer(ui::ElevationIndex::ModalSurface)
|
||||
.size(ui::ButtonSize::Compact)
|
||||
|
||||
@@ -5,7 +5,7 @@ use anyhow::{Context as _, Result};
|
||||
use client::ProjectId;
|
||||
use futures::channel::{mpsc, oneshot};
|
||||
use futures::StreamExt as _;
|
||||
use git::repository::{Branch, CommitDetails, ResetMode};
|
||||
use git::repository::{Branch, CommitDetails, PushOptions, Remote, ResetMode};
|
||||
use git::{
|
||||
repository::{GitRepository, RepoPath},
|
||||
status::{GitSummary, TrackedSummary},
|
||||
@@ -74,6 +74,18 @@ pub enum Message {
|
||||
Stage(GitRepo, Vec<RepoPath>),
|
||||
Unstage(GitRepo, Vec<RepoPath>),
|
||||
SetIndexText(GitRepo, RepoPath, Option<String>),
|
||||
Push {
|
||||
repo: GitRepo,
|
||||
branch_name: SharedString,
|
||||
remote_name: SharedString,
|
||||
options: Option<PushOptions>,
|
||||
},
|
||||
Pull {
|
||||
repo: GitRepo,
|
||||
branch_name: SharedString,
|
||||
remote_name: SharedString,
|
||||
},
|
||||
Fetch(GitRepo),
|
||||
}
|
||||
|
||||
pub enum GitEvent {
|
||||
@@ -107,6 +119,10 @@ impl GitStore {
|
||||
}
|
||||
|
||||
pub fn init(client: &AnyProtoClient) {
|
||||
client.add_entity_request_handler(Self::handle_get_remotes);
|
||||
client.add_entity_request_handler(Self::handle_push);
|
||||
client.add_entity_request_handler(Self::handle_pull);
|
||||
client.add_entity_request_handler(Self::handle_fetch);
|
||||
client.add_entity_request_handler(Self::handle_stage);
|
||||
client.add_entity_request_handler(Self::handle_unstage);
|
||||
client.add_entity_request_handler(Self::handle_commit);
|
||||
@@ -242,8 +258,10 @@ impl GitStore {
|
||||
mpsc::unbounded::<(Message, oneshot::Sender<Result<()>>)>();
|
||||
cx.spawn(|_, cx| async move {
|
||||
while let Some((msg, respond)) = update_receiver.next().await {
|
||||
let result = cx.background_spawn(Self::process_git_msg(msg)).await;
|
||||
respond.send(result).ok();
|
||||
if !respond.is_canceled() {
|
||||
let result = cx.background_spawn(Self::process_git_msg(msg)).await;
|
||||
respond.send(result).ok();
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
@@ -252,6 +270,94 @@ impl GitStore {
|
||||
|
||||
async fn process_git_msg(msg: Message) -> Result<()> {
|
||||
match msg {
|
||||
Message::Fetch(repo) => {
|
||||
match repo {
|
||||
GitRepo::Local(git_repository) => git_repository.fetch()?,
|
||||
GitRepo::Remote {
|
||||
project_id,
|
||||
client,
|
||||
worktree_id,
|
||||
work_directory_id,
|
||||
} => {
|
||||
client
|
||||
.request(proto::Fetch {
|
||||
project_id: project_id.0,
|
||||
worktree_id: worktree_id.to_proto(),
|
||||
work_directory_id: work_directory_id.to_proto(),
|
||||
})
|
||||
.await
|
||||
.context("sending fetch request")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Message::Pull {
|
||||
repo,
|
||||
branch_name,
|
||||
remote_name,
|
||||
} => {
|
||||
match repo {
|
||||
GitRepo::Local(git_repository) => {
|
||||
git_repository.pull(&branch_name, &remote_name)?
|
||||
}
|
||||
GitRepo::Remote {
|
||||
project_id,
|
||||
client,
|
||||
worktree_id,
|
||||
work_directory_id,
|
||||
} => {
|
||||
client
|
||||
.request(proto::Pull {
|
||||
project_id: project_id.0,
|
||||
worktree_id: worktree_id.to_proto(),
|
||||
work_directory_id: work_directory_id.to_proto(),
|
||||
branch_name: branch_name.to_string(),
|
||||
remote_name: remote_name.to_string(),
|
||||
})
|
||||
.await
|
||||
.context("sending pull request")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Message::Push {
|
||||
repo,
|
||||
branch_name,
|
||||
remote_name,
|
||||
options,
|
||||
} => {
|
||||
match repo {
|
||||
GitRepo::Local(git_repository) => {
|
||||
git_repository.push(&branch_name, &remote_name, options)?
|
||||
}
|
||||
GitRepo::Remote {
|
||||
project_id,
|
||||
client,
|
||||
worktree_id,
|
||||
work_directory_id,
|
||||
} => {
|
||||
client
|
||||
.request(proto::Push {
|
||||
project_id: project_id.0,
|
||||
worktree_id: worktree_id.to_proto(),
|
||||
work_directory_id: work_directory_id.to_proto(),
|
||||
branch_name: branch_name.to_string(),
|
||||
remote_name: remote_name.to_string(),
|
||||
options: options.map(|options| match options {
|
||||
PushOptions::Force => proto::push::PushOptions::Force,
|
||||
PushOptions::SetUpstream => {
|
||||
proto::push::PushOptions::SetUpstream
|
||||
}
|
||||
}
|
||||
as i32),
|
||||
})
|
||||
.await
|
||||
.context("sending push request")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Message::Stage(repo, paths) => {
|
||||
match repo {
|
||||
GitRepo::Local(repo) => repo.stage_paths(&paths)?,
|
||||
@@ -413,6 +519,73 @@ impl GitStore {
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_fetch(
|
||||
this: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::Fetch>,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<proto::Ack> {
|
||||
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
|
||||
let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
|
||||
let repository_handle =
|
||||
Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
|
||||
|
||||
repository_handle
|
||||
.update(&mut cx, |repository_handle, _cx| repository_handle.fetch())?
|
||||
.await??;
|
||||
Ok(proto::Ack {})
|
||||
}
|
||||
|
||||
async fn handle_push(
|
||||
this: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::Push>,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<proto::Ack> {
|
||||
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
|
||||
let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
|
||||
let repository_handle =
|
||||
Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
|
||||
|
||||
let options = envelope
|
||||
.payload
|
||||
.options
|
||||
.as_ref()
|
||||
.map(|_| match envelope.payload.options() {
|
||||
proto::push::PushOptions::SetUpstream => git::repository::PushOptions::SetUpstream,
|
||||
proto::push::PushOptions::Force => git::repository::PushOptions::Force,
|
||||
});
|
||||
|
||||
let branch_name = envelope.payload.branch_name.into();
|
||||
let remote_name = envelope.payload.remote_name.into();
|
||||
|
||||
repository_handle
|
||||
.update(&mut cx, |repository_handle, _cx| {
|
||||
repository_handle.push(branch_name, remote_name, options)
|
||||
})?
|
||||
.await??;
|
||||
Ok(proto::Ack {})
|
||||
}
|
||||
|
||||
async fn handle_pull(
|
||||
this: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::Pull>,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<proto::Ack> {
|
||||
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
|
||||
let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
|
||||
let repository_handle =
|
||||
Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
|
||||
|
||||
let branch_name = envelope.payload.branch_name.into();
|
||||
let remote_name = envelope.payload.remote_name.into();
|
||||
|
||||
repository_handle
|
||||
.update(&mut cx, |repository_handle, _cx| {
|
||||
repository_handle.pull(branch_name, remote_name)
|
||||
})?
|
||||
.await??;
|
||||
Ok(proto::Ack {})
|
||||
}
|
||||
|
||||
async fn handle_stage(
|
||||
this: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::Stage>,
|
||||
@@ -509,6 +682,34 @@ impl GitStore {
|
||||
Ok(proto::Ack {})
|
||||
}
|
||||
|
||||
async fn handle_get_remotes(
|
||||
this: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::GetRemotes>,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<proto::GetRemotesResponse> {
|
||||
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
|
||||
let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
|
||||
let repository_handle =
|
||||
Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
|
||||
|
||||
let branch_name = envelope.payload.branch_name;
|
||||
|
||||
let remotes = repository_handle
|
||||
.update(&mut cx, |repository_handle, cx| {
|
||||
repository_handle.get_remotes(branch_name, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
Ok(proto::GetRemotesResponse {
|
||||
remotes: remotes
|
||||
.into_iter()
|
||||
.map(|remotes| proto::get_remotes_response::Remote {
|
||||
name: remotes.name.to_string(),
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_show(
|
||||
this: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::GitShow>,
|
||||
@@ -648,7 +849,7 @@ impl Repository {
|
||||
(self.worktree_id, self.repository_entry.work_directory_id())
|
||||
}
|
||||
|
||||
pub fn branch(&self) -> Option<&Branch> {
|
||||
pub fn current_branch(&self) -> Option<&Branch> {
|
||||
self.repository_entry.branch()
|
||||
}
|
||||
|
||||
@@ -802,35 +1003,19 @@ impl Repository {
|
||||
commit: &str,
|
||||
paths: Vec<RepoPath>,
|
||||
) -> oneshot::Receiver<Result<()>> {
|
||||
let (result_tx, result_rx) = futures::channel::oneshot::channel();
|
||||
let commit = commit.to_string().into();
|
||||
self.update_sender
|
||||
.unbounded_send((
|
||||
Message::CheckoutFiles {
|
||||
repo: self.git_repo.clone(),
|
||||
commit,
|
||||
paths,
|
||||
},
|
||||
result_tx,
|
||||
))
|
||||
.ok();
|
||||
result_rx
|
||||
self.send_message(Message::CheckoutFiles {
|
||||
repo: self.git_repo.clone(),
|
||||
commit: commit.to_string().into(),
|
||||
paths,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn reset(&self, commit: &str, reset_mode: ResetMode) -> oneshot::Receiver<Result<()>> {
|
||||
let (result_tx, result_rx) = futures::channel::oneshot::channel();
|
||||
let commit = commit.to_string().into();
|
||||
self.update_sender
|
||||
.unbounded_send((
|
||||
Message::Reset {
|
||||
repo: self.git_repo.clone(),
|
||||
commit,
|
||||
reset_mode,
|
||||
},
|
||||
result_tx,
|
||||
))
|
||||
.ok();
|
||||
result_rx
|
||||
self.send_message(Message::Reset {
|
||||
repo: self.git_repo.clone(),
|
||||
commit: commit.to_string().into(),
|
||||
reset_mode,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn show(&self, commit: &str, cx: &Context<Self>) -> Task<Result<CommitDetails>> {
|
||||
@@ -987,18 +1172,41 @@ impl Repository {
|
||||
message: SharedString,
|
||||
name_and_email: Option<(SharedString, SharedString)>,
|
||||
) -> oneshot::Receiver<Result<()>> {
|
||||
let (result_tx, result_rx) = futures::channel::oneshot::channel();
|
||||
self.update_sender
|
||||
.unbounded_send((
|
||||
Message::Commit {
|
||||
git_repo: self.git_repo.clone(),
|
||||
message,
|
||||
name_and_email,
|
||||
},
|
||||
result_tx,
|
||||
))
|
||||
.ok();
|
||||
result_rx
|
||||
self.send_message(Message::Commit {
|
||||
git_repo: self.git_repo.clone(),
|
||||
message,
|
||||
name_and_email,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn fetch(&self) -> oneshot::Receiver<Result<()>> {
|
||||
self.send_message(Message::Fetch(self.git_repo.clone()))
|
||||
}
|
||||
|
||||
pub fn push(
|
||||
&self,
|
||||
branch: SharedString,
|
||||
remote: SharedString,
|
||||
options: Option<PushOptions>,
|
||||
) -> oneshot::Receiver<Result<()>> {
|
||||
self.send_message(Message::Push {
|
||||
repo: self.git_repo.clone(),
|
||||
branch_name: branch,
|
||||
remote_name: remote,
|
||||
options,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn pull(
|
||||
&self,
|
||||
branch: SharedString,
|
||||
remote: SharedString,
|
||||
) -> oneshot::Receiver<Result<()>> {
|
||||
self.send_message(Message::Pull {
|
||||
repo: self.git_repo.clone(),
|
||||
branch_name: branch,
|
||||
remote_name: remote,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_index_text(
|
||||
@@ -1006,13 +1214,49 @@ impl Repository {
|
||||
path: &RepoPath,
|
||||
content: Option<String>,
|
||||
) -> oneshot::Receiver<anyhow::Result<()>> {
|
||||
self.send_message(Message::SetIndexText(
|
||||
self.git_repo.clone(),
|
||||
path.clone(),
|
||||
content,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn get_remotes(&self, branch_name: Option<String>, cx: &App) -> Task<Result<Vec<Remote>>> {
|
||||
match self.git_repo.clone() {
|
||||
GitRepo::Local(git_repository) => {
|
||||
cx.background_spawn(
|
||||
async move { git_repository.get_remotes(branch_name.as_deref()) },
|
||||
)
|
||||
}
|
||||
GitRepo::Remote {
|
||||
project_id,
|
||||
client,
|
||||
worktree_id,
|
||||
work_directory_id,
|
||||
} => cx.background_spawn(async move {
|
||||
let response = client
|
||||
.request(proto::GetRemotes {
|
||||
project_id: project_id.0,
|
||||
worktree_id: worktree_id.to_proto(),
|
||||
work_directory_id: work_directory_id.to_proto(),
|
||||
branch_name,
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(response
|
||||
.remotes
|
||||
.into_iter()
|
||||
.map(|remotes| git::repository::Remote {
|
||||
name: remotes.name.into(),
|
||||
})
|
||||
.collect())
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn send_message(&self, message: Message) -> oneshot::Receiver<anyhow::Result<()>> {
|
||||
let (result_tx, result_rx) = futures::channel::oneshot::channel();
|
||||
self.update_sender
|
||||
.unbounded_send((
|
||||
Message::SetIndexText(self.git_repo.clone(), path.clone(), content),
|
||||
result_tx,
|
||||
))
|
||||
.ok();
|
||||
self.update_sender.unbounded_send((message, result_tx)).ok();
|
||||
result_rx
|
||||
}
|
||||
}
|
||||
|
||||
@@ -946,12 +946,17 @@ impl WorktreeStore {
|
||||
upstream: proto_branch.upstream.map(|upstream| {
|
||||
git::repository::Upstream {
|
||||
ref_name: upstream.ref_name.into(),
|
||||
tracking: upstream.tracking.map(|tracking| {
|
||||
git::repository::UpstreamTracking {
|
||||
ahead: tracking.ahead as u32,
|
||||
behind: tracking.behind as u32,
|
||||
}
|
||||
}),
|
||||
tracking: upstream
|
||||
.tracking
|
||||
.map(|tracking| {
|
||||
git::repository::UpstreamTracking::Tracked(
|
||||
git::repository::UpstreamTrackingStatus {
|
||||
ahead: tracking.ahead as u32,
|
||||
behind: tracking.behind as u32,
|
||||
},
|
||||
)
|
||||
})
|
||||
.unwrap_or(git::repository::UpstreamTracking::Gone),
|
||||
}
|
||||
}),
|
||||
most_recent_commit: proto_branch.most_recent_commit.map(|commit| {
|
||||
|
||||
@@ -265,7 +265,7 @@ struct ItemColors {
|
||||
default: Hsla,
|
||||
hover: Hsla,
|
||||
drag_over: Hsla,
|
||||
marked_active: Hsla,
|
||||
marked: Hsla,
|
||||
focused: Hsla,
|
||||
}
|
||||
|
||||
@@ -274,10 +274,10 @@ fn get_item_color(cx: &App) -> ItemColors {
|
||||
|
||||
ItemColors {
|
||||
default: colors.panel_background,
|
||||
hover: colors.ghost_element_hover,
|
||||
drag_over: colors.drop_target_background,
|
||||
marked_active: colors.element_selected,
|
||||
hover: colors.element_hover,
|
||||
marked: colors.element_selected,
|
||||
focused: colors.panel_focused_border,
|
||||
drag_over: colors.drop_target_background,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,6 +302,9 @@ impl ProjectPanel {
|
||||
this.reveal_entry(project.clone(), *entry_id, true, cx);
|
||||
}
|
||||
}
|
||||
project::Event::ActiveEntryChanged(None) => {
|
||||
this.marked_entries.clear();
|
||||
}
|
||||
project::Event::RevealInProjectPanel(entry_id) => {
|
||||
this.reveal_entry(project.clone(), *entry_id, false, cx);
|
||||
cx.emit(PanelEvent::Activate);
|
||||
@@ -3546,18 +3549,16 @@ impl ProjectPanel {
|
||||
marked_selections: selections,
|
||||
};
|
||||
|
||||
let bg_color = if is_marked || is_active {
|
||||
item_colors.marked_active
|
||||
let bg_color = if is_marked {
|
||||
item_colors.marked
|
||||
} else {
|
||||
item_colors.default
|
||||
};
|
||||
|
||||
let bg_hover_color = if self.mouse_down || is_marked || is_active {
|
||||
item_colors.marked_active
|
||||
} else if !is_active {
|
||||
item_colors.hover
|
||||
let bg_hover_color = if is_marked {
|
||||
item_colors.marked
|
||||
} else {
|
||||
item_colors.default
|
||||
item_colors.hover
|
||||
};
|
||||
|
||||
let border_color =
|
||||
@@ -4235,16 +4236,11 @@ impl ProjectPanel {
|
||||
let worktree_id = worktree.id();
|
||||
self.expand_entry(worktree_id, entry_id, cx);
|
||||
self.update_visible_entries(Some((worktree_id, entry_id)), cx);
|
||||
|
||||
if self.marked_entries.len() == 1
|
||||
&& self
|
||||
.marked_entries
|
||||
.first()
|
||||
.filter(|entry| entry.entry_id == entry_id)
|
||||
.is_none()
|
||||
{
|
||||
self.marked_entries.clear();
|
||||
}
|
||||
self.marked_entries.clear();
|
||||
self.marked_entries.insert(SelectedEntry {
|
||||
worktree_id,
|
||||
entry_id,
|
||||
});
|
||||
self.autoscroll(cx);
|
||||
cx.notify();
|
||||
}
|
||||
@@ -7333,7 +7329,7 @@ mod tests {
|
||||
select_path(&panel, "root/new", cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&["v root", " new <== selected"]
|
||||
&["v root", " new <== selected <== marked"]
|
||||
);
|
||||
panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
|
||||
panel.update_in(cx, |panel, window, cx| {
|
||||
@@ -7767,7 +7763,7 @@ mod tests {
|
||||
" > .git",
|
||||
" v dir_1",
|
||||
" > gitignored_dir",
|
||||
" file_1.py <== selected",
|
||||
" file_1.py <== selected <== marked",
|
||||
" file_2.py",
|
||||
" file_3.py",
|
||||
" > dir_2",
|
||||
@@ -7793,7 +7789,7 @@ mod tests {
|
||||
" file_2.py",
|
||||
" file_3.py",
|
||||
" v dir_2",
|
||||
" file_1.py <== selected",
|
||||
" file_1.py <== selected <== marked",
|
||||
" file_2.py",
|
||||
" file_3.py",
|
||||
" .gitignore",
|
||||
@@ -7820,7 +7816,7 @@ mod tests {
|
||||
" file_2.py",
|
||||
" file_3.py",
|
||||
" v dir_2",
|
||||
" file_1.py <== selected",
|
||||
" file_1.py <== selected <== marked",
|
||||
" file_2.py",
|
||||
" file_3.py",
|
||||
" .gitignore",
|
||||
@@ -7841,7 +7837,7 @@ mod tests {
|
||||
" > .git",
|
||||
" v dir_1",
|
||||
" v gitignored_dir",
|
||||
" file_a.py <== selected",
|
||||
" file_a.py <== selected <== marked",
|
||||
" file_b.py",
|
||||
" file_c.py",
|
||||
" file_1.py",
|
||||
@@ -7996,7 +7992,7 @@ mod tests {
|
||||
" > .git",
|
||||
" v dir_1",
|
||||
" > gitignored_dir",
|
||||
" file_1.py <== selected",
|
||||
" file_1.py <== selected <== marked",
|
||||
" file_2.py",
|
||||
" file_3.py",
|
||||
" > dir_2",
|
||||
@@ -8022,7 +8018,7 @@ mod tests {
|
||||
" file_2.py",
|
||||
" file_3.py",
|
||||
" v dir_2",
|
||||
" file_1.py <== selected",
|
||||
" file_1.py <== selected <== marked",
|
||||
" file_2.py",
|
||||
" file_3.py",
|
||||
" .gitignore",
|
||||
@@ -8043,7 +8039,7 @@ mod tests {
|
||||
" > .git",
|
||||
" v dir_1",
|
||||
" v gitignored_dir",
|
||||
" file_a.py <== selected",
|
||||
" file_a.py <== selected <== marked",
|
||||
" file_b.py",
|
||||
" file_c.py",
|
||||
" file_1.py",
|
||||
|
||||
@@ -321,7 +321,13 @@ message Envelope {
|
||||
GitCommitDetails git_commit_details = 302;
|
||||
|
||||
SetIndexText set_index_text = 299;
|
||||
GitCheckoutFiles git_checkout_files = 303; // current max
|
||||
GitCheckoutFiles git_checkout_files = 303;
|
||||
|
||||
Push push = 304;
|
||||
Fetch fetch = 305;
|
||||
GetRemotes get_remotes = 306;
|
||||
GetRemotesResponse get_remotes_response = 307;
|
||||
Pull pull = 308; // current max
|
||||
}
|
||||
|
||||
reserved 87 to 88;
|
||||
@@ -2772,3 +2778,46 @@ message OpenCommitMessageBuffer {
|
||||
uint64 worktree_id = 2;
|
||||
uint64 work_directory_id = 3;
|
||||
}
|
||||
|
||||
message Push {
|
||||
uint64 project_id = 1;
|
||||
uint64 worktree_id = 2;
|
||||
uint64 work_directory_id = 3;
|
||||
string remote_name = 4;
|
||||
string branch_name = 5;
|
||||
optional PushOptions options = 6;
|
||||
|
||||
enum PushOptions {
|
||||
SET_UPSTREAM = 0;
|
||||
FORCE = 1;
|
||||
}
|
||||
}
|
||||
|
||||
message Fetch {
|
||||
uint64 project_id = 1;
|
||||
uint64 worktree_id = 2;
|
||||
uint64 work_directory_id = 3;
|
||||
}
|
||||
|
||||
message GetRemotes {
|
||||
uint64 project_id = 1;
|
||||
uint64 worktree_id = 2;
|
||||
uint64 work_directory_id = 3;
|
||||
optional string branch_name = 4;
|
||||
}
|
||||
|
||||
message GetRemotesResponse {
|
||||
repeated Remote remotes = 1;
|
||||
|
||||
message Remote {
|
||||
string name = 1;
|
||||
}
|
||||
}
|
||||
|
||||
message Pull {
|
||||
uint64 project_id = 1;
|
||||
uint64 worktree_id = 2;
|
||||
uint64 work_directory_id = 3;
|
||||
string remote_name = 4;
|
||||
string branch_name = 5;
|
||||
}
|
||||
|
||||
@@ -445,6 +445,11 @@ messages!(
|
||||
(GitShow, Background),
|
||||
(GitCommitDetails, Background),
|
||||
(SetIndexText, Background),
|
||||
(Push, Background),
|
||||
(Fetch, Background),
|
||||
(GetRemotes, Background),
|
||||
(GetRemotesResponse, Background),
|
||||
(Pull, Background),
|
||||
);
|
||||
|
||||
request_messages!(
|
||||
@@ -582,6 +587,10 @@ request_messages!(
|
||||
(GitReset, Ack),
|
||||
(GitCheckoutFiles, Ack),
|
||||
(SetIndexText, Ack),
|
||||
(Push, Ack),
|
||||
(Fetch, Ack),
|
||||
(GetRemotes, GetRemotesResponse),
|
||||
(Pull, Ack),
|
||||
);
|
||||
|
||||
entity_messages!(
|
||||
@@ -678,6 +687,10 @@ entity_messages!(
|
||||
GitReset,
|
||||
GitCheckoutFiles,
|
||||
SetIndexText,
|
||||
Push,
|
||||
Fetch,
|
||||
GetRemotes,
|
||||
Pull,
|
||||
);
|
||||
|
||||
entity_messages!(
|
||||
|
||||
@@ -59,6 +59,7 @@ const DEFAULT_NUM_COLUMNS: usize = 128;
|
||||
pub fn text_style(window: &mut Window, cx: &mut App) -> TextStyle {
|
||||
let settings = ThemeSettings::get_global(cx).clone();
|
||||
|
||||
let font_size = settings.buffer_font_size(cx).into();
|
||||
let font_family = settings.buffer_font.family;
|
||||
let font_features = settings.buffer_font.features;
|
||||
let font_weight = settings.buffer_font.weight;
|
||||
@@ -71,7 +72,7 @@ pub fn text_style(window: &mut Window, cx: &mut App) -> TextStyle {
|
||||
font_features,
|
||||
font_weight,
|
||||
font_fallbacks,
|
||||
font_size: theme::get_buffer_font_size(cx).into(),
|
||||
font_size,
|
||||
font_style: FontStyle::Normal,
|
||||
line_height: window.line_height().into(),
|
||||
background_color: Some(theme.colors().terminal_ansi_background),
|
||||
|
||||
@@ -95,16 +95,12 @@ pub struct ThemeSettings {
|
||||
/// as well as the size of a [gpui::Rems] unit.
|
||||
///
|
||||
/// Changing this will impact the size of all UI elements.
|
||||
///
|
||||
/// Use [ThemeSettings::ui_font_size] to access this.
|
||||
ui_font_size: Pixels,
|
||||
/// The font used for UI elements.
|
||||
pub ui_font: Font,
|
||||
/// The font size used for buffers, and the terminal.
|
||||
///
|
||||
/// The terminal font size can be overridden using it's own setting.
|
||||
///
|
||||
/// Use [ThemeSettings::buffer_font_size] to access this.
|
||||
buffer_font_size: Pixels,
|
||||
/// The font used for buffers, and the terminal.
|
||||
///
|
||||
@@ -246,14 +242,14 @@ impl SystemAppearance {
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct AdjustedBufferFontSize(Pixels);
|
||||
struct BufferFontSize(Pixels);
|
||||
|
||||
impl Global for AdjustedBufferFontSize {}
|
||||
impl Global for BufferFontSize {}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct AdjustedUiFontSize(Pixels);
|
||||
pub(crate) struct UiFontSize(Pixels);
|
||||
|
||||
impl Global for AdjustedUiFontSize {}
|
||||
impl Global for UiFontSize {}
|
||||
|
||||
/// Represents the selection of a theme, which can be either static or dynamic.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
@@ -568,16 +564,18 @@ impl ThemeSettings {
|
||||
/// Returns the buffer font size.
|
||||
pub fn buffer_font_size(&self, cx: &App) -> Pixels {
|
||||
let font_size = cx
|
||||
.try_global::<AdjustedBufferFontSize>()
|
||||
.map_or(self.buffer_font_size, |size| size.0);
|
||||
.try_global::<BufferFontSize>()
|
||||
.map(|size| size.0)
|
||||
.unwrap_or(self.buffer_font_size);
|
||||
clamp_font_size(font_size)
|
||||
}
|
||||
|
||||
/// Returns the UI font size.
|
||||
pub fn ui_font_size(&self, cx: &App) -> Pixels {
|
||||
let font_size = cx
|
||||
.try_global::<AdjustedUiFontSize>()
|
||||
.map_or(self.ui_font_size, |size| size.0);
|
||||
.try_global::<UiFontSize>()
|
||||
.map(|size| size.0)
|
||||
.unwrap_or(self.ui_font_size);
|
||||
clamp_font_size(font_size)
|
||||
}
|
||||
|
||||
@@ -663,51 +661,38 @@ pub fn observe_buffer_font_size_adjustment<V: 'static>(
|
||||
cx: &mut Context<V>,
|
||||
f: impl 'static + Fn(&mut V, &mut Context<V>),
|
||||
) -> Subscription {
|
||||
cx.observe_global::<AdjustedBufferFontSize>(f)
|
||||
cx.observe_global::<BufferFontSize>(f)
|
||||
}
|
||||
|
||||
/// Sets the adjusted buffer font size.
|
||||
/// Gets the font size, adjusted by the difference between the current buffer font size and the one set in the settings.
|
||||
pub fn adjusted_font_size(size: Pixels, cx: &App) -> Pixels {
|
||||
let adjusted_font_size = if let Some(AdjustedBufferFontSize(adjusted_size)) =
|
||||
cx.try_global::<AdjustedBufferFontSize>()
|
||||
{
|
||||
let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size;
|
||||
let delta = *adjusted_size - buffer_font_size;
|
||||
size + delta
|
||||
} else {
|
||||
size
|
||||
};
|
||||
let adjusted_font_size =
|
||||
if let Some(BufferFontSize(adjusted_size)) = cx.try_global::<BufferFontSize>() {
|
||||
let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size;
|
||||
let delta = *adjusted_size - buffer_font_size;
|
||||
size + delta
|
||||
} else {
|
||||
size
|
||||
};
|
||||
clamp_font_size(adjusted_font_size)
|
||||
}
|
||||
|
||||
/// Returns the adjusted buffer font size.
|
||||
pub fn get_buffer_font_size(cx: &App) -> Pixels {
|
||||
let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size;
|
||||
cx.try_global::<AdjustedBufferFontSize>()
|
||||
.map_or(buffer_font_size, |adjusted_size| adjusted_size.0)
|
||||
}
|
||||
|
||||
/// Adjusts the buffer font size.
|
||||
pub fn adjust_buffer_font_size(cx: &mut App, mut f: impl FnMut(&mut Pixels)) {
|
||||
let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size;
|
||||
let mut adjusted_size = cx
|
||||
.try_global::<AdjustedBufferFontSize>()
|
||||
.try_global::<BufferFontSize>()
|
||||
.map_or(buffer_font_size, |adjusted_size| adjusted_size.0);
|
||||
|
||||
f(&mut adjusted_size);
|
||||
cx.set_global(AdjustedBufferFontSize(clamp_font_size(adjusted_size)));
|
||||
cx.set_global(BufferFontSize(clamp_font_size(adjusted_size)));
|
||||
cx.refresh_windows();
|
||||
}
|
||||
|
||||
/// Returns whether the buffer font size has been adjusted.
|
||||
pub fn has_adjusted_buffer_font_size(cx: &App) -> bool {
|
||||
cx.has_global::<AdjustedBufferFontSize>()
|
||||
}
|
||||
|
||||
/// Resets the buffer font size to the default value.
|
||||
pub fn reset_buffer_font_size(cx: &mut App) {
|
||||
if cx.has_global::<AdjustedBufferFontSize>() {
|
||||
cx.remove_global::<AdjustedBufferFontSize>();
|
||||
if cx.has_global::<BufferFontSize>() {
|
||||
cx.remove_global::<BufferFontSize>();
|
||||
cx.refresh_windows();
|
||||
}
|
||||
}
|
||||
@@ -718,41 +703,29 @@ pub fn setup_ui_font(window: &mut Window, cx: &mut App) -> gpui::Font {
|
||||
let (ui_font, ui_font_size) = {
|
||||
let theme_settings = ThemeSettings::get_global(cx);
|
||||
let font = theme_settings.ui_font.clone();
|
||||
(font, get_ui_font_size(cx))
|
||||
(font, theme_settings.ui_font_size(cx))
|
||||
};
|
||||
|
||||
window.set_rem_size(ui_font_size);
|
||||
ui_font
|
||||
}
|
||||
|
||||
/// Gets the adjusted UI font size.
|
||||
pub fn get_ui_font_size(cx: &App) -> Pixels {
|
||||
let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx);
|
||||
cx.try_global::<AdjustedUiFontSize>()
|
||||
.map_or(ui_font_size, |adjusted_size| adjusted_size.0)
|
||||
}
|
||||
|
||||
/// Sets the adjusted UI font size.
|
||||
pub fn adjust_ui_font_size(cx: &mut App, mut f: impl FnMut(&mut Pixels)) {
|
||||
let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx);
|
||||
let mut adjusted_size = cx
|
||||
.try_global::<AdjustedUiFontSize>()
|
||||
.try_global::<UiFontSize>()
|
||||
.map_or(ui_font_size, |adjusted_size| adjusted_size.0);
|
||||
|
||||
f(&mut adjusted_size);
|
||||
cx.set_global(AdjustedUiFontSize(clamp_font_size(adjusted_size)));
|
||||
cx.set_global(UiFontSize(clamp_font_size(adjusted_size)));
|
||||
cx.refresh_windows();
|
||||
}
|
||||
|
||||
/// Returns whether the UI font size has been adjusted.
|
||||
pub fn has_adjusted_ui_font_size(cx: &App) -> bool {
|
||||
cx.has_global::<AdjustedUiFontSize>()
|
||||
}
|
||||
|
||||
/// Resets the UI font size to the default value.
|
||||
pub fn reset_ui_font_size(cx: &mut App) {
|
||||
if cx.has_global::<AdjustedUiFontSize>() {
|
||||
cx.remove_global::<AdjustedUiFontSize>();
|
||||
if cx.has_global::<UiFontSize>() {
|
||||
cx.remove_global::<UiFontSize>();
|
||||
cx.refresh_windows();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use ::settings::Settings;
|
||||
use ::settings::SettingsStore;
|
||||
use anyhow::Result;
|
||||
use fallback_themes::apply_status_color_defaults;
|
||||
use fs::Fs;
|
||||
@@ -102,16 +101,6 @@ pub fn init(themes_to_load: LoadThemes, cx: &mut App) {
|
||||
|
||||
ThemeSettings::register(cx);
|
||||
FontFamilyCache::init_global(cx);
|
||||
|
||||
let mut prev_buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size(cx);
|
||||
cx.observe_global::<SettingsStore>(move |cx| {
|
||||
let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size(cx);
|
||||
if buffer_font_size != prev_buffer_font_size {
|
||||
prev_buffer_font_size = buffer_font_size;
|
||||
reset_buffer_font_size(cx);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
/// Implementing this trait allows accessing the active theme.
|
||||
@@ -341,14 +330,6 @@ impl Theme {
|
||||
}
|
||||
}
|
||||
|
||||
/// Compounds a color with an alpha value.
|
||||
/// TODO: Replace this with a method on Hsla.
|
||||
pub fn color_alpha(color: Hsla, alpha: f32) -> Hsla {
|
||||
let mut color = color;
|
||||
color.a = alpha;
|
||||
color
|
||||
}
|
||||
|
||||
/// Asynchronously reads the user theme from the specified path.
|
||||
pub async fn read_user_theme(theme_path: &Path, fs: Arc<dyn Fs>) -> Result<ThemeFamilyContent> {
|
||||
let reader = fs.open_sync(theme_path).await?;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use crate::KeyBinding;
|
||||
use crate::{h_flex, prelude::*};
|
||||
use crate::{ElevationIndex, KeyBinding};
|
||||
use gpui::{point, AnyElement, App, BoxShadow, IntoElement, Window};
|
||||
use gpui::{point, AnyElement, App, BoxShadow, FontStyle, Hsla, IntoElement, Window};
|
||||
use smallvec::smallvec;
|
||||
use theme::Appearance;
|
||||
|
||||
/// Represents a hint for a keybinding, optionally with a prefix and suffix.
|
||||
///
|
||||
@@ -23,7 +24,7 @@ pub struct KeybindingHint {
|
||||
suffix: Option<SharedString>,
|
||||
keybinding: KeyBinding,
|
||||
size: Option<Pixels>,
|
||||
elevation: Option<ElevationIndex>,
|
||||
background_color: Hsla,
|
||||
}
|
||||
|
||||
impl KeybindingHint {
|
||||
@@ -37,15 +38,15 @@ impl KeybindingHint {
|
||||
/// ```
|
||||
/// use ui::prelude::*;
|
||||
///
|
||||
/// let hint = KeybindingHint::new(KeyBinding::from_str("Ctrl+C"));
|
||||
/// let hint = KeybindingHint::new(KeyBinding::from_str("Ctrl+C"), Hsla::new(0.0, 0.0, 0.0, 1.0));
|
||||
/// ```
|
||||
pub fn new(keybinding: KeyBinding) -> Self {
|
||||
pub fn new(keybinding: KeyBinding, background_color: Hsla) -> Self {
|
||||
Self {
|
||||
prefix: None,
|
||||
suffix: None,
|
||||
keybinding,
|
||||
size: None,
|
||||
elevation: None,
|
||||
background_color,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,15 +60,19 @@ impl KeybindingHint {
|
||||
/// ```
|
||||
/// use ui::prelude::*;
|
||||
///
|
||||
/// let hint = KeybindingHint::with_prefix("Copy:", KeyBinding::from_str("Ctrl+C"));
|
||||
/// let hint = KeybindingHint::with_prefix("Copy:", KeyBinding::from_str("Ctrl+C"), Hsla::new(0.0, 0.0, 0.0, 1.0));
|
||||
/// ```
|
||||
pub fn with_prefix(prefix: impl Into<SharedString>, keybinding: KeyBinding) -> Self {
|
||||
pub fn with_prefix(
|
||||
prefix: impl Into<SharedString>,
|
||||
keybinding: KeyBinding,
|
||||
background_color: Hsla,
|
||||
) -> Self {
|
||||
Self {
|
||||
prefix: Some(prefix.into()),
|
||||
suffix: None,
|
||||
keybinding,
|
||||
size: None,
|
||||
elevation: None,
|
||||
background_color,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,15 +86,19 @@ impl KeybindingHint {
|
||||
/// ```
|
||||
/// use ui::prelude::*;
|
||||
///
|
||||
/// let hint = KeybindingHint::with_suffix(KeyBinding::from_str("Ctrl+V"), "Paste");
|
||||
/// let hint = KeybindingHint::with_suffix(KeyBinding::from_str("Ctrl+V"), "Paste", Hsla::new(0.0, 0.0, 0.0, 1.0));
|
||||
/// ```
|
||||
pub fn with_suffix(keybinding: KeyBinding, suffix: impl Into<SharedString>) -> Self {
|
||||
pub fn with_suffix(
|
||||
keybinding: KeyBinding,
|
||||
suffix: impl Into<SharedString>,
|
||||
background_color: Hsla,
|
||||
) -> Self {
|
||||
Self {
|
||||
prefix: None,
|
||||
suffix: Some(suffix.into()),
|
||||
keybinding,
|
||||
size: None,
|
||||
elevation: None,
|
||||
background_color,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,46 +152,37 @@ impl KeybindingHint {
|
||||
self.size = size.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the elevation of the keybinding hint.
|
||||
///
|
||||
/// This method allows specifying the elevation index for the keybinding hint,
|
||||
/// which affects its visual appearance in terms of depth or layering.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ui::prelude::*;
|
||||
///
|
||||
/// let hint = KeybindingHint::new(KeyBinding::from_str("Ctrl+A"))
|
||||
/// .elevation(ElevationIndex::new(1));
|
||||
/// ```
|
||||
pub fn elevation(mut self, elevation: impl Into<Option<ElevationIndex>>) -> Self {
|
||||
self.elevation = elevation.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for KeybindingHint {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let colors = cx.theme().colors().clone();
|
||||
let is_light = cx.theme().appearance() == Appearance::Light;
|
||||
|
||||
let border_color =
|
||||
self.background_color
|
||||
.blend(colors.text.alpha(if is_light { 0.08 } else { 0.16 }));
|
||||
let bg_color =
|
||||
self.background_color
|
||||
.blend(colors.text.alpha(if is_light { 0.06 } else { 0.12 }));
|
||||
let shadow_color = colors.text.alpha(if is_light { 0.04 } else { 0.08 });
|
||||
|
||||
let size = self
|
||||
.size
|
||||
.unwrap_or(TextSize::Small.rems(cx).to_pixels(window.rem_size()));
|
||||
let kb_size = size - px(2.0);
|
||||
let kb_bg = if let Some(elevation) = self.elevation {
|
||||
elevation.on_elevation_bg(cx)
|
||||
} else {
|
||||
theme::color_alpha(colors.element_background, 0.6)
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.items_center()
|
||||
let mut base = h_flex();
|
||||
|
||||
base.text_style()
|
||||
.get_or_insert_with(Default::default)
|
||||
.font_style = Some(FontStyle::Italic);
|
||||
|
||||
base.items_center()
|
||||
.gap_0p5()
|
||||
.font_buffer(cx)
|
||||
.text_size(size)
|
||||
.text_color(colors.text_muted)
|
||||
.text_color(colors.text_disabled)
|
||||
.children(self.prefix)
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -191,10 +191,10 @@ impl RenderOnce for KeybindingHint {
|
||||
.px_0p5()
|
||||
.mr_0p5()
|
||||
.border_1()
|
||||
.border_color(kb_bg)
|
||||
.bg(kb_bg.opacity(0.8))
|
||||
.border_color(border_color)
|
||||
.bg(bg_color)
|
||||
.shadow(smallvec![BoxShadow {
|
||||
color: cx.theme().colors().editor_background.opacity(0.8),
|
||||
color: shadow_color,
|
||||
offset: point(px(0.), px(1.)),
|
||||
blur_radius: px(0.),
|
||||
spread_radius: px(0.),
|
||||
@@ -212,6 +212,8 @@ impl ComponentPreview for KeybindingHint {
|
||||
let enter = KeyBinding::for_action(&menu::Confirm, window, cx)
|
||||
.unwrap_or(KeyBinding::new(enter_fallback, cx));
|
||||
|
||||
let bg_color = cx.theme().colors().surface_background;
|
||||
|
||||
v_flex()
|
||||
.gap_6()
|
||||
.children(vec![
|
||||
@@ -220,17 +222,17 @@ impl ComponentPreview for KeybindingHint {
|
||||
vec![
|
||||
single_example(
|
||||
"With Prefix",
|
||||
KeybindingHint::with_prefix("Go to Start:", enter.clone())
|
||||
KeybindingHint::with_prefix("Go to Start:", enter.clone(), bg_color)
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"With Suffix",
|
||||
KeybindingHint::with_suffix(enter.clone(), "Go to End")
|
||||
KeybindingHint::with_suffix(enter.clone(), "Go to End", bg_color)
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"With Prefix and Suffix",
|
||||
KeybindingHint::new(enter.clone())
|
||||
KeybindingHint::new(enter.clone(), bg_color)
|
||||
.prefix("Confirm:")
|
||||
.suffix("Execute selected action")
|
||||
.into_any_element(),
|
||||
@@ -242,21 +244,21 @@ impl ComponentPreview for KeybindingHint {
|
||||
vec![
|
||||
single_example(
|
||||
"Small",
|
||||
KeybindingHint::new(enter.clone())
|
||||
KeybindingHint::new(enter.clone(), bg_color)
|
||||
.size(Pixels::from(12.0))
|
||||
.prefix("Small:")
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Medium",
|
||||
KeybindingHint::new(enter.clone())
|
||||
KeybindingHint::new(enter.clone(), bg_color)
|
||||
.size(Pixels::from(16.0))
|
||||
.suffix("Medium")
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Large",
|
||||
KeybindingHint::new(enter.clone())
|
||||
KeybindingHint::new(enter.clone(), bg_color)
|
||||
.size(Pixels::from(20.0))
|
||||
.prefix("Large:")
|
||||
.suffix("Size")
|
||||
@@ -264,41 +266,6 @@ impl ComponentPreview for KeybindingHint {
|
||||
),
|
||||
],
|
||||
),
|
||||
example_group_with_title(
|
||||
"Elevations",
|
||||
vec![
|
||||
single_example(
|
||||
"Surface",
|
||||
KeybindingHint::new(enter.clone())
|
||||
.elevation(ElevationIndex::Surface)
|
||||
.prefix("Surface:")
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Elevated Surface",
|
||||
KeybindingHint::new(enter.clone())
|
||||
.elevation(ElevationIndex::ElevatedSurface)
|
||||
.suffix("Elevated")
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Editor Surface",
|
||||
KeybindingHint::new(enter.clone())
|
||||
.elevation(ElevationIndex::EditorSurface)
|
||||
.prefix("Editor:")
|
||||
.suffix("Surface")
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Modal Surface",
|
||||
KeybindingHint::new(enter.clone())
|
||||
.elevation(ElevationIndex::ModalSurface)
|
||||
.prefix("Modal:")
|
||||
.suffix("Enter")
|
||||
.into_any_element(),
|
||||
),
|
||||
],
|
||||
),
|
||||
])
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::fmt::{self, Display, Formatter};
|
||||
|
||||
use gpui::{hsla, point, px, App, BoxShadow, Hsla};
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use theme::ActiveTheme;
|
||||
use theme::{ActiveTheme, Appearance};
|
||||
|
||||
/// Today, elevation is primarily used to add shadows to elements, and set the correct background for elements like buttons.
|
||||
///
|
||||
@@ -40,19 +40,14 @@ impl Display for ElevationIndex {
|
||||
|
||||
impl ElevationIndex {
|
||||
/// Returns an appropriate shadow for the given elevation index.
|
||||
pub fn shadow(self) -> SmallVec<[BoxShadow; 2]> {
|
||||
pub fn shadow(self, cx: &App) -> SmallVec<[BoxShadow; 2]> {
|
||||
let is_light = cx.theme().appearance() == Appearance::Light;
|
||||
|
||||
match self {
|
||||
ElevationIndex::Surface => smallvec![],
|
||||
ElevationIndex::EditorSurface => smallvec![],
|
||||
|
||||
ElevationIndex::ElevatedSurface => smallvec![BoxShadow {
|
||||
color: hsla(0., 0., 0., 0.12),
|
||||
offset: point(px(0.), px(2.)),
|
||||
blur_radius: px(3.),
|
||||
spread_radius: px(0.),
|
||||
}],
|
||||
|
||||
ElevationIndex::ModalSurface => smallvec![
|
||||
ElevationIndex::ElevatedSurface => smallvec![
|
||||
BoxShadow {
|
||||
color: hsla(0., 0., 0., 0.12),
|
||||
offset: point(px(0.), px(2.)),
|
||||
@@ -60,7 +55,22 @@ impl ElevationIndex {
|
||||
spread_radius: px(0.),
|
||||
},
|
||||
BoxShadow {
|
||||
color: hsla(0., 0., 0., 0.08),
|
||||
color: hsla(0., 0., 0., if is_light { 0.03 } else { 0.06 }),
|
||||
offset: point(px(1.), px(1.)),
|
||||
blur_radius: px(0.),
|
||||
spread_radius: px(0.),
|
||||
}
|
||||
],
|
||||
|
||||
ElevationIndex::ModalSurface => smallvec![
|
||||
BoxShadow {
|
||||
color: hsla(0., 0., 0., if is_light { 0.06 } else { 0.12 }),
|
||||
offset: point(px(0.), px(2.)),
|
||||
blur_radius: px(3.),
|
||||
spread_radius: px(0.),
|
||||
},
|
||||
BoxShadow {
|
||||
color: hsla(0., 0., 0., if is_light { 0.06 } else { 0.08 }),
|
||||
offset: point(px(0.), px(3.)),
|
||||
blur_radius: px(6.),
|
||||
spread_radius: px(0.),
|
||||
@@ -71,6 +81,12 @@ impl ElevationIndex {
|
||||
blur_radius: px(12.),
|
||||
spread_radius: px(0.),
|
||||
},
|
||||
BoxShadow {
|
||||
color: hsla(0., 0., 0., if is_light { 0.04 } else { 0.12 }),
|
||||
offset: point(px(1.), px(1.)),
|
||||
blur_radius: px(0.),
|
||||
spread_radius: px(0.),
|
||||
},
|
||||
],
|
||||
|
||||
_ => smallvec![],
|
||||
|
||||
@@ -8,13 +8,13 @@ fn elevated<E: Styled>(this: E, cx: &App, index: ElevationIndex) -> E {
|
||||
.rounded_lg()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.shadow(index.shadow())
|
||||
.shadow(index.shadow(cx))
|
||||
}
|
||||
|
||||
fn elevated_borderless<E: Styled>(this: E, cx: &mut App, index: ElevationIndex) -> E {
|
||||
this.bg(cx.theme().colors().elevated_surface_background)
|
||||
.rounded_lg()
|
||||
.shadow(index.shadow())
|
||||
.shadow(index.shadow(cx))
|
||||
}
|
||||
|
||||
/// Extends [`gpui::Styled`] with Zed-specific styling methods.
|
||||
|
||||
@@ -155,7 +155,18 @@ impl Vim {
|
||||
original_start_columns.extend(original_start_column);
|
||||
}
|
||||
|
||||
editor.edit_with_block_indent(edits, original_start_columns, cx);
|
||||
let cursor_offset = editor.selections.last::<usize>(cx).head();
|
||||
if editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.snapshot(cx)
|
||||
.settings_at(cursor_offset, cx)
|
||||
.auto_indent_on_paste
|
||||
{
|
||||
editor.edit_with_block_indent(edits, original_start_columns, cx);
|
||||
} else {
|
||||
editor.edit(edits, cx);
|
||||
}
|
||||
|
||||
// in line_mode vim will insert the new text on the next (or previous if before) line
|
||||
// and put the cursor on the first non-blank character of the first inserted line (or at the end if the first line is blank).
|
||||
@@ -278,6 +289,10 @@ mod test {
|
||||
};
|
||||
use gpui::ClipboardItem;
|
||||
use indoc::indoc;
|
||||
use language::{
|
||||
language_settings::{AllLanguageSettings, LanguageSettingsContent},
|
||||
LanguageName,
|
||||
};
|
||||
use settings::SettingsStore;
|
||||
|
||||
#[gpui::test]
|
||||
@@ -614,6 +629,67 @@ mod test {
|
||||
class A {
|
||||
a(){}
|
||||
}
|
||||
"},
|
||||
Mode::Normal,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_paste_auto_indent(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
mod some_module {
|
||||
ˇfn main() {
|
||||
}
|
||||
}
|
||||
"},
|
||||
Mode::Normal,
|
||||
);
|
||||
// default auto indentation
|
||||
cx.simulate_keystrokes("y y p");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
mod some_module {
|
||||
fn main() {
|
||||
ˇfn main() {
|
||||
}
|
||||
}
|
||||
"},
|
||||
Mode::Normal,
|
||||
);
|
||||
// back to previous state
|
||||
cx.simulate_keystrokes("u u");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
mod some_module {
|
||||
ˇfn main() {
|
||||
}
|
||||
}
|
||||
"},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.update_global(|store: &mut SettingsStore, cx| {
|
||||
store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
|
||||
settings.languages.insert(
|
||||
LanguageName::new("Rust"),
|
||||
LanguageSettingsContent {
|
||||
auto_indent_on_paste: Some(false),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
// auto indentation turned off
|
||||
cx.simulate_keystrokes("y y p");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
mod some_module {
|
||||
fn main() {
|
||||
ˇfn main() {
|
||||
}
|
||||
}
|
||||
"},
|
||||
Mode::Normal,
|
||||
);
|
||||
|
||||
@@ -2265,13 +2265,13 @@ mod test {
|
||||
(
|
||||
"c i q",
|
||||
"This is a \"simple 'qˇuote'\" example.",
|
||||
"This is a \"ˇ\" example.", // Not supported by tree sitter queries for now
|
||||
"This is a \"ˇ\" example.", // Not supported by Tree-sitter queries for now
|
||||
Mode::Insert,
|
||||
),
|
||||
(
|
||||
"c a q",
|
||||
"This is a \"simple 'qˇuote'\" example.",
|
||||
"This is a ˇ example.", // Not supported by tree sitter queries for now
|
||||
"This is a ˇ example.", // Not supported by Tree-sitter queries for now
|
||||
Mode::Insert,
|
||||
),
|
||||
(
|
||||
|
||||
@@ -20,7 +20,7 @@ use futures::{
|
||||
};
|
||||
use fuzzy::CharBag;
|
||||
use git::{
|
||||
repository::{Branch, GitRepository, RepoPath},
|
||||
repository::{Branch, GitRepository, RepoPath, UpstreamTrackingStatus},
|
||||
status::{
|
||||
FileStatus, GitSummary, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode,
|
||||
},
|
||||
@@ -202,7 +202,7 @@ pub struct RepositoryEntry {
|
||||
pub(crate) statuses_by_path: SumTree<StatusEntry>,
|
||||
work_directory_id: ProjectEntryId,
|
||||
pub work_directory: WorkDirectory,
|
||||
pub(crate) branch: Option<Branch>,
|
||||
pub(crate) current_branch: Option<Branch>,
|
||||
pub current_merge_conflicts: TreeSet<RepoPath>,
|
||||
}
|
||||
|
||||
@@ -216,7 +216,7 @@ impl Deref for RepositoryEntry {
|
||||
|
||||
impl RepositoryEntry {
|
||||
pub fn branch(&self) -> Option<&Branch> {
|
||||
self.branch.as_ref()
|
||||
self.current_branch.as_ref()
|
||||
}
|
||||
|
||||
pub fn work_directory_id(&self) -> ProjectEntryId {
|
||||
@@ -244,8 +244,11 @@ impl RepositoryEntry {
|
||||
pub fn initial_update(&self) -> proto::RepositoryEntry {
|
||||
proto::RepositoryEntry {
|
||||
work_directory_id: self.work_directory_id.to_proto(),
|
||||
branch: self.branch.as_ref().map(|branch| branch.name.to_string()),
|
||||
branch_summary: self.branch.as_ref().map(branch_to_proto),
|
||||
branch: self
|
||||
.current_branch
|
||||
.as_ref()
|
||||
.map(|branch| branch.name.to_string()),
|
||||
branch_summary: self.current_branch.as_ref().map(branch_to_proto),
|
||||
updated_statuses: self
|
||||
.statuses_by_path
|
||||
.iter()
|
||||
@@ -304,8 +307,11 @@ impl RepositoryEntry {
|
||||
|
||||
proto::RepositoryEntry {
|
||||
work_directory_id: self.work_directory_id.to_proto(),
|
||||
branch: self.branch.as_ref().map(|branch| branch.name.to_string()),
|
||||
branch_summary: self.branch.as_ref().map(branch_to_proto),
|
||||
branch: self
|
||||
.current_branch
|
||||
.as_ref()
|
||||
.map(|branch| branch.name.to_string()),
|
||||
branch_summary: self.current_branch.as_ref().map(branch_to_proto),
|
||||
updated_statuses,
|
||||
removed_statuses,
|
||||
current_merge_conflicts: self
|
||||
@@ -329,7 +335,7 @@ pub fn branch_to_proto(branch: &git::repository::Branch) -> proto::Branch {
|
||||
ref_name: upstream.ref_name.to_string(),
|
||||
tracking: upstream
|
||||
.tracking
|
||||
.as_ref()
|
||||
.status()
|
||||
.map(|upstream| proto::UpstreamTracking {
|
||||
ahead: upstream.ahead as u64,
|
||||
behind: upstream.behind as u64,
|
||||
@@ -355,12 +361,16 @@ pub fn proto_to_branch(proto: &proto::Branch) -> git::repository::Branch {
|
||||
.as_ref()
|
||||
.map(|upstream| git::repository::Upstream {
|
||||
ref_name: upstream.ref_name.to_string().into(),
|
||||
tracking: upstream.tracking.as_ref().map(|tracking| {
|
||||
git::repository::UpstreamTracking {
|
||||
ahead: tracking.ahead as u32,
|
||||
behind: tracking.behind as u32,
|
||||
}
|
||||
}),
|
||||
tracking: upstream
|
||||
.tracking
|
||||
.as_ref()
|
||||
.map(|tracking| {
|
||||
git::repository::UpstreamTracking::Tracked(UpstreamTrackingStatus {
|
||||
ahead: tracking.ahead as u32,
|
||||
behind: tracking.behind as u32,
|
||||
})
|
||||
})
|
||||
.unwrap_or(git::repository::UpstreamTracking::Gone),
|
||||
}),
|
||||
most_recent_commit: proto.most_recent_commit.as_ref().map(|commit| {
|
||||
git::repository::CommitSummary {
|
||||
@@ -2682,7 +2692,8 @@ impl Snapshot {
|
||||
|
||||
self.repositories
|
||||
.update(&PathKey(work_dir_entry.path.clone()), &(), |repo| {
|
||||
repo.branch = repository.branch_summary.as_ref().map(proto_to_branch);
|
||||
repo.current_branch =
|
||||
repository.branch_summary.as_ref().map(proto_to_branch);
|
||||
repo.statuses_by_path.edit(edits, &());
|
||||
repo.current_merge_conflicts = conflicted_paths
|
||||
});
|
||||
@@ -2704,7 +2715,7 @@ impl Snapshot {
|
||||
work_directory: WorkDirectory::InProject {
|
||||
relative_path: work_dir_entry.path.clone(),
|
||||
},
|
||||
branch: repository.branch_summary.as_ref().map(proto_to_branch),
|
||||
current_branch: repository.branch_summary.as_ref().map(proto_to_branch),
|
||||
statuses_by_path: statuses,
|
||||
current_merge_conflicts: conflicted_paths,
|
||||
},
|
||||
@@ -3506,7 +3517,7 @@ impl BackgroundScannerState {
|
||||
RepositoryEntry {
|
||||
work_directory_id: work_dir_id,
|
||||
work_directory: work_directory.clone(),
|
||||
branch: None,
|
||||
current_branch: None,
|
||||
statuses_by_path: Default::default(),
|
||||
current_merge_conflicts: Default::default(),
|
||||
},
|
||||
@@ -5472,6 +5483,9 @@ impl BackgroundScanner {
|
||||
},
|
||||
&(),
|
||||
);
|
||||
if status.is_conflicted() {
|
||||
repository.current_merge_conflicts.insert(repo_path.clone());
|
||||
}
|
||||
|
||||
if let Some(path) = project_path {
|
||||
changed_paths.push(path);
|
||||
@@ -5577,7 +5591,7 @@ fn update_branches(
|
||||
let mut repository = snapshot
|
||||
.repository(repository.work_directory.path_key())
|
||||
.context("Missing repository")?;
|
||||
repository.branch = branches.into_iter().find(|branch| branch.is_head);
|
||||
repository.current_branch = branches.into_iter().find(|branch| branch.is_head);
|
||||
|
||||
let mut state = state.lock();
|
||||
state
|
||||
|
||||
@@ -563,7 +563,7 @@ fn register_actions(
|
||||
move |_, action: &zed_actions::IncreaseUiFontSize, _window, cx| {
|
||||
if action.persist {
|
||||
update_settings_file::<ThemeSettings>(fs.clone(), cx, move |settings, cx| {
|
||||
let ui_font_size = theme::get_ui_font_size(cx) + px(1.0);
|
||||
let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx) + px(1.0);
|
||||
let _ = settings
|
||||
.ui_font_size
|
||||
.insert(theme::clamp_font_size(ui_font_size).0);
|
||||
@@ -580,7 +580,7 @@ fn register_actions(
|
||||
move |_, action: &zed_actions::DecreaseUiFontSize, _window, cx| {
|
||||
if action.persist {
|
||||
update_settings_file::<ThemeSettings>(fs.clone(), cx, move |settings, cx| {
|
||||
let ui_font_size = theme::get_ui_font_size(cx) - px(1.0);
|
||||
let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx) - px(1.0);
|
||||
let _ = settings
|
||||
.ui_font_size
|
||||
.insert(theme::clamp_font_size(ui_font_size).0);
|
||||
@@ -609,7 +609,8 @@ fn register_actions(
|
||||
move |_, action: &zed_actions::IncreaseBufferFontSize, _window, cx| {
|
||||
if action.persist {
|
||||
update_settings_file::<ThemeSettings>(fs.clone(), cx, move |settings, cx| {
|
||||
let buffer_font_size = theme::get_buffer_font_size(cx) + px(1.0);
|
||||
let buffer_font_size =
|
||||
ThemeSettings::get_global(cx).buffer_font_size(cx) + px(1.0);
|
||||
let _ = settings
|
||||
.buffer_font_size
|
||||
.insert(theme::clamp_font_size(buffer_font_size).0);
|
||||
@@ -626,7 +627,8 @@ fn register_actions(
|
||||
move |_, action: &zed_actions::DecreaseBufferFontSize, _window, cx| {
|
||||
if action.persist {
|
||||
update_settings_file::<ThemeSettings>(fs.clone(), cx, move |settings, cx| {
|
||||
let buffer_font_size = theme::get_buffer_font_size(cx) - px(1.0);
|
||||
let buffer_font_size =
|
||||
ThemeSettings::get_global(cx).buffer_font_size(cx) - px(1.0);
|
||||
let _ = settings
|
||||
.buffer_font_size
|
||||
.insert(theme::clamp_font_size(buffer_font_size).0);
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
use regex::Regex;
|
||||
|
||||
/// The most common license locations, with US and UK English spelling.
|
||||
pub const LICENSE_FILES_TO_CHECK: &[&str] = &["LICENSE", "LICENCE", "LICENSE.txt", "LICENCE.txt"];
|
||||
pub const LICENSE_FILES_TO_CHECK: &[&str] = &[
|
||||
"LICENSE",
|
||||
"LICENCE",
|
||||
"LICENSE.txt",
|
||||
"LICENCE.txt",
|
||||
"LICENSE.md",
|
||||
"LICENCE.md",
|
||||
];
|
||||
|
||||
pub fn is_license_eligible_for_data_collection(license: &str) -> bool {
|
||||
// TODO: Include more licenses later (namely, Apache)
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
AsciiDoc language support in Zed is provided by the community-maintained [AsciiDoc extension](https://github.com/andreicek/zed-asciidoc).
|
||||
Report issues to: [https://github.com/andreicek/zed-asciidoc/issues](https://github.com/andreicek/zed-asciidoc/issues)
|
||||
|
||||
- Tree Sitter: [cathaysia/tree-sitter-asciidoc](https://github.com/cathaysia/tree-sitter-asciidoc)
|
||||
- Tree-sitter: [cathaysia/tree-sitter-asciidoc](https://github.com/cathaysia/tree-sitter-asciidoc)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Astro support is available through the [Astro extension](https://github.com/zed-extensions/astro).
|
||||
|
||||
- Tree Sitter: [virchau13/tree-sitter-astro](https://github.com/virchau13/tree-sitter-astro)
|
||||
- Tree-sitter: [virchau13/tree-sitter-astro](https://github.com/virchau13/tree-sitter-astro)
|
||||
- Language Server: [withastro/language-tools](https://github.com/withastro/language-tools)
|
||||
|
||||
<!--
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
Bash language support in Zed is provided by the community-maintained [Basher extension](https://github.com/d1y/bash.zed).
|
||||
Report issues to: [https://github.com/d1y/bash.zed/issues](https://github.com/d1y/bash.zed/issues)
|
||||
|
||||
- Tree Sitter: [tree-sitter/tree-sitter-bash](https://github.com/tree-sitter/tree-sitter-bash)
|
||||
- Tree-sitter: [tree-sitter/tree-sitter-bash](https://github.com/tree-sitter/tree-sitter-bash)
|
||||
- Language Server: [bash-lsp/bash-language-server](https://github.com/bash-lsp/bash-language-server)
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
C support is available natively in Zed.
|
||||
|
||||
- Tree Sitter: [tree-sitter/tree-sitter-c](https://github.com/tree-sitter/tree-sitter-c)
|
||||
- Tree-sitter: [tree-sitter/tree-sitter-c](https://github.com/tree-sitter/tree-sitter-c)
|
||||
- Language Server: [clangd/clangd](https://github.com/clangd/clangd)
|
||||
|
||||
## Clangd: Force detect as C
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Clojure support is available through the [Clojure extension](https://github.com/zed-extensions/clojure).
|
||||
|
||||
- Tree Sitter: [prcastro/tree-sitter-clojure](https://github.com/prcastro/tree-sitter-clojure)
|
||||
- Tree-sitter: [prcastro/tree-sitter-clojure](https://github.com/prcastro/tree-sitter-clojure)
|
||||
- Language Server: [clojure-lsp/clojure-lsp](https://github.com/clojure-lsp/clojure-lsp)
|
||||
|
||||
<!--
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
C++ support is available natively in Zed.
|
||||
|
||||
- Tree Sitter: [tree-sitter/tree-sitter-cpp](https://github.com/tree-sitter/tree-sitter-cpp)
|
||||
- Tree-sitter: [tree-sitter/tree-sitter-cpp](https://github.com/tree-sitter/tree-sitter-cpp)
|
||||
- Language Server: [clangd/clangd](https://github.com/clangd/clangd)
|
||||
|
||||
## Binary
|
||||
|
||||
@@ -4,7 +4,7 @@ Note language name is "CSharp" for settings not "C#'
|
||||
|
||||
C# support is available through the [C# extension](https://github.com/zed-industries/zed/tree/main/extensions/csharp).
|
||||
|
||||
- Tree Sitter: [tree-sitter/tree-sitter-c-sharp](https://github.com/tree-sitter/tree-sitter-c-sharp)
|
||||
- Tree-sitter: [tree-sitter/tree-sitter-c-sharp](https://github.com/tree-sitter/tree-sitter-c-sharp)
|
||||
- Language Server: [OmniSharp/omnisharp-roslyn](https://github.com/OmniSharp/omnisharp-roslyn)
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
CSS support is available natively in Zed.
|
||||
|
||||
- Tree Sitter: [tree-sitter/tree-sitter-css](https://github.com/tree-sitter/tree-sitter-css)
|
||||
- Tree-sitter: [tree-sitter/tree-sitter-css](https://github.com/tree-sitter/tree-sitter-css)
|
||||
- Language Servers:
|
||||
- [microsoft/vscode-html-languageservice](https://github.com/microsoft/vscode-html-languageservice)
|
||||
- [tailwindcss-language-server](https://github.com/tailwindlabs/tailwindcss-intellisense)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Dart support is available through the [Dart extension](https://github.com/zed-extensions/dart).
|
||||
|
||||
- Tree Sitter: [UserNobody14/tree-sitter-dart](https://github.com/UserNobody14/tree-sitter-dart)
|
||||
- Tree-sitter: [UserNobody14/tree-sitter-dart](https://github.com/UserNobody14/tree-sitter-dart)
|
||||
- Language Server: [dart language-server](https://github.com/dart-lang/sdk)
|
||||
|
||||
## Pre-requisites
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user