Compare commits
1 Commits
keymap_edi
...
batch-assi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c570f95736 |
49
.github/workflows/release_nightly.yml
vendored
49
.github/workflows/release_nightly.yml
vendored
@@ -170,6 +170,55 @@ jobs:
|
||||
- name: Upload Zed Nightly
|
||||
run: script/upload-nightly linux-targz
|
||||
|
||||
bundle-nix:
|
||||
timeout-minutes: 60
|
||||
name: (${{ matrix.system.os }}) Nix Build
|
||||
continue-on-error: true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
system:
|
||||
- os: x86 Linux
|
||||
runner: buildjet-16vcpu-ubuntu-2204
|
||||
install_nix: true
|
||||
- os: arm Mac
|
||||
runner: [macOS, ARM64, test]
|
||||
install_nix: false
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: ${{ matrix.system.runner }}
|
||||
needs: tests
|
||||
env:
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
||||
GIT_LFS_SKIP_SMUDGE: 1 # breaks the livekit rust sdk examples which we don't actually depend on
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
# on our macs we manually install nix. for some reason the cachix action is running
|
||||
# under a non-login /bin/bash shell which doesn't source the proper script to add the
|
||||
# nix profile to PATH, so we manually add them here
|
||||
- name: Set path
|
||||
if: ${{ ! matrix.system.install_nix }}
|
||||
run: |
|
||||
echo "/nix/var/nix/profiles/default/bin" >> $GITHUB_PATH
|
||||
echo "/Users/administrator/.nix-profile/bin" >> $GITHUB_PATH
|
||||
|
||||
- uses: cachix/install-nix-action@d1ca217b388ee87b2507a9a93bf01368bde7cec2 # v31
|
||||
if: ${{ matrix.system.install_nix }}
|
||||
with:
|
||||
github_access_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
with:
|
||||
name: zed-industries
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
- run: nix build
|
||||
- name: Limit /nix/store to 50GB
|
||||
run: '[ $(du -sm /nix/store | cut -f1) -gt 50000 ] && nix-collect-garbage -d'
|
||||
|
||||
update-nightly-tag:
|
||||
name: Update nightly tag
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
|
||||
12
Cargo.lock
generated
12
Cargo.lock
generated
@@ -94,7 +94,6 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"paths",
|
||||
"picker",
|
||||
"postage",
|
||||
"project",
|
||||
"prompt_store",
|
||||
"proto",
|
||||
@@ -730,9 +729,6 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"task",
|
||||
"terminal",
|
||||
"terminal_view",
|
||||
"tree-sitter-rust",
|
||||
"ui",
|
||||
"unindent",
|
||||
@@ -4993,7 +4989,6 @@ dependencies = [
|
||||
"language_model",
|
||||
"language_models",
|
||||
"languages",
|
||||
"markdown",
|
||||
"node_runtime",
|
||||
"pathdiff",
|
||||
"paths",
|
||||
@@ -10108,7 +10103,7 @@ name = "perplexity"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"zed_extension_api 0.5.0",
|
||||
"zed_extension_api 0.4.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -11119,7 +11114,6 @@ dependencies = [
|
||||
"paths",
|
||||
"rope",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"text",
|
||||
"util",
|
||||
"uuid",
|
||||
@@ -18632,7 +18626,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_extension_api"
|
||||
version = "0.5.0"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -18692,7 +18686,7 @@ dependencies = [
|
||||
name = "zed_test_extension"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.5.0",
|
||||
"zed_extension_api 0.4.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 12H16" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4 6H20" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4 18H12" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 402 B |
@@ -1,14 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2489_484)">
|
||||
<path d="M11 8.9V11C8.51716 11 7.48284 11 5 11V10.4L11 5.6V5H5V7.1" stroke="black" stroke-width="1.5"/>
|
||||
<path d="M1.5 5.5V1.5H5" stroke="black" stroke-opacity="0.5" stroke-width="1.5"/>
|
||||
<path d="M14.5 5.5V1.5H11" stroke="black" stroke-opacity="0.5" stroke-width="1.5"/>
|
||||
<path d="M1.5 10.5V14.5H5" stroke="black" stroke-opacity="0.5" stroke-width="1.5"/>
|
||||
<path d="M14.5 10.5V14.5H11" stroke="black" stroke-opacity="0.5" stroke-width="1.5"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2489_484">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 687 B |
@@ -60,6 +60,8 @@
|
||||
"shift-tab": "editor::Backtab",
|
||||
"ctrl-k": "editor::CutToEndOfLine",
|
||||
// "ctrl-t": "editor::Transpose",
|
||||
"ctrl-k ctrl-q": "editor::Rewrap",
|
||||
"ctrl-k q": "editor::Rewrap",
|
||||
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd",
|
||||
"cut": "editor::Cut",
|
||||
@@ -104,7 +106,28 @@
|
||||
"ctrl-shift-end": "editor::SelectToEnd",
|
||||
"ctrl-a": "editor::SelectAll",
|
||||
"ctrl-l": "editor::SelectLine",
|
||||
"ctrl-alt-space": "editor::ShowCharacterPalette"
|
||||
"ctrl-shift-i": "editor::Format",
|
||||
"alt-shift-o": "editor::OrganizeImports",
|
||||
// "cmd-shift-left": ["editor::SelectToBeginningOfLine", {"stop_at_soft_wraps": true, "stop_at_indent": true }],
|
||||
// "ctrl-shift-a": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
|
||||
"shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
|
||||
// "cmd-shift-right": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
|
||||
// "ctrl-shift-e": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
|
||||
"shift-end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
|
||||
// "alt-v": ["editor::MovePageUp", { "center_cursor": true }],
|
||||
"ctrl-alt-space": "editor::ShowCharacterPalette",
|
||||
"ctrl-;": "editor::ToggleLineNumbers",
|
||||
"ctrl-'": "editor::ToggleSelectedDiffHunks",
|
||||
"ctrl-\"": "editor::ExpandAllDiffHunks",
|
||||
"ctrl-i": "editor::ShowSignatureHelp",
|
||||
"alt-g b": "editor::ToggleGitBlame",
|
||||
"menu": "editor::OpenContextMenu",
|
||||
"shift-f10": "editor::OpenContextMenu",
|
||||
"ctrl-shift-e": "editor::ToggleEditPrediction",
|
||||
"f9": "editor::ToggleBreakpoint",
|
||||
"shift-f9": "editor::EditLogBreakpoint",
|
||||
"ctrl-shift-backspace": "editor::GoToPreviousChange",
|
||||
"ctrl-shift-alt-backspace": "editor::GoToNextChange"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -123,30 +146,7 @@
|
||||
"ctrl->": "assistant::QuoteSelection",
|
||||
"ctrl-<": "assistant::InsertIntoEditor",
|
||||
"ctrl-alt-e": "editor::SelectEnclosingSymbol",
|
||||
"alt-enter": "editor::OpenSelectionsInMultibuffer",
|
||||
"ctrl-k ctrl-q": "editor::Rewrap",
|
||||
"ctrl-k q": "editor::Rewrap",
|
||||
"ctrl-shift-i": "editor::Format",
|
||||
"alt-shift-o": "editor::OrganizeImports",
|
||||
// "cmd-shift-left": ["editor::SelectToBeginningOfLine", {"stop_at_soft_wraps": true, "stop_at_indent": true }],
|
||||
// "ctrl-shift-a": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
|
||||
"shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
|
||||
// "cmd-shift-right": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
|
||||
// "ctrl-shift-e": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
|
||||
"shift-end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
|
||||
// "alt-v": ["editor::MovePageUp", { "center_cursor": true }],
|
||||
"ctrl-;": "editor::ToggleLineNumbers",
|
||||
"ctrl-'": "editor::ToggleSelectedDiffHunks",
|
||||
"ctrl-\"": "editor::ExpandAllDiffHunks",
|
||||
"ctrl-i": "editor::ShowSignatureHelp",
|
||||
"alt-g b": "editor::ToggleGitBlame",
|
||||
"menu": "editor::OpenContextMenu",
|
||||
"shift-f10": "editor::OpenContextMenu",
|
||||
"ctrl-shift-e": "editor::ToggleEditPrediction",
|
||||
"f9": "editor::ToggleBreakpoint",
|
||||
"shift-f9": "editor::EditLogBreakpoint",
|
||||
"ctrl-shift-backspace": "editor::GoToPreviousChange",
|
||||
"ctrl-shift-alt-backspace": "editor::GoToNextChange"
|
||||
"alt-enter": "editor::OpenSelectionsInMultibuffer"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -245,19 +245,11 @@
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"ctrl-alt-/": "assistant::ToggleModelSelector",
|
||||
"ctrl-shift-a": "agent::ToggleContextPicker",
|
||||
"ctrl-shift-o": "agent::ToggleNavigationMenu",
|
||||
"ctrl-shift-i": "agent::ToggleOptionsMenu",
|
||||
"shift-escape": "agent::ExpandMessageEditor",
|
||||
"ctrl-e": "agent::ChatMode",
|
||||
"ctrl-alt-e": "agent::RemoveAllContext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel > NavigationMenu",
|
||||
"bindings": {
|
||||
"shift-backspace": "agent::DeleteRecentlyOpenThread"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel > Markdown",
|
||||
"bindings": {
|
||||
@@ -445,6 +437,8 @@
|
||||
"alt-down": "editor::MoveLineDown",
|
||||
"ctrl-alt-shift-up": "editor::DuplicateLineUp",
|
||||
"ctrl-alt-shift-down": "editor::DuplicateLineDown",
|
||||
"alt-shift-right": "editor::SelectLargerSyntaxNode", // Expand Selection
|
||||
"alt-shift-left": "editor::SelectSmallerSyntaxNode", // Shrink Selection
|
||||
"ctrl-shift-l": "editor::SelectAllMatches", // Select all occurrences of current selection
|
||||
"ctrl-f2": "editor::SelectAllMatches", // Select all occurrences of current word
|
||||
"ctrl-d": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch / find_under_expand
|
||||
@@ -452,20 +446,10 @@
|
||||
"ctrl-shift-up": ["editor::SelectPrevious", { "replace_newest": false }], // editor.action.addSelectionToPreviousFindMatch
|
||||
"ctrl-k ctrl-d": ["editor::SelectNext", { "replace_newest": true }], // editor.action.moveSelectionToNextFindMatch / find_under_expand_skip
|
||||
"ctrl-k ctrl-shift-d": ["editor::SelectPrevious", { "replace_newest": true }], // editor.action.moveSelectionToPreviousFindMatch
|
||||
"ctrl-u": "editor::UndoSelection",
|
||||
"ctrl-shift-u": "editor::RedoSelection",
|
||||
"ctrl-\\": "pane::SplitRight"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
"ctrl-shift-o": "outline::Toggle",
|
||||
"ctrl-g": "go_to_line::Toggle",
|
||||
"alt-shift-right": "editor::SelectLargerSyntaxNode", // Expand Selection
|
||||
"alt-shift-left": "editor::SelectSmallerSyntaxNode", // Shrink Selection
|
||||
"ctrl-k ctrl-i": "editor::Hover",
|
||||
"ctrl-/": ["editor::ToggleComments", { "advance_downwards": false }],
|
||||
"ctrl-u": "editor::UndoSelection",
|
||||
"ctrl-shift-u": "editor::RedoSelection",
|
||||
"f8": "editor::GoToDiagnostic",
|
||||
"shift-f8": "editor::GoToPreviousDiagnostic",
|
||||
"f2": "editor::Rename",
|
||||
@@ -499,6 +483,7 @@
|
||||
"ctrl-.": "editor::ToggleCodeActions",
|
||||
"ctrl-k r": "editor::RevealInFileManager",
|
||||
"ctrl-k p": "editor::CopyPath",
|
||||
"ctrl-\\": "pane::SplitRight",
|
||||
"ctrl-k v": "markdown::OpenPreviewToTheSide",
|
||||
"ctrl-shift-v": "markdown::OpenPreview",
|
||||
"ctrl-alt-shift-c": "editor::DisplayCursorNames",
|
||||
@@ -506,6 +491,13 @@
|
||||
"alt-,": "editor::GoToPreviousHunk"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
"ctrl-shift-o": "outline::Toggle",
|
||||
"ctrl-g": "go_to_line::Toggle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
@@ -528,7 +520,6 @@
|
||||
"shift-new": "workspace::NewWindow",
|
||||
"ctrl-shift-n": "workspace::NewWindow",
|
||||
"ctrl-`": "terminal_panel::ToggleFocus",
|
||||
"f10": ["app_menu::OpenApplicationMenu", "Zed"],
|
||||
"alt-1": ["workspace::ActivatePane", 0],
|
||||
"alt-2": ["workspace::ActivatePane", 1],
|
||||
"alt-3": ["workspace::ActivatePane", 2],
|
||||
@@ -587,7 +578,6 @@
|
||||
{
|
||||
"context": "ApplicationMenu",
|
||||
"bindings": {
|
||||
"f10": "menu::Cancel",
|
||||
"left": "app_menu::ActivateMenuLeft",
|
||||
"right": "app_menu::ActivateMenuRight"
|
||||
}
|
||||
|
||||
@@ -62,10 +62,6 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
// `Editor` context applies to all editor modes
|
||||
// - auto_height (multi-line: inline assistant, git commit messages, etc)
|
||||
// - single_line (command palette, renaming a file, etc)
|
||||
// - full (main editor buffers, assistant text threads)
|
||||
"context": "Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
@@ -80,6 +76,8 @@
|
||||
"ctrl-t": "editor::Transpose",
|
||||
"ctrl-k": "editor::KillRingCut",
|
||||
"ctrl-y": "editor::KillRingYank",
|
||||
"cmd-k cmd-q": "editor::Rewrap",
|
||||
"cmd-k q": "editor::Rewrap",
|
||||
"cmd-backspace": "editor::DeleteToBeginningOfLine",
|
||||
"cmd-delete": "editor::DeleteToEndOfLine",
|
||||
"alt-backspace": "editor::DeleteToPreviousWordStart",
|
||||
@@ -108,6 +106,7 @@
|
||||
"left": "editor::MoveLeft",
|
||||
"ctrl-f": "editor::MoveRight",
|
||||
"right": "editor::MoveRight",
|
||||
"ctrl-l": "editor::ScrollCursorCenter",
|
||||
"alt-left": "editor::MoveToPreviousWordStart",
|
||||
"alt-right": "editor::MoveToNextWordEnd",
|
||||
"cmd-left": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
|
||||
@@ -136,6 +135,8 @@
|
||||
"cmd-shift-down": "editor::SelectToEnd",
|
||||
"cmd-a": "editor::SelectAll",
|
||||
"cmd-l": "editor::SelectLine",
|
||||
"cmd-shift-i": "editor::Format",
|
||||
"alt-shift-o": "editor::OrganizeImports",
|
||||
"cmd-shift-left": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
|
||||
"shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
|
||||
"ctrl-shift-a": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
|
||||
@@ -144,7 +145,17 @@
|
||||
"ctrl-shift-e": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
|
||||
"ctrl-v": ["editor::MovePageDown", { "center_cursor": true }],
|
||||
"ctrl-shift-v": ["editor::MovePageUp", { "center_cursor": true }],
|
||||
"ctrl-cmd-space": "editor::ShowCharacterPalette"
|
||||
"ctrl-cmd-space": "editor::ShowCharacterPalette",
|
||||
"cmd-;": "editor::ToggleLineNumbers",
|
||||
"cmd-'": "editor::ToggleSelectedDiffHunks",
|
||||
"cmd-\"": "editor::ExpandAllDiffHunks",
|
||||
"cmd-alt-g b": "editor::ToggleGitBlame",
|
||||
"cmd-i": "editor::ShowSignatureHelp",
|
||||
"f9": "editor::ToggleBreakpoint",
|
||||
"shift-f9": "editor::EditLogBreakpoint",
|
||||
"ctrl-f12": "editor::GoToDeclaration",
|
||||
"alt-ctrl-f12": "editor::GoToDeclarationSplit",
|
||||
"ctrl-cmd-e": "editor::ToggleEditPrediction"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -163,22 +174,7 @@
|
||||
"cmd->": "assistant::QuoteSelection",
|
||||
"cmd-<": "assistant::InsertIntoEditor",
|
||||
"cmd-alt-e": "editor::SelectEnclosingSymbol",
|
||||
"cmd-k cmd-q": "editor::Rewrap",
|
||||
"cmd-k q": "editor::Rewrap",
|
||||
"ctrl-l": "editor::ScrollCursorCenter",
|
||||
"cmd-i": "editor::ShowSignatureHelp",
|
||||
"cmd-shift-i": "editor::Format",
|
||||
"alt-shift-o": "editor::OrganizeImports",
|
||||
"ctrl-cmd-e": "editor::ToggleEditPrediction",
|
||||
"f9": "editor::ToggleBreakpoint",
|
||||
"shift-f9": "editor::EditLogBreakpoint",
|
||||
"ctrl-f12": "editor::GoToDeclaration",
|
||||
"alt-ctrl-f12": "editor::GoToDeclarationSplit",
|
||||
"alt-enter": "editor::OpenSelectionsInMultibuffer",
|
||||
"cmd-;": "editor::ToggleLineNumbers",
|
||||
"cmd-'": "editor::ToggleSelectedDiffHunks",
|
||||
"cmd-\"": "editor::ExpandAllDiffHunks",
|
||||
"cmd-alt-g b": "editor::ToggleGitBlame"
|
||||
"alt-enter": "editor::OpenSelectionsInMultibuffer"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -294,19 +290,11 @@
|
||||
"cmd-i": "agent::ToggleProfileSelector",
|
||||
"cmd-alt-/": "assistant::ToggleModelSelector",
|
||||
"cmd-shift-a": "agent::ToggleContextPicker",
|
||||
"cmd-shift-o": "agent::ToggleNavigationMenu",
|
||||
"cmd-shift-i": "agent::ToggleOptionsMenu",
|
||||
"shift-escape": "agent::ExpandMessageEditor",
|
||||
"cmd-e": "agent::ChatMode",
|
||||
"cmd-alt-e": "agent::RemoveAllContext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel > NavigationMenu",
|
||||
"bindings": {
|
||||
"shift-backspace": "agent::DeleteRecentlyOpenThread"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel > Markdown",
|
||||
"use_key_equivalents": true,
|
||||
@@ -502,6 +490,8 @@
|
||||
"alt-down": "editor::MoveLineDown",
|
||||
"alt-shift-up": "editor::DuplicateLineUp",
|
||||
"alt-shift-down": "editor::DuplicateLineDown",
|
||||
"ctrl-shift-right": "editor::SelectLargerSyntaxNode", // Expand Selection
|
||||
"ctrl-shift-left": "editor::SelectSmallerSyntaxNode", // Shrink Selection
|
||||
"cmd-d": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch / find_under_expand
|
||||
"cmd-shift-l": "editor::SelectAllMatches", // Select all occurrences of current selection
|
||||
"cmd-f2": "editor::SelectAllMatches", // Select all occurrences of current word
|
||||
@@ -511,18 +501,21 @@
|
||||
// defaults write com.apple.symbolichotkeys AppleSymbolicHotKeys -dict-add 70 '<dict><key>enabled</key><false/></dict>'
|
||||
"ctrl-cmd-d": ["editor::SelectPrevious", { "replace_newest": false }], // editor.action.addSelectionToPreviousFindMatch
|
||||
"cmd-k ctrl-cmd-d": ["editor::SelectPrevious", { "replace_newest": true }], // editor.action.moveSelectionToPreviousFindMatch
|
||||
"cmd-k cmd-i": "editor::Hover",
|
||||
"cmd-/": ["editor::ToggleComments", { "advance_downwards": false }],
|
||||
"cmd-u": "editor::UndoSelection",
|
||||
"cmd-shift-u": "editor::RedoSelection",
|
||||
"f8": "editor::GoToDiagnostic",
|
||||
"shift-f8": "editor::GoToPreviousDiagnostic",
|
||||
"f2": "editor::Rename",
|
||||
"f12": "editor::GoToDefinition",
|
||||
"alt-f12": "editor::GoToDefinitionSplit",
|
||||
"cmd-f12": "editor::GoToTypeDefinition",
|
||||
"shift-f12": "editor::GoToImplementation",
|
||||
"alt-cmd-f12": "editor::GoToTypeDefinitionSplit",
|
||||
"alt-shift-f12": "editor::FindAllReferences",
|
||||
"cmd-|": "editor::MoveToEnclosingBracket",
|
||||
"ctrl-m": "editor::MoveToEnclosingBracket",
|
||||
"cmd-\\": "pane::SplitRight"
|
||||
}
|
||||
},
|
||||
// VSCode Bindings full Editors (e.g. buffers)
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"alt-cmd-[": "editor::Fold",
|
||||
"alt-cmd-]": "editor::UnfoldLines",
|
||||
"cmd-k cmd-l": "editor::ToggleFold",
|
||||
@@ -546,28 +539,22 @@
|
||||
"cmd-.": "editor::ToggleCodeActions",
|
||||
"cmd-k r": "editor::RevealInFileManager",
|
||||
"cmd-k p": "editor::CopyPath",
|
||||
"ctrl-shift-right": "editor::SelectLargerSyntaxNode", // Expand Selection
|
||||
"ctrl-shift-left": "editor::SelectSmallerSyntaxNode", // Shrink Selection
|
||||
"cmd-k cmd-i": "editor::Hover",
|
||||
"cmd-/": ["editor::ToggleComments", { "advance_downwards": false }],
|
||||
"f8": "editor::GoToDiagnostic",
|
||||
"shift-f8": "editor::GoToPreviousDiagnostic",
|
||||
"f2": "editor::Rename",
|
||||
"f12": "editor::GoToDefinition",
|
||||
"alt-f12": "editor::GoToDefinitionSplit",
|
||||
"cmd-f12": "editor::GoToTypeDefinition",
|
||||
"shift-f12": "editor::GoToImplementation",
|
||||
"alt-cmd-f12": "editor::GoToTypeDefinitionSplit",
|
||||
"alt-shift-f12": "editor::FindAllReferences",
|
||||
"cmd-\\": "pane::SplitRight",
|
||||
"cmd-k v": "markdown::OpenPreviewToTheSide",
|
||||
"cmd-shift-v": "markdown::OpenPreview",
|
||||
"ctrl-cmd-c": "editor::DisplayCursorNames",
|
||||
"cmd-shift-o": "outline::Toggle",
|
||||
"ctrl-g": "go_to_line::Toggle",
|
||||
"cmd-shift-backspace": "editor::GoToPreviousChange",
|
||||
"cmd-shift-alt-backspace": "editor::GoToNextChange"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-shift-o": "outline::Toggle",
|
||||
"ctrl-g": "go_to_line::Toggle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
"use_key_equivalents": true,
|
||||
|
||||
@@ -10,6 +10,11 @@
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"ctrl-shift-l": "language_selector::Toggle", // grammar-selector:show
|
||||
"ctrl-|": "pane::RevealInProjectPanel", // tree-view:reveal-active-file
|
||||
"ctrl-b": "editor::GoToDefinition", // fuzzy-finder:toggle-buffer-finder
|
||||
"ctrl-alt-b": "editor::GoToDefinitionSplit", // N/A: From JetBrains
|
||||
"ctrl-<": "editor::ScrollCursorCenter", // editor:scroll-to-cursor
|
||||
"f3": ["editor::SelectNext", { "replace_newest": true }], // find-and-replace:find-next
|
||||
"shift-f3": ["editor::SelectPrevious", { "replace_newest": true }], //find-and-replace:find-previous
|
||||
"alt-shift-down": "editor::AddSelectionBelow", // editor:add-selection-below
|
||||
@@ -20,19 +25,14 @@
|
||||
"ctrl-shift-d": "editor::DuplicateLineDown", // editor:duplicate-lines
|
||||
"ctrl-up": "editor::MoveLineUp", // editor:move-line-up
|
||||
"ctrl-down": "editor::MoveLineDown", // editor:move-line-down
|
||||
"ctrl-\\": "workspace::ToggleLeftDock" // tree-view:toggle (overrides bind in default keymap)
|
||||
"ctrl-\\": "workspace::ToggleLeftDock", // tree-view:toggle
|
||||
"ctrl-shift-m": "markdown::OpenPreviewToTheSide" // markdown-preview:toggle
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
"ctrl-shift-l": "language_selector::Toggle", // grammar-selector:show
|
||||
"ctrl-|": "pane::RevealInProjectPanel", // tree-view:reveal-active-file
|
||||
"ctrl-b": "editor::GoToDefinition", // fuzzy-finder:toggle-buffer-finder
|
||||
"ctrl-alt-b": "editor::GoToDefinitionSplit", // N/A: From JetBrains
|
||||
"ctrl-<": "editor::ScrollCursorCenter", // editor:scroll-to-cursor
|
||||
"ctrl-r": "outline::Toggle", // symbols-view:toggle-project-symbols
|
||||
"ctrl-shift-m": "markdown::OpenPreviewToTheSide" // markdown-preview:toggle
|
||||
"ctrl-r": "outline::Toggle" // symbols-view:toggle-project-symbols
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -32,15 +32,24 @@
|
||||
"ctrl-alt-down": "editor::AddSelectionBelow",
|
||||
"ctrl-shift-up": "editor::MoveLineUp",
|
||||
"ctrl-shift-down": "editor::MoveLineDown",
|
||||
"ctrl-shift-m": "editor::SelectLargerSyntaxNode",
|
||||
"ctrl-shift-l": "editor::SplitSelectionIntoLines",
|
||||
"ctrl-shift-a": "editor::SelectLargerSyntaxNode",
|
||||
"ctrl-shift-d": "editor::DuplicateSelection",
|
||||
"alt-f3": "editor::SelectAllMatches", // find_all_under
|
||||
// "ctrl-f3": "", // find_under (cancels any selections)
|
||||
// "cmd-alt-shift-g": "" // find_under_prev (cancels any selections)
|
||||
"f9": "editor::SortLinesCaseSensitive",
|
||||
"ctrl-f9": "editor::SortLinesCaseInsensitive",
|
||||
"f12": "editor::GoToDefinition",
|
||||
"ctrl-f12": "editor::GoToDefinitionSplit",
|
||||
"shift-f12": "editor::FindAllReferences",
|
||||
"ctrl-shift-f12": "editor::FindAllReferences",
|
||||
"ctrl-.": "editor::GoToHunk",
|
||||
"ctrl-,": "editor::GoToPreviousHunk",
|
||||
"ctrl-k ctrl-u": "editor::ConvertToUpperCase",
|
||||
"ctrl-k ctrl-l": "editor::ConvertToLowerCase",
|
||||
"shift-alt-m": "markdown::OpenPreviewToTheSide",
|
||||
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd",
|
||||
"f3": "editor::FindNextMatch",
|
||||
@@ -50,16 +59,7 @@
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
"ctrl-r": "outline::Toggle",
|
||||
"ctrl-shift-m": "editor::SelectLargerSyntaxNode",
|
||||
"ctrl-shift-a": "editor::SelectLargerSyntaxNode",
|
||||
"f12": "editor::GoToDefinition",
|
||||
"ctrl-f12": "editor::GoToDefinitionSplit",
|
||||
"shift-f12": "editor::FindAllReferences",
|
||||
"ctrl-shift-f12": "editor::FindAllReferences",
|
||||
"ctrl-.": "editor::GoToHunk",
|
||||
"ctrl-,": "editor::GoToPreviousHunk",
|
||||
"shift-alt-m": "markdown::OpenPreviewToTheSide"
|
||||
"ctrl-r": "outline::Toggle"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -10,6 +10,11 @@
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"ctrl-shift-l": "language_selector::Toggle",
|
||||
"cmd-|": "pane::RevealInProjectPanel",
|
||||
"cmd-b": "editor::GoToDefinition",
|
||||
"alt-cmd-b": "editor::GoToDefinitionSplit",
|
||||
"cmd-<": "editor::ScrollCursorCenter",
|
||||
"cmd-g": ["editor::SelectNext", { "replace_newest": true }],
|
||||
"cmd-shift-g": ["editor::SelectPrevious", { "replace_newest": true }],
|
||||
"ctrl-shift-down": "editor::AddSelectionBelow",
|
||||
@@ -21,18 +26,13 @@
|
||||
"cmd-shift-d": "editor::DuplicateLineDown",
|
||||
"ctrl-cmd-up": "editor::MoveLineUp",
|
||||
"ctrl-cmd-down": "editor::MoveLineDown",
|
||||
"cmd-\\": "workspace::ToggleLeftDock", // overrides bind in default keymap
|
||||
"cmd-\\": "workspace::ToggleLeftDock",
|
||||
"ctrl-shift-m": "markdown::OpenPreviewToTheSide"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
"ctrl-shift-l": "language_selector::Toggle",
|
||||
"cmd-|": "pane::RevealInProjectPanel",
|
||||
"cmd-b": "editor::GoToDefinition",
|
||||
"alt-cmd-b": "editor::GoToDefinitionSplit",
|
||||
"cmd-<": "editor::ScrollCursorCenter",
|
||||
"cmd-r": "outline::Toggle"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -50,12 +50,6 @@
|
||||
"] -": "vim::NextLesserIndent",
|
||||
"] +": "vim::NextGreaterIndent",
|
||||
"] =": "vim::NextSameIndent",
|
||||
"] b": "pane::ActivateNextItem",
|
||||
"[ b": "pane::ActivatePreviousItem",
|
||||
"] shift-b": "pane::ActivateLastItem",
|
||||
"[ shift-b": ["pane::ActivateItem", 0],
|
||||
"] space": "vim::InsertEmptyLineBelow",
|
||||
"[ space": "vim::InsertEmptyLineAbove",
|
||||
// Word motions
|
||||
"w": "vim::NextWordStart",
|
||||
"e": "vim::NextWordEnd",
|
||||
@@ -114,11 +108,7 @@
|
||||
"ctrl-e": "vim::LineDown",
|
||||
"ctrl-y": "vim::LineUp",
|
||||
// "g" commands
|
||||
"g shift-r": "vim::PushReplaceWithRegister",
|
||||
"g r n": "editor::Rename",
|
||||
"g r r": "editor::FindAllReferences",
|
||||
"g r i": "editor::GoToImplementation",
|
||||
"g r a": "editor::ToggleCodeActions",
|
||||
"g r": "vim::PushReplaceWithRegister",
|
||||
"g g": "vim::StartOfDocument",
|
||||
"g h": "editor::Hover",
|
||||
"g t": "pane::ActivateNextItem",
|
||||
@@ -137,7 +127,6 @@
|
||||
"g <": ["editor::SelectPrevious", { "replace_newest": true }],
|
||||
"g a": "editor::SelectAllMatches",
|
||||
"g s": "outline::Toggle",
|
||||
"g shift-o": "outline::Toggle",
|
||||
"g shift-s": "project_symbols::Toggle",
|
||||
"g .": "editor::ToggleCodeActions", // zed specific
|
||||
"g shift-a": "editor::FindAllReferences", // zed specific
|
||||
@@ -316,7 +305,7 @@
|
||||
"!": "vim::ShellCommand",
|
||||
"i": ["vim::PushObject", { "around": false }],
|
||||
"a": ["vim::PushObject", { "around": true }],
|
||||
"g shift-r": ["vim::Paste", { "preserve_clipboard": true }],
|
||||
"g r": ["vim::Paste", { "preserve_clipboard": true }],
|
||||
"g c": "vim::ToggleComments",
|
||||
"g q": "vim::Rewrap",
|
||||
"g ?": "vim::ConvertToRot13",
|
||||
@@ -350,8 +339,7 @@
|
||||
"ctrl-shift-q": ["vim::PushLiteral", {}],
|
||||
"ctrl-r": "vim::PushRegister",
|
||||
"insert": "vim::ToggleReplace",
|
||||
"ctrl-o": "vim::TemporaryNormal",
|
||||
"ctrl-s": "editor::ShowSignatureHelp"
|
||||
"ctrl-o": "vim::TemporaryNormal"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -516,14 +504,12 @@
|
||||
"'": "vim::Quotes",
|
||||
"`": "vim::BackQuotes",
|
||||
"\"": "vim::DoubleQuotes",
|
||||
// "q": "vim::AnyQuotes",
|
||||
"q": "vim::MiniQuotes",
|
||||
"q": "vim::AnyQuotes",
|
||||
"|": "vim::VerticalBars",
|
||||
"(": "vim::Parentheses",
|
||||
")": "vim::Parentheses",
|
||||
"b": "vim::Parentheses",
|
||||
// "b": "vim::AnyBrackets",
|
||||
// "b": "vim::MiniBrackets",
|
||||
"[": "vim::SquareBrackets",
|
||||
"]": "vim::SquareBrackets",
|
||||
"r": "vim::SquareBrackets",
|
||||
@@ -644,10 +630,9 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == gR",
|
||||
"context": "vim_operator == gr",
|
||||
"bindings": {
|
||||
"r": "vim::CurrentLine",
|
||||
"shift-r": "vim::CurrentLine"
|
||||
"r": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -15,7 +15,6 @@ You are a highly skilled software engineer with extensive knowledge in many prog
|
||||
3. DO NOT use tools to access items that are already available in the context section.
|
||||
4. Use only the tools that are currently available.
|
||||
5. DO NOT use a tool that is not available just because it appears in the conversation. This means the user turned it off.
|
||||
6. NEVER run commands that don't terminate on their own such as web servers (like `npm run start`, `npm run dev`, `python -m http.server`, etc) or file watchers.
|
||||
|
||||
## Searching and Reading
|
||||
|
||||
@@ -28,89 +27,14 @@ If appropriate, use tool calls to explore the current project, which contains th
|
||||
- `{{root_name}}`
|
||||
{{/each}}
|
||||
|
||||
- Bias towards not asking the user for help if you can find the answer yourself.
|
||||
- When providing paths to tools, the path should always begin with a path that starts with a project root directory listed above.
|
||||
- Before you read or edit a file, you must first find the full path. DO NOT ever guess a file path!
|
||||
{{# if (has_tool 'grep') }}
|
||||
- When looking for symbols in the project, prefer the `grep` tool.
|
||||
- As you learn about the structure of the project, use that information to scope `grep` searches to targeted subtrees of the project.
|
||||
- Bias towards not asking the user for help if you can find the answer yourself.
|
||||
{{! TODO: Only mention tools if they are enabled }}
|
||||
- The user might specify a partial file path. If you don't know the full path, use `find_path` (not `grep`) before you read the file.
|
||||
{{/if}}
|
||||
- Before you read or edit a file, you must first find the full path. DO NOT ever guess a file path!
|
||||
|
||||
## Code Block Formatting
|
||||
|
||||
Whenever you mention a code block, you MUST use ONLY use the following format:
|
||||
```path/to/Something.blah#L123-456
|
||||
(code goes here)
|
||||
```
|
||||
The `#L123-456` means the line number range 123 through 456, and the path/to/Something.blah
|
||||
is a path in the project. (If there is no valid path in the project, then you can use
|
||||
/dev/null/path.extension for its path.) This is the ONLY valid way to format code blocks, because the Markdown parser
|
||||
does not understand the more common ```language syntax, or bare ``` blocks. It only
|
||||
understands this path-based syntax, and if the path is missing, then it will error and you will have to do it over again.
|
||||
Just to be really clear about this, if you ever find yourself writing three backticks followed by a language name, STOP!
|
||||
You have made a mistake. You can only ever put paths after triple backticks!
|
||||
<example>
|
||||
Based on all the information I've gathered, here's a summary of how this system works:
|
||||
1. The README file is loaded into the system.
|
||||
2. The system finds the first two headers, including everything in between. In this case, that would be:
|
||||
```path/to/README.md#L8-12
|
||||
# First Header
|
||||
This is the info under the first header.
|
||||
## Sub-header
|
||||
```
|
||||
3. Then the system finds the last header in the README:
|
||||
```path/to/README.md#L27-29
|
||||
## Last Header
|
||||
This is the last header in the README.
|
||||
```
|
||||
4. Finally, it passes this information on to the next process.
|
||||
</example>
|
||||
<example>
|
||||
In Markdown, hash marks signify headings. For example:
|
||||
```/dev/null/example.md#L1-3
|
||||
# Level 1 heading
|
||||
## Level 2 heading
|
||||
### Level 3 heading
|
||||
```
|
||||
</example>
|
||||
Here are examples of ways you must never render code blocks:
|
||||
<bad_example_do_not_do_this>
|
||||
In Markdown, hash marks signify headings. For example:
|
||||
```
|
||||
# Level 1 heading
|
||||
## Level 2 heading
|
||||
### Level 3 heading
|
||||
```
|
||||
</bad_example_do_not_do_this>
|
||||
This example is unacceptable because it does not include the path.
|
||||
<bad_example_do_not_do_this>
|
||||
In Markdown, hash marks signify headings. For example:
|
||||
```markdown
|
||||
# Level 1 heading
|
||||
## Level 2 heading
|
||||
### Level 3 heading
|
||||
```
|
||||
</bad_example_do_not_do_this>
|
||||
This example is unacceptable because it has the language instead of the path.
|
||||
<bad_example_do_not_do_this>
|
||||
In Markdown, hash marks signify headings. For example:
|
||||
# Level 1 heading
|
||||
## Level 2 heading
|
||||
### Level 3 heading
|
||||
</bad_example_do_not_do_this>
|
||||
This example is unacceptable because it uses indentation to mark the code block
|
||||
instead of backticks with a path.
|
||||
<bad_example_do_not_do_this>
|
||||
In Markdown, hash marks signify headings. For example:
|
||||
```markdown
|
||||
/dev/null/example.md#L1-3
|
||||
# Level 1 heading
|
||||
## Level 2 heading
|
||||
### Level 3 heading
|
||||
```
|
||||
</bad_example_do_not_do_this>
|
||||
This example is unacceptable because the path is in the wrong place. The path must be directly after the opening backticks.
|
||||
## Fixing Diagnostics
|
||||
|
||||
1. Make 1-2 attempts at fixing diagnostics, then defer to the user.
|
||||
|
||||
@@ -226,7 +226,7 @@
|
||||
// Hide the values of in variables from visual display in private files
|
||||
"redact_private_values": false,
|
||||
// The default number of lines to expand excerpts in the multibuffer by.
|
||||
"expand_excerpt_lines": 5,
|
||||
"expand_excerpt_lines": 3,
|
||||
// Globs to match against file paths to determine if a file is private.
|
||||
"private_files": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/secrets.yml"],
|
||||
// Whether to use additional LSP queries to format (and amend) the code after
|
||||
@@ -601,13 +601,6 @@
|
||||
//
|
||||
// Default: main
|
||||
"fallback_branch_name": "main",
|
||||
|
||||
// Whether to sort entries in the panel by path
|
||||
// or by status (the default).
|
||||
//
|
||||
// Default: false
|
||||
"sort_by_path": false,
|
||||
|
||||
"scrollbar": {
|
||||
// When to show the scrollbar in the git panel.
|
||||
//
|
||||
@@ -702,11 +695,6 @@
|
||||
"thinking": true,
|
||||
"web_search": true
|
||||
}
|
||||
},
|
||||
"manual": {
|
||||
"name": "Manual",
|
||||
"enable_all_context_servers": false,
|
||||
"tools": {}
|
||||
}
|
||||
},
|
||||
// Where to show notifications when an agent has either completed
|
||||
|
||||
@@ -60,7 +60,6 @@ ordered-float.workspace = true
|
||||
parking_lot.workspace = true
|
||||
paths.workspace = true
|
||||
picker.workspace = true
|
||||
postage.workspace = true
|
||||
project.workspace = true
|
||||
rules_library.workspace = true
|
||||
prompt_store.workspace = true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::context::{AgentContextHandle, RULES_ICON};
|
||||
use crate::context::{AgentContext, RULES_ICON};
|
||||
use crate::context_picker::MentionLink;
|
||||
use crate::thread::{
|
||||
LastRestoreCheckpoint, MessageId, MessageSegment, Thread, ThreadError, ThreadEvent,
|
||||
@@ -23,17 +23,16 @@ use gpui::{
|
||||
Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, WindowHandle,
|
||||
linear_color_stop, linear_gradient, list, percentage, pulsating_between,
|
||||
};
|
||||
use language::{Buffer, Language, LanguageRegistry};
|
||||
use language::{Buffer, LanguageRegistry};
|
||||
use language_model::{
|
||||
LanguageModelRequestMessage, LanguageModelToolUseId, MessageContent, RequestUsage, Role,
|
||||
StopReason,
|
||||
LanguageModelRegistry, LanguageModelRequestMessage, LanguageModelToolUseId, MessageContent,
|
||||
RequestUsage, Role, StopReason,
|
||||
};
|
||||
use markdown::parser::{CodeBlockKind, CodeBlockMetadata};
|
||||
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle, ParsedMarkdown};
|
||||
use project::{ProjectEntryId, ProjectItem as _};
|
||||
use rope::Point;
|
||||
use settings::{Settings as _, update_settings_file};
|
||||
use std::ffi::OsStr;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
@@ -347,130 +346,130 @@ fn render_markdown_code_block(
|
||||
.child(Label::new("untitled").size(LabelSize::Small))
|
||||
.into_any_element(),
|
||||
),
|
||||
CodeBlockKind::FencedLang(raw_language_name) => Some(render_code_language(
|
||||
parsed_markdown.languages_by_name.get(raw_language_name),
|
||||
raw_language_name.clone(),
|
||||
cx,
|
||||
)),
|
||||
CodeBlockKind::FencedSrc(path_range) => path_range.path.file_name().map(|file_name| {
|
||||
// We tell the model to use /dev/null for the path instead of using ```language
|
||||
// because otherwise it consistently fails to use code citations.
|
||||
if path_range.path.starts_with("/dev/null") {
|
||||
let ext = path_range
|
||||
.path
|
||||
.extension()
|
||||
.and_then(OsStr::to_str)
|
||||
.map(|str| SharedString::new(str.to_string()))
|
||||
.unwrap_or_default();
|
||||
|
||||
render_code_language(
|
||||
CodeBlockKind::FencedLang(raw_language_name) => Some(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.children(
|
||||
parsed_markdown
|
||||
.languages_by_path
|
||||
.get(&path_range.path)
|
||||
.or_else(|| parsed_markdown.languages_by_name.get(&ext)),
|
||||
ext,
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
let content = if let Some(parent) = path_range.path.parent() {
|
||||
h_flex()
|
||||
.ml_1()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new(file_name.to_string_lossy().to_string())
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.child(
|
||||
Label::new(parent.to_string_lossy().to_string())
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
Label::new(path_range.path.to_string_lossy().to_string())
|
||||
.size(LabelSize::Small)
|
||||
.ml_1()
|
||||
.into_any_element()
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.id(("code-block-header-label", ix))
|
||||
.w_full()
|
||||
.max_w_full()
|
||||
.px_1()
|
||||
.gap_0p5()
|
||||
.cursor_pointer()
|
||||
.rounded_sm()
|
||||
.hover(|item| item.bg(cx.theme().colors().element_hover.opacity(0.5)))
|
||||
.tooltip(Tooltip::text("Jump to File"))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.children(
|
||||
file_icons::FileIcons::get_icon(&path_range.path, cx)
|
||||
.map(Icon::from_path)
|
||||
.map(|icon| icon.color(Color::Muted).size(IconSize::XSmall)),
|
||||
)
|
||||
.child(content)
|
||||
.child(
|
||||
Icon::new(IconName::ArrowUpRight)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Ignored),
|
||||
),
|
||||
)
|
||||
.on_click({
|
||||
let path_range = path_range.clone();
|
||||
move |_, window, cx| {
|
||||
workspace
|
||||
.update(cx, {
|
||||
|workspace, cx| {
|
||||
let Some(project_path) = workspace
|
||||
.project()
|
||||
.read(cx)
|
||||
.find_project_path(&path_range.path, cx)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(target) = path_range.range.as_ref().map(|range| {
|
||||
Point::new(
|
||||
// Line number is 1-based
|
||||
range.start.line.saturating_sub(1),
|
||||
range.start.col.unwrap_or(0),
|
||||
)
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
let open_task = workspace.open_path(
|
||||
project_path,
|
||||
None,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
let item = open_task.await?;
|
||||
if let Some(active_editor) =
|
||||
item.downcast::<Editor>()
|
||||
{
|
||||
active_editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor.go_to_singleton_buffer_point(
|
||||
target, window, cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
.languages_by_name
|
||||
.get(raw_language_name)
|
||||
.and_then(|language| {
|
||||
language
|
||||
.config()
|
||||
.matcher
|
||||
.path_suffixes
|
||||
.iter()
|
||||
.find_map(|extension| {
|
||||
file_icons::FileIcons::get_icon(Path::new(extension), cx)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.map(Icon::from_path)
|
||||
.map(|icon| icon.color(Color::Muted).size(IconSize::Small))
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Label::new(
|
||||
parsed_markdown
|
||||
.languages_by_name
|
||||
.get(raw_language_name)
|
||||
.map(|language| language.name().into())
|
||||
.clone()
|
||||
.unwrap_or_else(|| raw_language_name.clone()),
|
||||
)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
CodeBlockKind::FencedSrc(path_range) => path_range.path.file_name().map(|file_name| {
|
||||
let content = if let Some(parent) = path_range.path.parent() {
|
||||
h_flex()
|
||||
.ml_1()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new(file_name.to_string_lossy().to_string()).size(LabelSize::Small),
|
||||
)
|
||||
.child(
|
||||
Label::new(parent.to_string_lossy().to_string())
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
} else {
|
||||
Label::new(path_range.path.to_string_lossy().to_string())
|
||||
.size(LabelSize::Small)
|
||||
.ml_1()
|
||||
.into_any_element()
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.id(("code-block-header-label", ix))
|
||||
.w_full()
|
||||
.max_w_full()
|
||||
.px_1()
|
||||
.gap_0p5()
|
||||
.cursor_pointer()
|
||||
.rounded_sm()
|
||||
.hover(|item| item.bg(cx.theme().colors().element_hover.opacity(0.5)))
|
||||
.tooltip(Tooltip::text("Jump to File"))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.children(
|
||||
file_icons::FileIcons::get_icon(&path_range.path, cx)
|
||||
.map(Icon::from_path)
|
||||
.map(|icon| icon.color(Color::Muted).size(IconSize::XSmall)),
|
||||
)
|
||||
.child(content)
|
||||
.child(
|
||||
Icon::new(IconName::ArrowUpRight)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Ignored),
|
||||
),
|
||||
)
|
||||
.on_click({
|
||||
let path_range = path_range.clone();
|
||||
move |_, window, cx| {
|
||||
workspace
|
||||
.update(cx, {
|
||||
|workspace, cx| {
|
||||
let Some(project_path) = workspace
|
||||
.project()
|
||||
.read(cx)
|
||||
.find_project_path(&path_range.path, cx)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(target) = path_range.range.as_ref().map(|range| {
|
||||
Point::new(
|
||||
// Line number is 1-based
|
||||
range.start.line.saturating_sub(1),
|
||||
range.start.col.unwrap_or(0),
|
||||
)
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
let open_task =
|
||||
workspace.open_path(project_path, None, true, window, cx);
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
let item = open_task.await?;
|
||||
if let Some(active_editor) = item.downcast::<Editor>() {
|
||||
active_editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor.go_to_singleton_buffer_point(
|
||||
target, window, cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -605,32 +604,6 @@ fn render_markdown_code_block(
|
||||
)
|
||||
}
|
||||
|
||||
fn render_code_language(
|
||||
language: Option<&Arc<Language>>,
|
||||
name_fallback: SharedString,
|
||||
cx: &App,
|
||||
) -> AnyElement {
|
||||
let icon_path = language.and_then(|language| {
|
||||
language
|
||||
.config()
|
||||
.matcher
|
||||
.path_suffixes
|
||||
.iter()
|
||||
.find_map(|extension| file_icons::FileIcons::get_icon(Path::new(extension), cx))
|
||||
.map(Icon::from_path)
|
||||
});
|
||||
|
||||
let language_label = language
|
||||
.map(|language| language.name().into())
|
||||
.unwrap_or(name_fallback);
|
||||
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.children(icon_path.map(|icon| icon.color(Color::Muted).size(IconSize::Small)))
|
||||
.child(Label::new(language_label).size(LabelSize::Small))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn open_markdown_link(
|
||||
text: SharedString,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
@@ -709,7 +682,7 @@ fn open_markdown_link(
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel
|
||||
.open_thread_by_id(&thread_id, window, cx)
|
||||
.open_thread(&thread_id, window, cx)
|
||||
.detach_and_log_err(cx)
|
||||
});
|
||||
}
|
||||
@@ -729,7 +702,7 @@ struct EditMessageState {
|
||||
editor: Entity<Editor>,
|
||||
last_estimated_token_count: Option<usize>,
|
||||
_subscription: Subscription,
|
||||
_update_token_count_task: Option<Task<()>>,
|
||||
_update_token_count_task: Option<Task<anyhow::Result<()>>>,
|
||||
}
|
||||
|
||||
impl ActiveThread {
|
||||
@@ -930,11 +903,6 @@ impl ActiveThread {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
ThreadEvent::CancelEditing => {
|
||||
if self.editing_message.is_some() {
|
||||
self.cancel_editing_message(&menu::Cancel, window, cx);
|
||||
}
|
||||
}
|
||||
ThreadEvent::ShowError(error) => {
|
||||
self.last_error = Some(error.clone());
|
||||
}
|
||||
@@ -1279,7 +1247,7 @@ impl ActiveThread {
|
||||
cx.emit(ActiveThreadEvent::EditingMessageTokenCountChanged);
|
||||
state._update_token_count_task.take();
|
||||
|
||||
let Some(configured_model) = self.thread.read(cx).configured_model() else {
|
||||
let Some(default_model) = LanguageModelRegistry::read_global(cx).default_model() else {
|
||||
state.last_estimated_token_count.take();
|
||||
return;
|
||||
};
|
||||
@@ -1295,65 +1263,58 @@ impl ActiveThread {
|
||||
.await;
|
||||
}
|
||||
|
||||
let token_count = if let Some(task) = cx
|
||||
.update(|cx| {
|
||||
let Some(message) = thread.read(cx).message(message_id) else {
|
||||
log::error!("Message that was being edited no longer exists");
|
||||
return None;
|
||||
};
|
||||
let message_text = editor.read(cx).text(cx);
|
||||
let token_count = if let Some(task) = cx.update(|cx| {
|
||||
let Some(message) = thread.read(cx).message(message_id) else {
|
||||
log::error!("Message that was being edited no longer exists");
|
||||
return None;
|
||||
};
|
||||
let message_text = editor.read(cx).text(cx);
|
||||
|
||||
if message_text.is_empty() && message.loaded_context.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if message_text.is_empty() && message.loaded_context.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut request_message = LanguageModelRequestMessage {
|
||||
role: language_model::Role::User,
|
||||
content: Vec::new(),
|
||||
cache: false,
|
||||
};
|
||||
let mut request_message = LanguageModelRequestMessage {
|
||||
role: language_model::Role::User,
|
||||
content: Vec::new(),
|
||||
cache: false,
|
||||
};
|
||||
|
||||
message
|
||||
.loaded_context
|
||||
.add_to_request_message(&mut request_message);
|
||||
message
|
||||
.loaded_context
|
||||
.add_to_request_message(&mut request_message);
|
||||
|
||||
if !message_text.is_empty() {
|
||||
request_message
|
||||
.content
|
||||
.push(MessageContent::Text(message_text));
|
||||
}
|
||||
if !message_text.is_empty() {
|
||||
request_message
|
||||
.content
|
||||
.push(MessageContent::Text(message_text));
|
||||
}
|
||||
|
||||
let request = language_model::LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
mode: None,
|
||||
messages: vec![request_message],
|
||||
tools: vec![],
|
||||
stop: vec![],
|
||||
temperature: None,
|
||||
};
|
||||
let request = language_model::LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
mode: None,
|
||||
messages: vec![request_message],
|
||||
tools: vec![],
|
||||
stop: vec![],
|
||||
temperature: None,
|
||||
};
|
||||
|
||||
Some(configured_model.model.count_tokens(request, cx))
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
{
|
||||
task.await.log_err()
|
||||
Some(default_model.model.count_tokens(request, cx))
|
||||
})? {
|
||||
task.await?
|
||||
} else {
|
||||
Some(0)
|
||||
0
|
||||
};
|
||||
|
||||
if let Some(token_count) = token_count {
|
||||
this.update(cx, |this, cx| {
|
||||
let Some((_message_id, state)) = this.editing_message.as_mut() else {
|
||||
return;
|
||||
};
|
||||
this.update(cx, |this, cx| {
|
||||
let Some((_message_id, state)) = this.editing_message.as_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
state.last_estimated_token_count = Some(token_count);
|
||||
cx.emit(ActiveThreadEvent::EditingMessageTokenCountChanged);
|
||||
})
|
||||
.ok();
|
||||
};
|
||||
state.last_estimated_token_count = Some(token_count);
|
||||
cx.emit(ActiveThreadEvent::EditingMessageTokenCountChanged);
|
||||
})
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1372,7 +1333,7 @@ impl ActiveThread {
|
||||
return;
|
||||
};
|
||||
let edited_text = state.editor.read(cx).text(cx);
|
||||
let thread_model = self.thread.update(cx, |thread, cx| {
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.edit_message(
|
||||
message_id,
|
||||
Role::User,
|
||||
@@ -1382,10 +1343,9 @@ impl ActiveThread {
|
||||
for message_id in self.messages_after(message_id) {
|
||||
thread.delete_message(*message_id, cx);
|
||||
}
|
||||
thread.get_or_init_configured_model(cx)
|
||||
});
|
||||
|
||||
let Some(model) = thread_model else {
|
||||
let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -1531,13 +1491,19 @@ impl ActiveThread {
|
||||
|
||||
let workspace = self.workspace.clone();
|
||||
let thread = self.thread.read(cx);
|
||||
let prompt_store = self.thread_store.read(cx).prompt_store().as_ref();
|
||||
|
||||
// Get all the data we need from thread before we start using it in closures
|
||||
let checkpoint = thread.checkpoint_for_message(message_id);
|
||||
let added_context = thread
|
||||
.context_for_message(message_id)
|
||||
.map(|context| AddedContext::new_attached(context, cx))
|
||||
.collect::<Vec<_>>();
|
||||
let added_context = if let Some(workspace) = workspace.upgrade() {
|
||||
let project = workspace.read(cx).project().read(cx);
|
||||
thread
|
||||
.context_for_message(message_id)
|
||||
.flat_map(|context| AddedContext::new(context.clone(), prompt_store, project, cx))
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
return Empty.into_any();
|
||||
};
|
||||
|
||||
let tool_uses = thread.tool_uses_for_message(message_id, cx);
|
||||
let has_tool_uses = !tool_uses.is_empty();
|
||||
@@ -1548,6 +1514,8 @@ impl ActiveThread {
|
||||
|
||||
let show_feedback = thread.is_turn_end(ix);
|
||||
|
||||
let needs_confirmation = tool_uses.iter().any(|tool_use| tool_use.needs_confirmation);
|
||||
|
||||
let generating_label = (is_generating && is_last_message)
|
||||
.then(|| AnimatedLabel::new("Generating").size(LabelSize::Small));
|
||||
|
||||
@@ -1570,11 +1538,10 @@ impl ActiveThread {
|
||||
});
|
||||
|
||||
// For all items that should be aligned with the LLM's response.
|
||||
const RESPONSE_PADDING_X: Pixels = px(19.);
|
||||
const RESPONSE_PADDING_X: Pixels = px(18.);
|
||||
|
||||
let feedback_container = h_flex()
|
||||
.group("feedback_container")
|
||||
.mt_1()
|
||||
.py_2()
|
||||
.px(RESPONSE_PADDING_X)
|
||||
.gap_1()
|
||||
@@ -1698,7 +1665,7 @@ impl ActiveThread {
|
||||
if let Some(edit_message_editor) = edit_message_editor.clone() {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let font_size = TextSize::Small.rems(cx);
|
||||
let line_height = font_size.to_pixels(window.rem_size()) * 1.75;
|
||||
let line_height = font_size.to_pixels(window.rem_size()) * 1.5;
|
||||
|
||||
let text_style = TextStyle {
|
||||
color: cx.theme().colors().text,
|
||||
@@ -1746,7 +1713,7 @@ impl ActiveThread {
|
||||
.when(!added_context.is_empty(), |parent| {
|
||||
parent.child(h_flex().flex_wrap().gap_1().children(
|
||||
added_context.into_iter().map(|added_context| {
|
||||
let context = added_context.handle.clone();
|
||||
let context = added_context.context.clone();
|
||||
ContextPill::added(added_context, false, false, None).on_click(Rc::new(
|
||||
cx.listener({
|
||||
let workspace = workspace.clone();
|
||||
@@ -1782,7 +1749,8 @@ impl ActiveThread {
|
||||
.cursor_pointer()
|
||||
.child(
|
||||
h_flex()
|
||||
.p_2p5()
|
||||
.p_2()
|
||||
.pt_3()
|
||||
.gap_1()
|
||||
.children(message_content)
|
||||
.when_some(edit_message_editor.clone(), |this, edit_editor| {
|
||||
@@ -1952,7 +1920,7 @@ impl ActiveThread {
|
||||
parent.child(self.render_rules_item(cx))
|
||||
})
|
||||
.child(styled_message)
|
||||
.when(generating_label.is_some(), |this| {
|
||||
.when(!needs_confirmation && generating_label.is_some(), |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.h_8()
|
||||
@@ -3220,13 +3188,13 @@ impl Render for ActiveThread {
|
||||
}
|
||||
|
||||
pub(crate) fn open_context(
|
||||
context: &AgentContextHandle,
|
||||
context: &AgentContext,
|
||||
workspace: Entity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
match context {
|
||||
AgentContextHandle::File(file_context) => {
|
||||
AgentContext::File(file_context) => {
|
||||
if let Some(project_path) = file_context.project_path(cx) {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
@@ -3236,7 +3204,7 @@ pub(crate) fn open_context(
|
||||
}
|
||||
}
|
||||
|
||||
AgentContextHandle::Directory(directory_context) => {
|
||||
AgentContext::Directory(directory_context) => {
|
||||
let entry_id = directory_context.entry_id;
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.project().update(cx, |_project, cx| {
|
||||
@@ -3245,7 +3213,7 @@ pub(crate) fn open_context(
|
||||
})
|
||||
}
|
||||
|
||||
AgentContextHandle::Symbol(symbol_context) => {
|
||||
AgentContext::Symbol(symbol_context) => {
|
||||
let buffer = symbol_context.buffer.read(cx);
|
||||
if let Some(project_path) = buffer.project_path(cx) {
|
||||
let snapshot = buffer.snapshot();
|
||||
@@ -3255,7 +3223,7 @@ pub(crate) fn open_context(
|
||||
}
|
||||
}
|
||||
|
||||
AgentContextHandle::Selection(selection_context) => {
|
||||
AgentContext::Selection(selection_context) => {
|
||||
let buffer = selection_context.buffer.read(cx);
|
||||
if let Some(project_path) = buffer.project_path(cx) {
|
||||
let snapshot = buffer.snapshot();
|
||||
@@ -3266,29 +3234,29 @@ pub(crate) fn open_context(
|
||||
}
|
||||
}
|
||||
|
||||
AgentContextHandle::FetchedUrl(fetched_url_context) => {
|
||||
AgentContext::FetchedUrl(fetched_url_context) => {
|
||||
cx.open_url(&fetched_url_context.url);
|
||||
}
|
||||
|
||||
AgentContextHandle::Thread(thread_context) => workspace.update(cx, |workspace, cx| {
|
||||
AgentContext::Thread(thread_context) => workspace.update(cx, |workspace, cx| {
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
panel.update(cx, |panel, cx| {
|
||||
let thread_id = thread_context.thread.read(cx).id().clone();
|
||||
panel
|
||||
.open_thread_by_id(&thread_id, window, cx)
|
||||
.open_thread(&thread_id, window, cx)
|
||||
.detach_and_log_err(cx)
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
AgentContextHandle::Rules(rules_context) => window.dispatch_action(
|
||||
AgentContext::Rules(rules_context) => window.dispatch_action(
|
||||
Box::new(OpenRulesLibrary {
|
||||
prompt_to_select: Some(rules_context.prompt_id.0),
|
||||
}),
|
||||
cx,
|
||||
),
|
||||
|
||||
AgentContextHandle::Image(_) => {}
|
||||
AgentContext::Image(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -951,7 +951,6 @@ mod tests {
|
||||
ThemeSettings::register(cx);
|
||||
ContextServerSettings::register(cx);
|
||||
EditorSettings::register(cx);
|
||||
language_model::init_settings(cx);
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
|
||||
@@ -3,6 +3,7 @@ mod agent_diff;
|
||||
mod assistant_configuration;
|
||||
mod assistant_model_selector;
|
||||
mod assistant_panel;
|
||||
pub mod batch_assist;
|
||||
mod buffer_codegen;
|
||||
mod context;
|
||||
mod context_picker;
|
||||
@@ -41,7 +42,7 @@ use crate::assistant_configuration::{AddContextServerModal, ManageProfilesModal}
|
||||
pub use crate::assistant_panel::{AssistantPanel, ConcreteAssistantPanelDelegate};
|
||||
pub use crate::context::{ContextLoadResult, LoadedContext};
|
||||
pub use crate::inline_assistant::InlineAssistant;
|
||||
pub use crate::thread::{Message, MessageSegment, Thread, ThreadEvent};
|
||||
pub use crate::thread::{Message, Thread, ThreadEvent};
|
||||
pub use crate::thread_store::ThreadStore;
|
||||
pub use agent_diff::{AgentDiff, AgentDiffToolbar};
|
||||
|
||||
@@ -50,9 +51,6 @@ actions!(
|
||||
[
|
||||
NewTextThread,
|
||||
ToggleContextPicker,
|
||||
ToggleNavigationMenu,
|
||||
ToggleOptionsMenu,
|
||||
DeleteRecentlyOpenThread,
|
||||
ToggleProfileSelector,
|
||||
RemoveAllContext,
|
||||
ExpandMessageEditor,
|
||||
|
||||
@@ -2,8 +2,6 @@ use assistant_settings::AssistantSettings;
|
||||
use fs::Fs;
|
||||
use gpui::{Entity, FocusHandle, SharedString};
|
||||
|
||||
use crate::Thread;
|
||||
use language_model::{ConfiguredModel, LanguageModelRegistry};
|
||||
use language_model_selector::{
|
||||
LanguageModelSelector, LanguageModelSelectorPopoverMenu, ToggleModelSelector,
|
||||
};
|
||||
@@ -11,11 +9,7 @@ use settings::update_settings_file;
|
||||
use std::sync::Arc;
|
||||
use ui::{ButtonLike, PopoverMenuHandle, Tooltip, prelude::*};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum ModelType {
|
||||
Default(Entity<Thread>),
|
||||
InlineAssistant,
|
||||
}
|
||||
pub use language_model_selector::ModelType;
|
||||
|
||||
pub struct AssistantModelSelector {
|
||||
selector: Entity<LanguageModelSelector>,
|
||||
@@ -30,39 +24,18 @@ impl AssistantModelSelector {
|
||||
focus_handle: FocusHandle,
|
||||
model_type: ModelType,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
cx: &mut App,
|
||||
) -> Self {
|
||||
Self {
|
||||
selector: cx.new(move |cx| {
|
||||
selector: cx.new(|cx| {
|
||||
let fs = fs.clone();
|
||||
LanguageModelSelector::new(
|
||||
{
|
||||
let model_type = model_type.clone();
|
||||
move |cx| match &model_type {
|
||||
ModelType::Default(thread) => thread.read(cx).configured_model(),
|
||||
ModelType::InlineAssistant => {
|
||||
LanguageModelRegistry::read_global(cx).inline_assistant_model()
|
||||
}
|
||||
}
|
||||
},
|
||||
move |model, cx| {
|
||||
let provider = model.provider_id().0.to_string();
|
||||
let model_id = model.id().0.to_string();
|
||||
match &model_type {
|
||||
ModelType::Default(thread) => {
|
||||
thread.update(cx, |thread, cx| {
|
||||
let registry = LanguageModelRegistry::read_global(cx);
|
||||
if let Some(provider) = registry.provider(&model.provider_id())
|
||||
{
|
||||
thread.set_configured_model(
|
||||
Some(ConfiguredModel {
|
||||
provider,
|
||||
model: model.clone(),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
match model_type {
|
||||
ModelType::Default => {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
@@ -85,6 +58,7 @@ impl AssistantModelSelector {
|
||||
}
|
||||
}
|
||||
},
|
||||
model_type,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::ops::Range;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -18,8 +18,8 @@ use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem,
|
||||
Corner, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext,
|
||||
Pixels, Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between,
|
||||
Corner, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext, Pixels,
|
||||
Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::{LanguageModelProviderTosView, LanguageModelRegistry};
|
||||
@@ -41,16 +41,15 @@ use zed_actions::assistant::{OpenRulesLibrary, ToggleFocus};
|
||||
|
||||
use crate::active_thread::{ActiveThread, ActiveThreadEvent};
|
||||
use crate::assistant_configuration::{AssistantConfiguration, AssistantConfigurationEvent};
|
||||
use crate::history_store::{HistoryEntry, HistoryStore, RecentEntry};
|
||||
use crate::history_store::{HistoryEntry, HistoryStore};
|
||||
use crate::message_editor::{MessageEditor, MessageEditorEvent};
|
||||
use crate::thread::{Thread, ThreadError, ThreadId, TokenUsageRatio};
|
||||
use crate::thread_history::{PastContext, PastThread, ThreadHistory};
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::ui::UsageBanner;
|
||||
use crate::{
|
||||
AddContextServer, AgentDiff, DeleteRecentlyOpenThread, ExpandMessageEditor, InlineAssistant,
|
||||
NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ThreadEvent,
|
||||
ToggleContextPicker, ToggleNavigationMenu, ToggleOptionsMenu,
|
||||
AddContextServer, AgentDiff, ExpandMessageEditor, InlineAssistant, NewTextThread, NewThread,
|
||||
OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ThreadEvent, ToggleContextPicker,
|
||||
};
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
@@ -105,22 +104,6 @@ pub fn init(cx: &mut App) {
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &ToggleNavigationMenu, window, cx| {
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
workspace.focus_panel::<AssistantPanel>(window, cx);
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.toggle_navigation_menu(&ToggleNavigationMenu, window, cx);
|
||||
});
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &ToggleOptionsMenu, window, cx| {
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
workspace.focus_panel::<AssistantPanel>(window, cx);
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.toggle_options_menu(&ToggleOptionsMenu, window, cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
)
|
||||
@@ -130,7 +113,6 @@ pub fn init(cx: &mut App) {
|
||||
enum ActiveView {
|
||||
Thread {
|
||||
change_title_editor: Entity<Editor>,
|
||||
thread: WeakEntity<Thread>,
|
||||
_subscriptions: Vec<gpui::Subscription>,
|
||||
},
|
||||
PromptEditor {
|
||||
@@ -148,7 +130,7 @@ impl ActiveView {
|
||||
|
||||
let editor = cx.new(|cx| {
|
||||
let mut editor = Editor::single_line(window, cx);
|
||||
editor.set_text(summary.clone(), window, cx);
|
||||
editor.set_text(summary, window, cx);
|
||||
editor
|
||||
});
|
||||
|
||||
@@ -194,7 +176,6 @@ impl ActiveView {
|
||||
|
||||
Self::Thread {
|
||||
change_title_editor: editor,
|
||||
thread: thread.downgrade(),
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
@@ -287,7 +268,6 @@ pub struct AssistantPanel {
|
||||
thread: Entity<ActiveThread>,
|
||||
message_editor: Entity<MessageEditor>,
|
||||
_active_thread_subscriptions: Vec<Subscription>,
|
||||
_default_model_subscription: Subscription,
|
||||
context_store: Entity<assistant_context_editor::ContextStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
configuration: Option<Entity<AssistantConfiguration>>,
|
||||
@@ -298,8 +278,6 @@ pub struct AssistantPanel {
|
||||
history_store: Entity<HistoryStore>,
|
||||
history: Entity<ThreadHistory>,
|
||||
assistant_dropdown_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
assistant_navigation_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
assistant_navigation_menu: Option<Entity<ContextMenu>>,
|
||||
width: Option<Pixels>,
|
||||
height: Option<Pixels>,
|
||||
}
|
||||
@@ -401,14 +379,8 @@ impl AssistantPanel {
|
||||
}
|
||||
});
|
||||
|
||||
let history_store = cx.new(|cx| {
|
||||
HistoryStore::new(
|
||||
thread_store.clone(),
|
||||
context_store.clone(),
|
||||
[RecentEntry::Thread(thread.clone())],
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let history_store =
|
||||
cx.new(|cx| HistoryStore::new(thread_store.clone(), context_store.clone(), cx));
|
||||
|
||||
cx.observe(&history_store, |_, _, cx| cx.notify()).detach();
|
||||
|
||||
@@ -419,7 +391,7 @@ impl AssistantPanel {
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
let active_thread = cx.new(|cx| {
|
||||
let thread = cx.new(|cx| {
|
||||
ActiveThread::new(
|
||||
thread.clone(),
|
||||
thread_store.clone(),
|
||||
@@ -430,127 +402,12 @@ impl AssistantPanel {
|
||||
)
|
||||
});
|
||||
|
||||
let active_thread_subscription =
|
||||
cx.subscribe(&active_thread, |_, _, event, cx| match &event {
|
||||
ActiveThreadEvent::EditingMessageTokenCountChanged => {
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
let weak_panel = weak_self.clone();
|
||||
|
||||
window.defer(cx, move |window, cx| {
|
||||
let panel = weak_panel.clone();
|
||||
let assistant_navigation_menu =
|
||||
ContextMenu::build_persistent(window, cx, move |mut menu, _window, cx| {
|
||||
let recently_opened = panel
|
||||
.update(cx, |this, cx| {
|
||||
this.history_store.update(cx, |history_store, cx| {
|
||||
history_store.recently_opened_entries(cx)
|
||||
})
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
if !recently_opened.is_empty() {
|
||||
menu = menu.header("Recently Opened");
|
||||
|
||||
for entry in recently_opened.iter() {
|
||||
let summary = entry.summary(cx);
|
||||
menu = menu.entry_with_end_slot(
|
||||
summary,
|
||||
None,
|
||||
{
|
||||
let panel = panel.clone();
|
||||
let entry = entry.clone();
|
||||
move |window, cx| {
|
||||
panel
|
||||
.update(cx, {
|
||||
let entry = entry.clone();
|
||||
move |this, cx| match entry {
|
||||
RecentEntry::Thread(thread) => {
|
||||
this.open_thread(thread, window, cx)
|
||||
}
|
||||
RecentEntry::Context(context) => {
|
||||
let Some(path) = context.read(cx).path()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
this.open_saved_prompt_editor(
|
||||
path.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx)
|
||||
}
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
},
|
||||
IconName::Close,
|
||||
"Close Entry".into(),
|
||||
{
|
||||
let panel = panel.clone();
|
||||
let entry = entry.clone();
|
||||
move |_window, cx| {
|
||||
panel
|
||||
.update(cx, |this, cx| {
|
||||
this.history_store.update(
|
||||
cx,
|
||||
|history_store, cx| {
|
||||
history_store.remove_recently_opened_entry(
|
||||
&entry, cx,
|
||||
);
|
||||
},
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
menu = menu.separator();
|
||||
}
|
||||
|
||||
menu.action("View All", Box::new(OpenHistory))
|
||||
.end_slot_action(DeleteRecentlyOpenThread.boxed_clone())
|
||||
.fixed_width(px(320.).into())
|
||||
.keep_open_on_confirm(false)
|
||||
.key_context("NavigationMenu")
|
||||
});
|
||||
weak_panel
|
||||
.update(cx, |panel, cx| {
|
||||
cx.subscribe_in(
|
||||
&assistant_navigation_menu,
|
||||
window,
|
||||
|_, menu, _: &DismissEvent, window, cx| {
|
||||
menu.update(cx, |menu, _| {
|
||||
menu.clear_selected();
|
||||
});
|
||||
cx.focus_self(window);
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
panel.assistant_navigation_menu = Some(assistant_navigation_menu);
|
||||
})
|
||||
.ok();
|
||||
let active_thread_subscription = cx.subscribe(&thread, |_, _, event, cx| match &event {
|
||||
ActiveThreadEvent::EditingMessageTokenCountChanged => {
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
let _default_model_subscription = cx.subscribe(
|
||||
&LanguageModelRegistry::global(cx),
|
||||
|this, _, event: &language_model::Event, cx| match event {
|
||||
language_model::Event::DefaultModelChanged => {
|
||||
this.thread
|
||||
.read(cx)
|
||||
.thread()
|
||||
.clone()
|
||||
.update(cx, |thread, cx| thread.get_or_init_configured_model(cx));
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
);
|
||||
|
||||
Self {
|
||||
active_view,
|
||||
workspace,
|
||||
@@ -559,14 +416,13 @@ impl AssistantPanel {
|
||||
fs: fs.clone(),
|
||||
language_registry,
|
||||
thread_store: thread_store.clone(),
|
||||
thread: active_thread,
|
||||
thread,
|
||||
message_editor,
|
||||
_active_thread_subscriptions: vec![
|
||||
thread_subscription,
|
||||
active_thread_subscription,
|
||||
message_editor_subscription,
|
||||
],
|
||||
_default_model_subscription,
|
||||
context_store,
|
||||
prompt_store,
|
||||
configuration: None,
|
||||
@@ -579,8 +435,6 @@ impl AssistantPanel {
|
||||
history_store: history_store.clone(),
|
||||
history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, window, cx)),
|
||||
assistant_dropdown_menu_handle: PopoverMenuHandle::default(),
|
||||
assistant_navigation_menu_handle: PopoverMenuHandle::default(),
|
||||
assistant_navigation_menu: None,
|
||||
width: None,
|
||||
height: None,
|
||||
}
|
||||
@@ -775,13 +629,13 @@ impl AssistantPanel {
|
||||
|
||||
pub(crate) fn open_saved_prompt_editor(
|
||||
&mut self,
|
||||
path: Arc<Path>,
|
||||
path: PathBuf,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let context = self
|
||||
.context_store
|
||||
.update(cx, |store, cx| store.open_local_context(path, cx));
|
||||
.update(cx, |store, cx| store.open_local_context(path.clone(), cx));
|
||||
let fs = self.fs.clone();
|
||||
let project = self.project.clone();
|
||||
let workspace = self.workspace.clone();
|
||||
@@ -815,7 +669,7 @@ impl AssistantPanel {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn open_thread_by_id(
|
||||
pub(crate) fn open_thread(
|
||||
&mut self,
|
||||
thread_id: &ThreadId,
|
||||
window: &mut Window,
|
||||
@@ -824,83 +678,73 @@ impl AssistantPanel {
|
||||
let open_thread_task = self
|
||||
.thread_store
|
||||
.update(cx, |this, cx| this.open_thread(thread_id, cx));
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let thread = open_thread_task.await?;
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.open_thread(thread, window, cx);
|
||||
anyhow::Ok(())
|
||||
})??;
|
||||
Ok(())
|
||||
let thread_view = ActiveView::thread(thread.clone(), window, cx);
|
||||
this.set_active_view(thread_view, window, cx);
|
||||
let message_editor_context_store = cx.new(|_cx| {
|
||||
crate::context_store::ContextStore::new(
|
||||
this.project.downgrade(),
|
||||
Some(this.thread_store.downgrade()),
|
||||
)
|
||||
});
|
||||
let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
|
||||
if let ThreadEvent::MessageAdded(_) = &event {
|
||||
// needed to leave empty state
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
this.thread = cx.new(|cx| {
|
||||
ActiveThread::new(
|
||||
thread.clone(),
|
||||
this.thread_store.clone(),
|
||||
this.language_registry.clone(),
|
||||
this.workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let active_thread_subscription =
|
||||
cx.subscribe(&this.thread, |_, _, event, cx| match &event {
|
||||
ActiveThreadEvent::EditingMessageTokenCountChanged => {
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
this.message_editor = cx.new(|cx| {
|
||||
MessageEditor::new(
|
||||
this.fs.clone(),
|
||||
this.workspace.clone(),
|
||||
message_editor_context_store,
|
||||
this.prompt_store.clone(),
|
||||
this.thread_store.downgrade(),
|
||||
thread,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
this.message_editor.focus_handle(cx).focus(window);
|
||||
|
||||
let message_editor_subscription =
|
||||
cx.subscribe(&this.message_editor, |_, _, event, cx| match event {
|
||||
MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
this._active_thread_subscriptions = vec![
|
||||
thread_subscription,
|
||||
active_thread_subscription,
|
||||
message_editor_subscription,
|
||||
];
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn open_thread(
|
||||
&mut self,
|
||||
thread: Entity<Thread>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let thread_view = ActiveView::thread(thread.clone(), window, cx);
|
||||
self.set_active_view(thread_view, window, cx);
|
||||
let message_editor_context_store = cx.new(|_cx| {
|
||||
crate::context_store::ContextStore::new(
|
||||
self.project.downgrade(),
|
||||
Some(self.thread_store.downgrade()),
|
||||
)
|
||||
});
|
||||
let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
|
||||
if let ThreadEvent::MessageAdded(_) = &event {
|
||||
// needed to leave empty state
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
self.thread = cx.new(|cx| {
|
||||
ActiveThread::new(
|
||||
thread.clone(),
|
||||
self.thread_store.clone(),
|
||||
self.language_registry.clone(),
|
||||
self.workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let active_thread_subscription =
|
||||
cx.subscribe(&self.thread, |_, _, event, cx| match &event {
|
||||
ActiveThreadEvent::EditingMessageTokenCountChanged => {
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
self.message_editor = cx.new(|cx| {
|
||||
MessageEditor::new(
|
||||
self.fs.clone(),
|
||||
self.workspace.clone(),
|
||||
message_editor_context_store,
|
||||
self.prompt_store.clone(),
|
||||
self.thread_store.downgrade(),
|
||||
thread,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
self.message_editor.focus_handle(cx).focus(window);
|
||||
|
||||
let message_editor_subscription =
|
||||
cx.subscribe(&self.message_editor, |_, _, event, cx| match event {
|
||||
MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
self._active_thread_subscriptions = vec![
|
||||
thread_subscription,
|
||||
active_thread_subscription,
|
||||
message_editor_subscription,
|
||||
];
|
||||
}
|
||||
|
||||
pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context<Self>) {
|
||||
match self.active_view {
|
||||
ActiveView::Configuration | ActiveView::History => {
|
||||
@@ -913,24 +757,6 @@ impl AssistantPanel {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_navigation_menu(
|
||||
&mut self,
|
||||
_: &ToggleNavigationMenu,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.assistant_navigation_menu_handle.toggle(window, cx);
|
||||
}
|
||||
|
||||
pub fn toggle_options_menu(
|
||||
&mut self,
|
||||
_: &ToggleOptionsMenu,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.assistant_dropdown_menu_handle.toggle(window, cx);
|
||||
}
|
||||
|
||||
pub fn open_agent_diff(
|
||||
&mut self,
|
||||
_: &OpenAgentDiff,
|
||||
@@ -1079,7 +905,7 @@ impl AssistantPanel {
|
||||
|
||||
pub(crate) fn delete_context(
|
||||
&mut self,
|
||||
path: Arc<Path>,
|
||||
path: PathBuf,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.context_store
|
||||
@@ -1095,32 +921,6 @@ impl AssistantPanel {
|
||||
let current_is_history = matches!(self.active_view, ActiveView::History);
|
||||
let new_is_history = matches!(new_view, ActiveView::History);
|
||||
|
||||
match &self.active_view {
|
||||
ActiveView::Thread { thread, .. } => self.history_store.update(cx, |store, cx| {
|
||||
if let Some(thread) = thread.upgrade() {
|
||||
if thread.read(cx).is_empty() {
|
||||
store.remove_recently_opened_entry(&RecentEntry::Thread(thread), cx);
|
||||
}
|
||||
}
|
||||
}),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
match &new_view {
|
||||
ActiveView::Thread { thread, .. } => self.history_store.update(cx, |store, cx| {
|
||||
if let Some(thread) = thread.upgrade() {
|
||||
store.push_recently_opened_entry(RecentEntry::Thread(thread), cx);
|
||||
}
|
||||
}),
|
||||
ActiveView::PromptEditor { context_editor, .. } => {
|
||||
self.history_store.update(cx, |store, cx| {
|
||||
let context = context_editor.read(cx).context().clone();
|
||||
store.push_recently_opened_entry(RecentEntry::Context(context), cx)
|
||||
})
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if current_is_history && !new_is_history {
|
||||
self.active_view = new_view;
|
||||
} else if !current_is_history && new_is_history {
|
||||
@@ -1250,13 +1050,16 @@ impl AssistantPanel {
|
||||
if is_empty {
|
||||
Label::new(Thread::DEFAULT_SUMMARY.clone())
|
||||
.truncate()
|
||||
.ml_2()
|
||||
.into_any_element()
|
||||
} else if summary.is_none() {
|
||||
Label::new(LOADING_SUMMARY_PLACEHOLDER)
|
||||
.ml_2()
|
||||
.truncate()
|
||||
.into_any_element()
|
||||
} else {
|
||||
div()
|
||||
.ml_2()
|
||||
.w_full()
|
||||
.child(change_title_editor.clone())
|
||||
.into_any_element()
|
||||
@@ -1273,15 +1076,18 @@ impl AssistantPanel {
|
||||
match summary {
|
||||
None => Label::new(AssistantContext::DEFAULT_SUMMARY.clone())
|
||||
.truncate()
|
||||
.ml_2()
|
||||
.into_any_element(),
|
||||
Some(summary) => {
|
||||
if summary.done {
|
||||
div()
|
||||
.ml_2()
|
||||
.w_full()
|
||||
.child(title_editor.clone())
|
||||
.into_any_element()
|
||||
} else {
|
||||
Label::new(LOADING_SUMMARY_PLACEHOLDER)
|
||||
.ml_2()
|
||||
.truncate()
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -1308,6 +1114,7 @@ impl AssistantPanel {
|
||||
let thread = active_thread.thread().read(cx);
|
||||
let thread_id = thread.id().clone();
|
||||
let is_empty = active_thread.is_empty();
|
||||
let is_history = matches!(self.active_view, ActiveView::History);
|
||||
|
||||
let show_token_count = match &self.active_view {
|
||||
ActiveView::Thread { .. } => !is_empty,
|
||||
@@ -1317,108 +1124,30 @@ impl AssistantPanel {
|
||||
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
|
||||
let go_back_button = div().child(
|
||||
IconButton::new("go-back", IconName::ArrowLeft)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.go_back(&workspace::GoBack, window, cx);
|
||||
}))
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Go Back",
|
||||
&workspace::GoBack,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
let recent_entries_menu = div().child(
|
||||
PopoverMenu::new("agent-nav-menu")
|
||||
.trigger_with_tooltip(
|
||||
IconButton::new("agent-nav-menu", IconName::MenuAlt)
|
||||
let go_back_button = match &self.active_view {
|
||||
ActiveView::History | ActiveView::Configuration => Some(
|
||||
div().pl_1().child(
|
||||
IconButton::new("go-back", IconName::ArrowLeft)
|
||||
.icon_size(IconSize::Small)
|
||||
.style(ui::ButtonStyle::Subtle),
|
||||
{
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Toggle Panel Menu",
|
||||
&ToggleNavigationMenu,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
.anchor(Corner::TopLeft)
|
||||
.with_handle(self.assistant_navigation_menu_handle.clone())
|
||||
.menu({
|
||||
let menu = self.assistant_navigation_menu.clone();
|
||||
move |window, cx| {
|
||||
if let Some(menu) = menu.as_ref() {
|
||||
menu.update(cx, |_, cx| {
|
||||
cx.defer_in(window, |menu, window, cx| {
|
||||
menu.rebuild(window, cx);
|
||||
});
|
||||
})
|
||||
}
|
||||
menu.clone()
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
let agent_extra_menu = PopoverMenu::new("agent-options-menu")
|
||||
.trigger_with_tooltip(
|
||||
IconButton::new("agent-options-menu", IconName::Ellipsis)
|
||||
.icon_size(IconSize::Small),
|
||||
{
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Toggle Agent Menu",
|
||||
&ToggleOptionsMenu,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
.anchor(Corner::TopRight)
|
||||
.with_handle(self.assistant_dropdown_menu_handle.clone())
|
||||
.menu(move |window, cx| {
|
||||
Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
|
||||
menu.when(!is_empty, |menu| {
|
||||
menu.action(
|
||||
"Start New From Summary",
|
||||
Box::new(NewThread {
|
||||
from_thread_id: Some(thread_id.clone()),
|
||||
}),
|
||||
)
|
||||
.separator()
|
||||
})
|
||||
.action("New Text Thread", NewTextThread.boxed_clone())
|
||||
.action("Rules Library", Box::new(OpenRulesLibrary::default()))
|
||||
.action("Settings", Box::new(OpenConfiguration))
|
||||
.separator()
|
||||
.header("MCPs")
|
||||
.action(
|
||||
"View Server Extensions",
|
||||
Box::new(zed_actions::Extensions {
|
||||
category_filter: Some(
|
||||
zed_actions::ExtensionCategoryFilter::ContextServers,
|
||||
),
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.go_back(&workspace::GoBack, window, cx);
|
||||
}))
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Go Back",
|
||||
&workspace::GoBack,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
.action("Add Custom Server", Box::new(AddContextServer))
|
||||
}))
|
||||
});
|
||||
),
|
||||
),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.id("assistant-toolbar")
|
||||
@@ -1432,22 +1161,18 @@ impl AssistantPanel {
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
h_flex()
|
||||
.size_full()
|
||||
.pl_1()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.child(match &self.active_view {
|
||||
ActiveView::History | ActiveView::Configuration => go_back_button,
|
||||
_ => recent_entries_menu,
|
||||
})
|
||||
.children(go_back_button)
|
||||
.child(self.render_title_view(window, cx)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.h_full()
|
||||
.gap_2()
|
||||
.when(show_token_count, |parent| {
|
||||
.when(show_token_count, |parent|
|
||||
parent.children(self.render_token_count(&thread, cx))
|
||||
})
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.h_full()
|
||||
@@ -1475,7 +1200,72 @@ impl AssistantPanel {
|
||||
);
|
||||
}),
|
||||
)
|
||||
.child(agent_extra_menu),
|
||||
.child(
|
||||
IconButton::new("open-history", IconName::HistoryRerun)
|
||||
.icon_size(IconSize::Small)
|
||||
.toggle_state(is_history)
|
||||
.selected_icon_color(Color::Accent)
|
||||
.tooltip({
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"History",
|
||||
&OpenHistory,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(move |_event, window, cx| {
|
||||
window.dispatch_action(OpenHistory.boxed_clone(), cx);
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
PopoverMenu::new("assistant-menu")
|
||||
.trigger_with_tooltip(
|
||||
IconButton::new("new", IconName::Ellipsis)
|
||||
.icon_size(IconSize::Small)
|
||||
.style(ButtonStyle::Subtle),
|
||||
Tooltip::text("Toggle Agent Menu"),
|
||||
)
|
||||
.anchor(Corner::TopRight)
|
||||
.with_handle(self.assistant_dropdown_menu_handle.clone())
|
||||
.menu(move |window, cx| {
|
||||
Some(ContextMenu::build(
|
||||
window,
|
||||
cx,
|
||||
|menu, _window, _cx| {
|
||||
menu
|
||||
.when(!is_empty, |menu| {
|
||||
menu.action(
|
||||
"Start New From Summary",
|
||||
Box::new(NewThread {
|
||||
from_thread_id: Some(thread_id.clone()),
|
||||
}),
|
||||
).separator()
|
||||
})
|
||||
.action(
|
||||
"New Text Thread",
|
||||
NewTextThread.boxed_clone(),
|
||||
)
|
||||
.action("Rules Library", Box::new(OpenRulesLibrary::default()))
|
||||
.action("Settings", Box::new(OpenConfiguration))
|
||||
.separator()
|
||||
.header("MCPs")
|
||||
.action(
|
||||
"View Server Extensions",
|
||||
Box::new(zed_actions::Extensions {
|
||||
category_filter: Some(
|
||||
zed_actions::ExtensionCategoryFilter::ContextServers,
|
||||
),
|
||||
}),
|
||||
)
|
||||
.action("Add Custom Server", Box::new(AddContextServer))
|
||||
},
|
||||
))
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -1484,13 +1274,12 @@ impl AssistantPanel {
|
||||
let is_generating = thread.is_generating();
|
||||
let message_editor = self.message_editor.read(cx);
|
||||
|
||||
let conversation_token_usage = thread.total_token_usage()?;
|
||||
|
||||
let conversation_token_usage = thread.total_token_usage(cx);
|
||||
let (total_token_usage, is_estimating) = if let Some((editing_message_id, unsent_tokens)) =
|
||||
self.thread.read(cx).editing_message_id()
|
||||
{
|
||||
let combined = thread
|
||||
.token_usage_up_to_message(editing_message_id)
|
||||
.token_usage_up_to_message(editing_message_id, cx)
|
||||
.add(unsent_tokens);
|
||||
|
||||
(combined, unsent_tokens > 0)
|
||||
@@ -2176,8 +1965,6 @@ impl Render for AssistantPanel {
|
||||
.on_action(cx.listener(Self::deploy_rules_library))
|
||||
.on_action(cx.listener(Self::open_agent_diff))
|
||||
.on_action(cx.listener(Self::go_back))
|
||||
.on_action(cx.listener(Self::toggle_navigation_menu))
|
||||
.on_action(cx.listener(Self::toggle_options_menu))
|
||||
.child(self.render_toolbar(window, cx))
|
||||
.map(|parent| match &self.active_view {
|
||||
ActiveView::Thread { .. } => parent
|
||||
@@ -2262,7 +2049,7 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
|
||||
fn open_saved_context(
|
||||
&self,
|
||||
workspace: &mut Workspace,
|
||||
path: Arc<Path>,
|
||||
path: std::path::PathBuf,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> Task<Result<()>> {
|
||||
|
||||
64
crates/agent/src/batch_assist.rs
Normal file
64
crates/agent/src/batch_assist.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
use editor::Editor;
|
||||
use gpui::{Entity, EventEmitter};
|
||||
use ui::{Render, Styled, div, px};
|
||||
use workspace::{ToolbarItemEvent, ToolbarItemLocation};
|
||||
|
||||
use crate::{buffer_codegen::BufferCodegen, inline_prompt_editor::PromptEditor};
|
||||
|
||||
pub struct BatchAssistToolbarItem {
|
||||
visible: bool,
|
||||
assist: Option<Entity<BatchCodegen>>,
|
||||
active_editor: Option<Entity<Editor>>,
|
||||
// prompt_editor: Entity<PromptEditor>,
|
||||
}
|
||||
|
||||
impl BatchAssistToolbarItem {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
visible: false,
|
||||
assist: None,
|
||||
active_editor: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deploy(&mut self, cx: &mut ui::Context<Self>) {
|
||||
self.visible = true;
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<ToolbarItemEvent> for BatchAssistToolbarItem {}
|
||||
|
||||
impl workspace::ToolbarItemView for BatchAssistToolbarItem {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn workspace::ItemHandle>,
|
||||
_window: &mut ui::Window,
|
||||
cx: &mut ui::Context<Self>,
|
||||
) -> workspace::ToolbarItemLocation {
|
||||
if let Some(editor) = active_pane_item.and_then(|item| item.act_as::<Editor>(cx)) {
|
||||
self.active_editor = Some(editor);
|
||||
ToolbarItemLocation::Secondary
|
||||
} else {
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for BatchAssistToolbarItem {
|
||||
fn render(
|
||||
&mut self,
|
||||
window: &mut ui::Window,
|
||||
cx: &mut ui::Context<Self>,
|
||||
) -> impl ui::IntoElement {
|
||||
dbg!(&self.active_editor);
|
||||
if self.visible {
|
||||
div().bg(gpui::red()).w(px(100.)).h(px(100.))
|
||||
} else {
|
||||
div()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BatchCodegen {
|
||||
codegens: Vec<Entity<BufferCodegen>>,
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -267,7 +267,7 @@ impl ContextPicker {
|
||||
context_picker.update(cx, |this, cx| this.select_entry(entry, window, cx))
|
||||
})
|
||||
}))
|
||||
.keep_open_on_confirm(true)
|
||||
.keep_open_on_confirm()
|
||||
});
|
||||
|
||||
cx.subscribe(&menu, move |_, _, _: &DismissEvent, cx| {
|
||||
|
||||
@@ -4,21 +4,21 @@ use std::sync::Arc;
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use collections::{HashSet, IndexSet};
|
||||
use futures::future::join_all;
|
||||
use futures::{self, FutureExt};
|
||||
use gpui::{App, Context, Entity, Image, SharedString, Task, WeakEntity};
|
||||
use language::Buffer;
|
||||
use language_model::LanguageModelImage;
|
||||
use project::image_store::is_image_file;
|
||||
use project::{Project, ProjectItem, ProjectPath, Symbol};
|
||||
use prompt_store::UserPromptId;
|
||||
use ref_cast::RefCast as _;
|
||||
use text::{Anchor, OffsetRangeExt};
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::ThreadStore;
|
||||
use crate::context::{
|
||||
AgentContextHandle, AgentContextKey, ContextId, DirectoryContextHandle, FetchedUrlContext,
|
||||
FileContextHandle, ImageContext, RulesContextHandle, SelectionContextHandle,
|
||||
SymbolContextHandle, ThreadContextHandle,
|
||||
AgentContext, AgentContextKey, ContextId, DirectoryContext, FetchedUrlContext, FileContext,
|
||||
ImageContext, RulesContext, SelectionContext, SymbolContext, ThreadContext,
|
||||
};
|
||||
use crate::context_strip::SuggestedContext;
|
||||
use crate::thread::{Thread, ThreadId};
|
||||
@@ -26,6 +26,7 @@ use crate::thread::{Thread, ThreadId};
|
||||
pub struct ContextStore {
|
||||
project: WeakEntity<Project>,
|
||||
thread_store: Option<WeakEntity<ThreadStore>>,
|
||||
thread_summary_tasks: Vec<Task<()>>,
|
||||
next_context_id: ContextId,
|
||||
context_set: IndexSet<AgentContextKey>,
|
||||
context_thread_ids: HashSet<ThreadId>,
|
||||
@@ -39,13 +40,14 @@ impl ContextStore {
|
||||
Self {
|
||||
project,
|
||||
thread_store,
|
||||
thread_summary_tasks: Vec::new(),
|
||||
next_context_id: ContextId::zero(),
|
||||
context_set: IndexSet::default(),
|
||||
context_thread_ids: HashSet::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn context(&self) -> impl Iterator<Item = &AgentContextHandle> {
|
||||
pub fn context(&self) -> impl Iterator<Item = &AgentContext> {
|
||||
self.context_set.iter().map(|entry| entry.as_ref())
|
||||
}
|
||||
|
||||
@@ -54,16 +56,11 @@ impl ContextStore {
|
||||
self.context_thread_ids.clear();
|
||||
}
|
||||
|
||||
pub fn new_context_for_thread(&self, thread: &Thread) -> Vec<AgentContextHandle> {
|
||||
pub fn new_context_for_thread(&self, thread: &Thread) -> Vec<AgentContext> {
|
||||
let existing_context = thread
|
||||
.messages()
|
||||
.flat_map(|message| {
|
||||
message
|
||||
.loaded_context
|
||||
.contexts
|
||||
.iter()
|
||||
.map(|context| AgentContextKey(context.handle()))
|
||||
})
|
||||
.flat_map(|message| &message.loaded_context.contexts)
|
||||
.map(AgentContextKey::ref_cast)
|
||||
.collect::<HashSet<_>>();
|
||||
self.context_set
|
||||
.iter()
|
||||
@@ -82,19 +79,15 @@ impl ContextStore {
|
||||
return Task::ready(Err(anyhow!("failed to read project")));
|
||||
};
|
||||
|
||||
if is_image_file(&project, &project_path, cx) {
|
||||
self.add_image_from_path(project_path, remove_if_exists, cx)
|
||||
} else {
|
||||
cx.spawn(async move |this, cx| {
|
||||
let open_buffer_task = project.update(cx, |project, cx| {
|
||||
project.open_buffer(project_path.clone(), cx)
|
||||
})?;
|
||||
let buffer = open_buffer_task.await?;
|
||||
this.update(cx, |this, cx| {
|
||||
this.add_file_from_buffer(&project_path, buffer, remove_if_exists, cx)
|
||||
})
|
||||
cx.spawn(async move |this, cx| {
|
||||
let open_buffer_task = project.update(cx, |project, cx| {
|
||||
project.open_buffer(project_path.clone(), cx)
|
||||
})?;
|
||||
let buffer = open_buffer_task.await?;
|
||||
this.update(cx, |this, cx| {
|
||||
this.add_file_from_buffer(&project_path, buffer, remove_if_exists, cx)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn add_file_from_buffer(
|
||||
@@ -105,7 +98,7 @@ impl ContextStore {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let context_id = self.next_context_id.post_inc();
|
||||
let context = AgentContextHandle::File(FileContextHandle { buffer, context_id });
|
||||
let context = AgentContext::File(FileContext { buffer, context_id });
|
||||
|
||||
let already_included = if self.has_context(&context) {
|
||||
if remove_if_exists {
|
||||
@@ -140,7 +133,7 @@ impl ContextStore {
|
||||
};
|
||||
|
||||
let context_id = self.next_context_id.post_inc();
|
||||
let context = AgentContextHandle::Directory(DirectoryContextHandle {
|
||||
let context = AgentContext::Directory(DirectoryContext {
|
||||
entry_id,
|
||||
context_id,
|
||||
});
|
||||
@@ -166,7 +159,7 @@ impl ContextStore {
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
let context_id = self.next_context_id.post_inc();
|
||||
let context = AgentContextHandle::Symbol(SymbolContextHandle {
|
||||
let context = AgentContext::Symbol(SymbolContext {
|
||||
buffer,
|
||||
symbol,
|
||||
range,
|
||||
@@ -191,7 +184,7 @@ impl ContextStore {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let context_id = self.next_context_id.post_inc();
|
||||
let context = AgentContextHandle::Thread(ThreadContextHandle { thread, context_id });
|
||||
let context = AgentContext::Thread(ThreadContext { thread, context_id });
|
||||
|
||||
if self.has_context(&context) {
|
||||
if remove_if_exists {
|
||||
@@ -202,6 +195,41 @@ impl ContextStore {
|
||||
}
|
||||
}
|
||||
|
||||
fn start_summarizing_thread_if_needed(
|
||||
&mut self,
|
||||
thread: &Entity<Thread>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(summary_task) =
|
||||
thread.update(cx, |thread, cx| thread.generate_detailed_summary(cx))
|
||||
{
|
||||
let thread = thread.clone();
|
||||
let thread_store = self.thread_store.clone();
|
||||
|
||||
self.thread_summary_tasks.push(cx.spawn(async move |_, cx| {
|
||||
summary_task.await;
|
||||
|
||||
if let Some(thread_store) = thread_store {
|
||||
// Save thread so its summary can be reused later
|
||||
let save_task = thread_store
|
||||
.update(cx, |thread_store, cx| thread_store.save_thread(&thread, cx));
|
||||
|
||||
if let Some(save_task) = save_task.ok() {
|
||||
save_task.await.log_err();
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn wait_for_summaries(&mut self, cx: &App) -> Task<()> {
|
||||
let tasks = std::mem::take(&mut self.thread_summary_tasks);
|
||||
|
||||
cx.spawn(async move |_cx| {
|
||||
join_all(tasks).await;
|
||||
})
|
||||
}
|
||||
|
||||
pub fn add_rules(
|
||||
&mut self,
|
||||
prompt_id: UserPromptId,
|
||||
@@ -209,7 +237,7 @@ impl ContextStore {
|
||||
cx: &mut Context<ContextStore>,
|
||||
) {
|
||||
let context_id = self.next_context_id.post_inc();
|
||||
let context = AgentContextHandle::Rules(RulesContextHandle {
|
||||
let context = AgentContext::Rules(RulesContext {
|
||||
prompt_id,
|
||||
context_id,
|
||||
});
|
||||
@@ -229,7 +257,7 @@ impl ContextStore {
|
||||
text: impl Into<SharedString>,
|
||||
cx: &mut Context<ContextStore>,
|
||||
) {
|
||||
let context = AgentContextHandle::FetchedUrl(FetchedUrlContext {
|
||||
let context = AgentContext::FetchedUrl(FetchedUrlContext {
|
||||
url: url.into(),
|
||||
text: text.into(),
|
||||
context_id: self.next_context_id.post_inc(),
|
||||
@@ -238,55 +266,13 @@ impl ContextStore {
|
||||
self.insert_context(context, cx);
|
||||
}
|
||||
|
||||
pub fn add_image_from_path(
|
||||
&mut self,
|
||||
project_path: ProjectPath,
|
||||
remove_if_exists: bool,
|
||||
cx: &mut Context<ContextStore>,
|
||||
) -> Task<Result<()>> {
|
||||
let project = self.project.clone();
|
||||
cx.spawn(async move |this, cx| {
|
||||
let open_image_task = project.update(cx, |project, cx| {
|
||||
project.open_image(project_path.clone(), cx)
|
||||
})?;
|
||||
let image_item = open_image_task.await?;
|
||||
let image = image_item.read_with(cx, |image_item, _| image_item.image.clone())?;
|
||||
this.update(cx, |this, cx| {
|
||||
this.insert_image(
|
||||
Some(image_item.read(cx).project_path(cx)),
|
||||
image,
|
||||
remove_if_exists,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn add_image_instance(&mut self, image: Arc<Image>, cx: &mut Context<ContextStore>) {
|
||||
self.insert_image(None, image, false, cx);
|
||||
}
|
||||
|
||||
fn insert_image(
|
||||
&mut self,
|
||||
project_path: Option<ProjectPath>,
|
||||
image: Arc<Image>,
|
||||
remove_if_exists: bool,
|
||||
cx: &mut Context<ContextStore>,
|
||||
) {
|
||||
pub fn add_image(&mut self, image: Arc<Image>, cx: &mut Context<ContextStore>) {
|
||||
let image_task = LanguageModelImage::from_image(image.clone(), cx).shared();
|
||||
let context = AgentContextHandle::Image(ImageContext {
|
||||
project_path,
|
||||
let context = AgentContext::Image(ImageContext {
|
||||
original_image: image,
|
||||
image_task,
|
||||
context_id: self.next_context_id.post_inc(),
|
||||
});
|
||||
if self.has_context(&context) {
|
||||
if remove_if_exists {
|
||||
self.remove_context(&context, cx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.insert_context(context, cx);
|
||||
}
|
||||
|
||||
@@ -297,7 +283,7 @@ impl ContextStore {
|
||||
cx: &mut Context<ContextStore>,
|
||||
) {
|
||||
let context_id = self.next_context_id.post_inc();
|
||||
let context = AgentContextHandle::Selection(SelectionContextHandle {
|
||||
let context = AgentContext::Selection(SelectionContext {
|
||||
buffer,
|
||||
range,
|
||||
context_id,
|
||||
@@ -318,17 +304,14 @@ impl ContextStore {
|
||||
} => {
|
||||
if let Some(buffer) = buffer.upgrade() {
|
||||
let context_id = self.next_context_id.post_inc();
|
||||
self.insert_context(
|
||||
AgentContextHandle::File(FileContextHandle { buffer, context_id }),
|
||||
cx,
|
||||
);
|
||||
self.insert_context(AgentContext::File(FileContext { buffer, context_id }), cx);
|
||||
};
|
||||
}
|
||||
SuggestedContext::Thread { thread, name: _ } => {
|
||||
if let Some(thread) = thread.upgrade() {
|
||||
let context_id = self.next_context_id.post_inc();
|
||||
self.insert_context(
|
||||
AgentContextHandle::Thread(ThreadContextHandle { thread, context_id }),
|
||||
AgentContext::Thread(ThreadContext { thread, context_id }),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
@@ -336,18 +319,12 @@ impl ContextStore {
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_context(&mut self, context: AgentContextHandle, cx: &mut Context<Self>) -> bool {
|
||||
fn insert_context(&mut self, context: AgentContext, cx: &mut Context<Self>) -> bool {
|
||||
match &context {
|
||||
AgentContextHandle::Thread(thread_context) => {
|
||||
if let Some(thread_store) = self.thread_store.clone() {
|
||||
thread_context.thread.update(cx, |thread, cx| {
|
||||
thread.start_generating_detailed_summary_if_needed(thread_store, cx);
|
||||
});
|
||||
self.context_thread_ids
|
||||
.insert(thread_context.thread.read(cx).id().clone());
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
AgentContext::Thread(thread_context) => {
|
||||
self.context_thread_ids
|
||||
.insert(thread_context.thread.read(cx).id().clone());
|
||||
self.start_summarizing_thread_if_needed(&thread_context.thread, cx);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -358,13 +335,13 @@ impl ContextStore {
|
||||
inserted
|
||||
}
|
||||
|
||||
pub fn remove_context(&mut self, context: &AgentContextHandle, cx: &mut Context<Self>) {
|
||||
pub fn remove_context(&mut self, context: &AgentContext, cx: &mut Context<Self>) {
|
||||
if self
|
||||
.context_set
|
||||
.shift_remove(AgentContextKey::ref_cast(context))
|
||||
{
|
||||
match context {
|
||||
AgentContextHandle::Thread(thread_context) => {
|
||||
AgentContext::Thread(thread_context) => {
|
||||
self.context_thread_ids
|
||||
.remove(thread_context.thread.read(cx).id());
|
||||
}
|
||||
@@ -374,7 +351,7 @@ impl ContextStore {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_context(&mut self, context: &AgentContextHandle) -> bool {
|
||||
pub fn has_context(&mut self, context: &AgentContext) -> bool {
|
||||
self.context_set
|
||||
.contains(AgentContextKey::ref_cast(context))
|
||||
}
|
||||
@@ -384,13 +361,8 @@ impl ContextStore {
|
||||
pub fn file_path_included(&self, path: &ProjectPath, cx: &App) -> Option<FileInclusion> {
|
||||
let project = self.project.upgrade()?.read(cx);
|
||||
self.context().find_map(|context| match context {
|
||||
AgentContextHandle::File(file_context) => {
|
||||
FileInclusion::check_file(file_context, path, cx)
|
||||
}
|
||||
AgentContextHandle::Image(image_context) => {
|
||||
FileInclusion::check_image(image_context, path)
|
||||
}
|
||||
AgentContextHandle::Directory(directory_context) => {
|
||||
AgentContext::File(file_context) => FileInclusion::check_file(file_context, path, cx),
|
||||
AgentContext::Directory(directory_context) => {
|
||||
FileInclusion::check_directory(directory_context, path, project, cx)
|
||||
}
|
||||
_ => None,
|
||||
@@ -404,7 +376,7 @@ impl ContextStore {
|
||||
) -> Option<FileInclusion> {
|
||||
let project = self.project.upgrade()?.read(cx);
|
||||
self.context().find_map(|context| match context {
|
||||
AgentContextHandle::Directory(directory_context) => {
|
||||
AgentContext::Directory(directory_context) => {
|
||||
FileInclusion::check_directory(directory_context, path, project, cx)
|
||||
}
|
||||
_ => None,
|
||||
@@ -413,7 +385,7 @@ impl ContextStore {
|
||||
|
||||
pub fn includes_symbol(&self, symbol: &Symbol, cx: &App) -> bool {
|
||||
self.context().any(|context| match context {
|
||||
AgentContextHandle::Symbol(context) => {
|
||||
AgentContext::Symbol(context) => {
|
||||
if context.symbol != symbol.name {
|
||||
return false;
|
||||
}
|
||||
@@ -438,7 +410,7 @@ impl ContextStore {
|
||||
|
||||
pub fn includes_user_rules(&self, prompt_id: UserPromptId) -> bool {
|
||||
self.context_set
|
||||
.contains(&RulesContextHandle::lookup_key(prompt_id))
|
||||
.contains(&RulesContext::lookup_key(prompt_id))
|
||||
}
|
||||
|
||||
pub fn includes_url(&self, url: impl Into<SharedString>) -> bool {
|
||||
@@ -449,17 +421,17 @@ impl ContextStore {
|
||||
pub fn file_paths(&self, cx: &App) -> HashSet<ProjectPath> {
|
||||
self.context()
|
||||
.filter_map(|context| match context {
|
||||
AgentContextHandle::File(file) => {
|
||||
AgentContext::File(file) => {
|
||||
let buffer = file.buffer.read(cx);
|
||||
buffer.project_path(cx)
|
||||
}
|
||||
AgentContextHandle::Directory(_)
|
||||
| AgentContextHandle::Symbol(_)
|
||||
| AgentContextHandle::Selection(_)
|
||||
| AgentContextHandle::FetchedUrl(_)
|
||||
| AgentContextHandle::Thread(_)
|
||||
| AgentContextHandle::Rules(_)
|
||||
| AgentContextHandle::Image(_) => None,
|
||||
AgentContext::Directory(_)
|
||||
| AgentContext::Symbol(_)
|
||||
| AgentContext::Selection(_)
|
||||
| AgentContext::FetchedUrl(_)
|
||||
| AgentContext::Thread(_)
|
||||
| AgentContext::Rules(_)
|
||||
| AgentContext::Image(_) => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -475,7 +447,7 @@ pub enum FileInclusion {
|
||||
}
|
||||
|
||||
impl FileInclusion {
|
||||
fn check_file(file_context: &FileContextHandle, path: &ProjectPath, cx: &App) -> Option<Self> {
|
||||
fn check_file(file_context: &FileContext, path: &ProjectPath, cx: &App) -> Option<Self> {
|
||||
let file_path = file_context.buffer.read(cx).project_path(cx)?;
|
||||
if path == &file_path {
|
||||
Some(FileInclusion::Direct)
|
||||
@@ -484,17 +456,8 @@ impl FileInclusion {
|
||||
}
|
||||
}
|
||||
|
||||
fn check_image(image_context: &ImageContext, path: &ProjectPath) -> Option<Self> {
|
||||
let image_path = image_context.project_path.as_ref()?;
|
||||
if path == image_path {
|
||||
Some(FileInclusion::Direct)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn check_directory(
|
||||
directory_context: &DirectoryContextHandle,
|
||||
directory_context: &DirectoryContext,
|
||||
path: &ProjectPath,
|
||||
project: &Project,
|
||||
cx: &App,
|
||||
|
||||
@@ -14,7 +14,7 @@ use project::ProjectItem;
|
||||
use ui::{KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::context::{AgentContextHandle, ContextKind};
|
||||
use crate::context::{AgentContext, ContextKind};
|
||||
use crate::context_picker::ContextPicker;
|
||||
use crate::context_store::ContextStore;
|
||||
use crate::thread::Thread;
|
||||
@@ -92,9 +92,7 @@ impl ContextStrip {
|
||||
self.context_store
|
||||
.read(cx)
|
||||
.context()
|
||||
.flat_map(|context| {
|
||||
AddedContext::new_pending(context.clone(), prompt_store, project, cx)
|
||||
})
|
||||
.flat_map(|context| AddedContext::new(context.clone(), prompt_store, project, cx))
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
Vec::new()
|
||||
@@ -290,7 +288,7 @@ impl ContextStrip {
|
||||
best.map(|(index, _, _)| index)
|
||||
}
|
||||
|
||||
fn open_context(&mut self, context: &AgentContextHandle, window: &mut Window, cx: &mut App) {
|
||||
fn open_context(&mut self, context: &AgentContext, window: &mut Window, cx: &mut App) {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
@@ -311,7 +309,7 @@ impl ContextStrip {
|
||||
};
|
||||
|
||||
self.context_store.update(cx, |this, cx| {
|
||||
this.remove_context(&context.handle, cx);
|
||||
this.remove_context(&context.context, cx);
|
||||
});
|
||||
|
||||
let is_now_empty = added_contexts.len() == 1;
|
||||
@@ -464,7 +462,7 @@ impl Render for ContextStrip {
|
||||
.enumerate()
|
||||
.map(|(i, added_context)| {
|
||||
let name = added_context.name.clone();
|
||||
let context = added_context.handle.clone();
|
||||
let context = added_context.context.clone();
|
||||
ContextPill::added(
|
||||
added_context,
|
||||
dupe_names.contains(&name),
|
||||
|
||||
@@ -1,27 +1,10 @@
|
||||
use std::{collections::VecDeque, path::Path};
|
||||
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use assistant_context_editor::{AssistantContext, SavedContextMetadata};
|
||||
use assistant_context_editor::SavedContextMetadata;
|
||||
use chrono::{DateTime, Utc};
|
||||
use futures::future::{TryFutureExt as _, join_all};
|
||||
use gpui::{Entity, Task, prelude::*};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smol::future::FutureExt;
|
||||
use std::time::Duration;
|
||||
use ui::{App, SharedString};
|
||||
use util::ResultExt as _;
|
||||
use gpui::{Entity, prelude::*};
|
||||
|
||||
use crate::{
|
||||
Thread,
|
||||
thread::ThreadId,
|
||||
thread_store::{SerializedThreadMetadata, ThreadStore},
|
||||
};
|
||||
use crate::thread_store::{SerializedThreadMetadata, ThreadStore};
|
||||
|
||||
const MAX_RECENTLY_OPENED_ENTRIES: usize = 6;
|
||||
const NAVIGATION_HISTORY_PATH: &str = "agent-navigation-history.json";
|
||||
const SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE: Duration = Duration::from_millis(50);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Debug)]
|
||||
pub enum HistoryEntry {
|
||||
Thread(SerializedThreadMetadata),
|
||||
Context(SavedContextMetadata),
|
||||
@@ -36,40 +19,16 @@ impl HistoryEntry {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum RecentEntry {
|
||||
Thread(Entity<Thread>),
|
||||
Context(Entity<AssistantContext>),
|
||||
}
|
||||
|
||||
impl RecentEntry {
|
||||
pub(crate) fn summary(&self, cx: &App) -> SharedString {
|
||||
match self {
|
||||
RecentEntry::Thread(thread) => thread.read(cx).summary_or_default(),
|
||||
RecentEntry::Context(context) => context.read(cx).summary_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
enum SerializedRecentEntry {
|
||||
Thread(String),
|
||||
Context(String),
|
||||
}
|
||||
|
||||
pub struct HistoryStore {
|
||||
thread_store: Entity<ThreadStore>,
|
||||
context_store: Entity<assistant_context_editor::ContextStore>,
|
||||
recently_opened_entries: VecDeque<RecentEntry>,
|
||||
_subscriptions: Vec<gpui::Subscription>,
|
||||
_save_recently_opened_entries_task: Task<()>,
|
||||
}
|
||||
|
||||
impl HistoryStore {
|
||||
pub fn new(
|
||||
thread_store: Entity<ThreadStore>,
|
||||
context_store: Entity<assistant_context_editor::ContextStore>,
|
||||
initial_recent_entries: impl IntoIterator<Item = RecentEntry>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let subscriptions = vec![
|
||||
@@ -77,61 +36,10 @@ impl HistoryStore {
|
||||
cx.observe(&context_store, |_, _, cx| cx.notify()),
|
||||
];
|
||||
|
||||
cx.spawn({
|
||||
let thread_store = thread_store.downgrade();
|
||||
let context_store = context_store.downgrade();
|
||||
async move |this, cx| {
|
||||
let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH);
|
||||
let contents = cx
|
||||
.background_spawn(async move { std::fs::read_to_string(path) })
|
||||
.await
|
||||
.context("reading persisted agent panel navigation history")?;
|
||||
let entries = serde_json::from_str::<Vec<SerializedRecentEntry>>(&contents)
|
||||
.context("deserializing persisted agent panel navigation history")?
|
||||
.into_iter()
|
||||
.take(MAX_RECENTLY_OPENED_ENTRIES)
|
||||
.map(|serialized| match serialized {
|
||||
SerializedRecentEntry::Thread(id) => thread_store
|
||||
.update(cx, |thread_store, cx| {
|
||||
thread_store
|
||||
.open_thread(&ThreadId::from(id.as_str()), cx)
|
||||
.map_ok(RecentEntry::Thread)
|
||||
.boxed()
|
||||
})
|
||||
.unwrap_or_else(|_| async { Err(anyhow!("no thread store")) }.boxed()),
|
||||
SerializedRecentEntry::Context(id) => context_store
|
||||
.update(cx, |context_store, cx| {
|
||||
context_store
|
||||
.open_local_context(Path::new(&id).into(), cx)
|
||||
.map_ok(RecentEntry::Context)
|
||||
.boxed()
|
||||
})
|
||||
.unwrap_or_else(|_| async { Err(anyhow!("no context store")) }.boxed()),
|
||||
});
|
||||
let entries = join_all(entries)
|
||||
.await
|
||||
.into_iter()
|
||||
.filter_map(|result| result.log_err())
|
||||
.collect::<VecDeque<_>>();
|
||||
|
||||
this.update(cx, |this, _| {
|
||||
this.recently_opened_entries.extend(entries);
|
||||
this.recently_opened_entries
|
||||
.truncate(MAX_RECENTLY_OPENED_ENTRIES);
|
||||
})
|
||||
.ok();
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
Self {
|
||||
thread_store,
|
||||
context_store,
|
||||
recently_opened_entries: initial_recent_entries.into_iter().collect(),
|
||||
_subscriptions: subscriptions,
|
||||
_save_recently_opened_entries_task: Task::ready(()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,57 +69,4 @@ impl HistoryStore {
|
||||
pub fn recent_entries(&self, limit: usize, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
|
||||
self.entries(cx).into_iter().take(limit).collect()
|
||||
}
|
||||
|
||||
fn save_recently_opened_entries(&mut self, cx: &mut Context<Self>) {
|
||||
let serialized_entries = self
|
||||
.recently_opened_entries
|
||||
.iter()
|
||||
.filter_map(|entry| match entry {
|
||||
RecentEntry::Context(context) => Some(SerializedRecentEntry::Context(
|
||||
context.read(cx).path()?.to_str()?.to_owned(),
|
||||
)),
|
||||
RecentEntry::Thread(thread) => Some(SerializedRecentEntry::Thread(
|
||||
thread.read(cx).id().to_string(),
|
||||
)),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
self._save_recently_opened_entries_task = cx.spawn(async move |_, cx| {
|
||||
cx.background_executor()
|
||||
.timer(SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE)
|
||||
.await;
|
||||
cx.background_spawn(async move {
|
||||
let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH);
|
||||
let content = serde_json::to_string(&serialized_entries)?;
|
||||
std::fs::write(path, content)?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.await
|
||||
.log_err();
|
||||
});
|
||||
}
|
||||
|
||||
pub fn push_recently_opened_entry(&mut self, entry: RecentEntry, cx: &mut Context<Self>) {
|
||||
self.recently_opened_entries
|
||||
.retain(|old_entry| old_entry != &entry);
|
||||
self.recently_opened_entries.push_front(entry);
|
||||
self.recently_opened_entries
|
||||
.truncate(MAX_RECENTLY_OPENED_ENTRIES);
|
||||
self.save_recently_opened_entries(cx);
|
||||
}
|
||||
|
||||
pub fn remove_recently_opened_entry(&mut self, entry: &RecentEntry, cx: &mut Context<Self>) {
|
||||
self.recently_opened_entries
|
||||
.retain(|old_entry| old_entry != entry);
|
||||
self.save_recently_opened_entries(cx);
|
||||
}
|
||||
|
||||
pub fn recently_opened_entries(&self, _cx: &mut Context<Self>) -> VecDeque<RecentEntry> {
|
||||
#[cfg(debug_assertions)]
|
||||
if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
|
||||
return VecDeque::new();
|
||||
}
|
||||
|
||||
self.recently_opened_entries.clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ use workspace::{ItemHandle, Toast, Workspace, dock::Panel, notifications::Notifi
|
||||
use zed_actions::agent::OpenConfiguration;
|
||||
|
||||
use crate::AssistantPanel;
|
||||
use crate::batch_assist::BatchAssistToolbarItem;
|
||||
use crate::buffer_codegen::{BufferCodegen, CodegenAlternative, CodegenEvent};
|
||||
use crate::context_store::ContextStore;
|
||||
use crate::inline_prompt_editor::{CodegenStatus, InlineAssistId, PromptEditor, PromptEditorEvent};
|
||||
@@ -335,12 +336,37 @@ impl InlineAssistant {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
// if there's multiple selections
|
||||
// find the pane containing this editor, grab the assist toolbar off it, show it
|
||||
|
||||
let (snapshot, initial_selections) = editor.update(cx, |editor, cx| {
|
||||
(
|
||||
editor.snapshot(window, cx),
|
||||
editor.selections.all::<Point>(cx),
|
||||
)
|
||||
});
|
||||
if initial_selections.len() > 1 {
|
||||
cx.defer(move |cx| {
|
||||
let assist = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.active_pane()
|
||||
.read(cx)
|
||||
.toolbar()
|
||||
.read(cx)
|
||||
.item_of_type::<BatchAssistToolbarItem>()
|
||||
})
|
||||
.ok()
|
||||
.flatten();
|
||||
let Some(assist) = assist else {
|
||||
dbg!("oops");
|
||||
return;
|
||||
};
|
||||
|
||||
assist.update(cx, |assist, cx| assist.deploy(cx));
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let mut selections = Vec::<Selection<Point>>::new();
|
||||
let mut newest_selection = None;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::assistant_model_selector::{AssistantModelSelector, ModelType};
|
||||
use crate::assistant_model_selector::AssistantModelSelector;
|
||||
use crate::batch_assist::BatchCodegen;
|
||||
use crate::buffer_codegen::BufferCodegen;
|
||||
use crate::context_picker::ContextPicker;
|
||||
use crate::context_store::ContextStore;
|
||||
@@ -20,7 +21,7 @@ use gpui::{
|
||||
Focusable, FontWeight, Subscription, TextStyle, WeakEntity, Window, anchored, deferred, point,
|
||||
};
|
||||
use language_model::{LanguageModel, LanguageModelRegistry};
|
||||
use language_model_selector::ToggleModelSelector;
|
||||
use language_model_selector::{ModelType, ToggleModelSelector};
|
||||
use parking_lot::Mutex;
|
||||
use settings::Settings;
|
||||
use std::cmp;
|
||||
@@ -33,7 +34,7 @@ use ui::{
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub struct PromptEditor<T> {
|
||||
pub struct PromptEditor {
|
||||
pub editor: Entity<Editor>,
|
||||
mode: PromptEditorMode,
|
||||
context_store: Entity<ContextStore>,
|
||||
@@ -48,12 +49,11 @@ pub struct PromptEditor<T> {
|
||||
editor_subscriptions: Vec<Subscription>,
|
||||
_context_strip_subscription: Subscription,
|
||||
show_rate_limit_notice: bool,
|
||||
_phantom: std::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T: 'static> EventEmitter<PromptEditorEvent> for PromptEditor<T> {}
|
||||
impl EventEmitter<PromptEditorEvent> for PromptEditor {}
|
||||
|
||||
impl<T: 'static> Render for PromptEditor<T> {
|
||||
impl Render for PromptEditor {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx);
|
||||
let mut buttons = Vec::new();
|
||||
@@ -74,6 +74,7 @@ impl<T: 'static> Render for PromptEditor<T> {
|
||||
|
||||
gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0)
|
||||
}
|
||||
PromptEditorMode::Batch { .. } => px(0.),
|
||||
PromptEditorMode::Terminal { .. } => {
|
||||
// Give the equivalent of the same left-padding that we're using on the right
|
||||
Pixels::from(40.0)
|
||||
@@ -82,6 +83,7 @@ impl<T: 'static> Render for PromptEditor<T> {
|
||||
|
||||
let bottom_padding = match &self.mode {
|
||||
PromptEditorMode::Buffer { .. } => Pixels::from(0.),
|
||||
PromptEditorMode::Batch { .. } => Pixels::from(0.),
|
||||
PromptEditorMode::Terminal { .. } => Pixels::from(8.0),
|
||||
};
|
||||
|
||||
@@ -208,18 +210,19 @@ impl<T: 'static> Render for PromptEditor<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static> Focusable for PromptEditor<T> {
|
||||
impl Focusable for PromptEditor {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
self.editor.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static> PromptEditor<T> {
|
||||
impl PromptEditor {
|
||||
const MAX_LINES: u8 = 8;
|
||||
|
||||
fn codegen_status<'a>(&'a self, cx: &'a App) -> &'a CodegenStatus {
|
||||
match &self.mode {
|
||||
PromptEditorMode::Buffer { codegen, .. } => codegen.read(cx).status(cx),
|
||||
PromptEditorMode::Batch { .. } => todo!(),
|
||||
PromptEditorMode::Terminal { codegen, .. } => &codegen.read(cx).status,
|
||||
}
|
||||
}
|
||||
@@ -269,6 +272,7 @@ impl<T: 'static> PromptEditor<T> {
|
||||
"Transform"
|
||||
}
|
||||
}
|
||||
PromptEditorMode::Batch { .. } => "Transform",
|
||||
PromptEditorMode::Terminal { .. } => "Generate",
|
||||
};
|
||||
|
||||
@@ -448,6 +452,7 @@ impl<T: 'static> PromptEditor<T> {
|
||||
GenerationMode::Transform
|
||||
}
|
||||
}
|
||||
PromptEditorMode::Batch { .. } => GenerationMode::Transform,
|
||||
PromptEditorMode::Terminal { .. } => GenerationMode::Generate,
|
||||
};
|
||||
|
||||
@@ -535,7 +540,9 @@ impl<T: 'static> PromptEditor<T> {
|
||||
}))
|
||||
.into_any_element(),
|
||||
],
|
||||
PromptEditorMode::Buffer { .. } => vec![accept],
|
||||
PromptEditorMode::Buffer { .. } | PromptEditorMode::Batch { .. } => {
|
||||
vec![accept]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -552,6 +559,9 @@ impl<T: 'static> PromptEditor<T> {
|
||||
PromptEditorMode::Buffer { codegen, .. } => {
|
||||
codegen.update(cx, |codegen, cx| codegen.cycle_prev(cx));
|
||||
}
|
||||
PromptEditorMode::Batch { .. } => {
|
||||
todo!()
|
||||
}
|
||||
PromptEditorMode::Terminal { .. } => {
|
||||
// no cycle buttons in terminal mode
|
||||
}
|
||||
@@ -563,6 +573,9 @@ impl<T: 'static> PromptEditor<T> {
|
||||
PromptEditorMode::Buffer { codegen, .. } => {
|
||||
codegen.update(cx, |codegen, cx| codegen.cycle_next(cx));
|
||||
}
|
||||
PromptEditorMode::Batch { .. } => {
|
||||
todo!()
|
||||
}
|
||||
PromptEditorMode::Terminal { .. } => {
|
||||
// no cycle buttons in terminal mode
|
||||
}
|
||||
@@ -796,6 +809,9 @@ pub enum PromptEditorMode {
|
||||
codegen: Entity<BufferCodegen>,
|
||||
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
|
||||
},
|
||||
Batch {
|
||||
codegen: Entity<BatchCodegen>,
|
||||
},
|
||||
Terminal {
|
||||
id: TerminalInlineAssistId,
|
||||
codegen: Entity<TerminalCodegen>,
|
||||
@@ -823,7 +839,7 @@ impl InlineAssistId {
|
||||
}
|
||||
}
|
||||
|
||||
impl PromptEditor<BufferCodegen> {
|
||||
impl PromptEditor {
|
||||
pub fn new_buffer(
|
||||
id: InlineAssistId,
|
||||
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
|
||||
@@ -835,9 +851,9 @@ impl PromptEditor<BufferCodegen> {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
thread_store: Option<WeakEntity<ThreadStore>>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<PromptEditor<BufferCodegen>>,
|
||||
) -> PromptEditor<BufferCodegen> {
|
||||
let codegen_subscription = cx.observe(&codegen, Self::handle_codegen_changed);
|
||||
cx: &mut Context<PromptEditor>,
|
||||
) -> PromptEditor {
|
||||
let codegen_subscription = cx.observe(&codegen, Self::handle_buffer_codegen_changed);
|
||||
let mode = PromptEditorMode::Buffer {
|
||||
id,
|
||||
codegen,
|
||||
@@ -880,7 +896,7 @@ impl PromptEditor<BufferCodegen> {
|
||||
let context_strip_subscription =
|
||||
cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event);
|
||||
|
||||
let mut this: PromptEditor<BufferCodegen> = PromptEditor {
|
||||
let mut this: PromptEditor = PromptEditor {
|
||||
editor: prompt_editor.clone(),
|
||||
context_store,
|
||||
context_strip,
|
||||
@@ -904,17 +920,16 @@ impl PromptEditor<BufferCodegen> {
|
||||
_context_strip_subscription: context_strip_subscription,
|
||||
show_rate_limit_notice: false,
|
||||
mode,
|
||||
_phantom: Default::default(),
|
||||
};
|
||||
|
||||
this.subscribe_to_editor(window, cx);
|
||||
this
|
||||
}
|
||||
|
||||
fn handle_codegen_changed(
|
||||
fn handle_buffer_codegen_changed(
|
||||
&mut self,
|
||||
_: Entity<BufferCodegen>,
|
||||
cx: &mut Context<PromptEditor<BufferCodegen>>,
|
||||
cx: &mut Context<PromptEditor>,
|
||||
) {
|
||||
match self.codegen_status(cx) {
|
||||
CodegenStatus::Idle => {
|
||||
@@ -949,6 +964,7 @@ impl PromptEditor<BufferCodegen> {
|
||||
pub fn id(&self) -> InlineAssistId {
|
||||
match &self.mode {
|
||||
PromptEditorMode::Buffer { id, .. } => *id,
|
||||
PromptEditorMode::Batch { .. } => unreachable!(),
|
||||
PromptEditorMode::Terminal { .. } => unreachable!(),
|
||||
}
|
||||
}
|
||||
@@ -956,6 +972,7 @@ impl PromptEditor<BufferCodegen> {
|
||||
pub fn codegen(&self) -> &Entity<BufferCodegen> {
|
||||
match &self.mode {
|
||||
PromptEditorMode::Buffer { codegen, .. } => codegen,
|
||||
PromptEditorMode::Batch { .. } => unreachable!(),
|
||||
PromptEditorMode::Terminal { .. } => unreachable!(),
|
||||
}
|
||||
}
|
||||
@@ -965,6 +982,7 @@ impl PromptEditor<BufferCodegen> {
|
||||
PromptEditorMode::Buffer {
|
||||
gutter_dimensions, ..
|
||||
} => gutter_dimensions,
|
||||
PromptEditorMode::Batch { .. } => unreachable!(),
|
||||
PromptEditorMode::Terminal { .. } => unreachable!(),
|
||||
}
|
||||
}
|
||||
@@ -981,7 +999,7 @@ impl TerminalInlineAssistId {
|
||||
}
|
||||
}
|
||||
|
||||
impl PromptEditor<TerminalCodegen> {
|
||||
impl PromptEditor {
|
||||
pub fn new_terminal(
|
||||
id: TerminalInlineAssistId,
|
||||
prompt_history: VecDeque<String>,
|
||||
@@ -994,7 +1012,7 @@ impl PromptEditor<TerminalCodegen> {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let codegen_subscription = cx.observe(&codegen, Self::handle_codegen_changed);
|
||||
let codegen_subscription = cx.observe(&codegen, Self::handle_terminal_codegen_changed);
|
||||
let mode = PromptEditorMode::Terminal {
|
||||
id,
|
||||
codegen,
|
||||
@@ -1057,7 +1075,6 @@ impl PromptEditor<TerminalCodegen> {
|
||||
_context_strip_subscription: context_strip_subscription,
|
||||
mode,
|
||||
show_rate_limit_notice: false,
|
||||
_phantom: Default::default(),
|
||||
};
|
||||
this.count_lines(cx);
|
||||
this.subscribe_to_editor(window, cx);
|
||||
@@ -1084,12 +1101,17 @@ impl PromptEditor<TerminalCodegen> {
|
||||
cx.emit(PromptEditorEvent::Resized { height_in_lines });
|
||||
}
|
||||
}
|
||||
PromptEditorMode::Batch { .. } => unreachable!(),
|
||||
PromptEditorMode::Buffer { .. } => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_codegen_changed(&mut self, _: Entity<TerminalCodegen>, cx: &mut Context<Self>) {
|
||||
match &self.codegen().read(cx).status {
|
||||
fn handle_terminal_codegen_changed(
|
||||
&mut self,
|
||||
_: Entity<TerminalCodegen>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match &self.terminal_codegen().read(cx).status {
|
||||
CodegenStatus::Idle => {
|
||||
self.editor
|
||||
.update(cx, |editor, _| editor.set_read_only(false));
|
||||
@@ -1106,16 +1128,18 @@ impl PromptEditor<TerminalCodegen> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn codegen(&self) -> &Entity<TerminalCodegen> {
|
||||
pub fn terminal_codegen(&self) -> &Entity<TerminalCodegen> {
|
||||
match &self.mode {
|
||||
PromptEditorMode::Buffer { .. } => unreachable!(),
|
||||
PromptEditorMode::Batch { .. } => unreachable!(),
|
||||
PromptEditorMode::Terminal { codegen, .. } => codegen,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> TerminalInlineAssistId {
|
||||
pub fn terminal_inline_assist_id(&self) -> TerminalInlineAssistId {
|
||||
match &self.mode {
|
||||
PromptEditorMode::Buffer { .. } => unreachable!(),
|
||||
PromptEditorMode::Batch { .. } => unreachable!(),
|
||||
PromptEditorMode::Terminal { id, .. } => *id,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::assistant_model_selector::{AssistantModelSelector, ModelType};
|
||||
use crate::assistant_model_selector::ModelType;
|
||||
use crate::context::{ContextLoadResult, load_context};
|
||||
use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip};
|
||||
use crate::ui::AnimatedLabel;
|
||||
use buffer_diff::BufferDiff;
|
||||
use collections::HashSet;
|
||||
use editor::actions::{MoveUp, Paste};
|
||||
@@ -22,7 +21,9 @@ use gpui::{
|
||||
Task, TextStyle, WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between,
|
||||
};
|
||||
use language::{Buffer, Language};
|
||||
use language_model::{ConfiguredModel, LanguageModelRequestMessage, MessageContent};
|
||||
use language_model::{
|
||||
ConfiguredModel, LanguageModelRegistry, LanguageModelRequestMessage, MessageContent,
|
||||
};
|
||||
use language_model_selector::ToggleModelSelector;
|
||||
use multi_buffer;
|
||||
use project::Project;
|
||||
@@ -35,6 +36,7 @@ use util::ResultExt as _;
|
||||
use workspace::Workspace;
|
||||
use zed_llm_client::CompletionMode;
|
||||
|
||||
use crate::assistant_model_selector::AssistantModelSelector;
|
||||
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider};
|
||||
use crate::context_store::ContextStore;
|
||||
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
|
||||
@@ -58,12 +60,13 @@ pub struct MessageEditor {
|
||||
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||
model_selector: Entity<AssistantModelSelector>,
|
||||
last_loaded_context: Option<ContextLoadResult>,
|
||||
load_context_task: Option<Shared<Task<()>>>,
|
||||
context_load_task: Option<Shared<Task<()>>>,
|
||||
profile_selector: Entity<ProfileSelector>,
|
||||
edits_expanded: bool,
|
||||
editor_is_expanded: bool,
|
||||
waiting_for_summaries_to_send: bool,
|
||||
last_estimated_token_count: Option<usize>,
|
||||
update_token_count_task: Option<Task<()>>,
|
||||
update_token_count_task: Option<Task<anyhow::Result<()>>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
@@ -146,22 +149,10 @@ impl MessageEditor {
|
||||
_ => {}
|
||||
}),
|
||||
cx.observe(&context_store, |this, _, cx| {
|
||||
// When context changes, reload it for token counting.
|
||||
let _ = this.reload_context(cx);
|
||||
let _ = this.start_context_load(cx);
|
||||
}),
|
||||
];
|
||||
|
||||
let model_selector = cx.new(|cx| {
|
||||
AssistantModelSelector::new(
|
||||
fs.clone(),
|
||||
model_selector_menu_handle,
|
||||
editor.focus_handle(cx),
|
||||
ModelType::Default(thread.clone()),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
Self {
|
||||
editor: editor.clone(),
|
||||
project: thread.read(cx).project().clone(),
|
||||
@@ -172,11 +163,21 @@ impl MessageEditor {
|
||||
prompt_store,
|
||||
context_strip,
|
||||
context_picker_menu_handle,
|
||||
load_context_task: None,
|
||||
context_load_task: None,
|
||||
last_loaded_context: None,
|
||||
model_selector,
|
||||
model_selector: cx.new(|cx| {
|
||||
AssistantModelSelector::new(
|
||||
fs.clone(),
|
||||
model_selector_menu_handle,
|
||||
editor.focus_handle(cx),
|
||||
ModelType::Default,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
edits_expanded: false,
|
||||
editor_is_expanded: false,
|
||||
waiting_for_summaries_to_send: false,
|
||||
profile_selector: cx
|
||||
.new(|cx| ProfileSelector::new(fs, thread_store, editor.focus_handle(cx), cx)),
|
||||
last_estimated_token_count: None,
|
||||
@@ -244,10 +245,6 @@ impl MessageEditor {
|
||||
return;
|
||||
}
|
||||
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.cancel_editing(cx);
|
||||
});
|
||||
|
||||
if self.thread.read(cx).is_generating() {
|
||||
self.stop_current_and_send_new_message(window, cx);
|
||||
return;
|
||||
@@ -263,11 +260,15 @@ impl MessageEditor {
|
||||
self.editor.read(cx).text(cx).trim().is_empty()
|
||||
}
|
||||
|
||||
fn is_model_selected(&self, cx: &App) -> bool {
|
||||
LanguageModelRegistry::read_global(cx)
|
||||
.default_model()
|
||||
.is_some()
|
||||
}
|
||||
|
||||
fn send_to_model(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(ConfiguredModel { model, provider }) = self
|
||||
.thread
|
||||
.update(cx, |thread, cx| thread.get_or_init_configured_model(cx))
|
||||
else {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
let Some(ConfiguredModel { model, provider }) = model_registry.default_model() else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -288,7 +289,7 @@ impl MessageEditor {
|
||||
let thread = self.thread.clone();
|
||||
let git_store = self.project.read(cx).git_store().clone();
|
||||
let checkpoint = git_store.update(cx, |git_store, cx| git_store.checkpoint(cx));
|
||||
let context_task = self.reload_context(cx);
|
||||
let context_task = self.load_context(cx);
|
||||
let window_handle = window.window_handle();
|
||||
|
||||
cx.spawn(async move |_this, cx| {
|
||||
@@ -311,11 +312,31 @@ impl MessageEditor {
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn stop_current_and_send_new_message(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.cancel_editing(cx);
|
||||
});
|
||||
fn wait_for_summaries(&mut self, cx: &mut Context<Self>) -> Task<()> {
|
||||
let context_store = self.context_store.clone();
|
||||
cx.spawn(async move |this, cx| {
|
||||
if let Some(wait_for_summaries) = context_store
|
||||
.update(cx, |context_store, cx| context_store.wait_for_summaries(cx))
|
||||
.ok()
|
||||
{
|
||||
this.update(cx, |this, cx| {
|
||||
this.waiting_for_summaries_to_send = true;
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
|
||||
wait_for_summaries.await;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.waiting_for_summaries_to_send = false;
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn stop_current_and_send_new_message(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let cancelled = self.thread.update(cx, |thread, cx| {
|
||||
thread.cancel_last_completion(Some(window.window_handle()), cx)
|
||||
});
|
||||
@@ -375,7 +396,7 @@ impl MessageEditor {
|
||||
|
||||
self.context_store.update(cx, |store, cx| {
|
||||
for image in images {
|
||||
store.add_image_instance(Arc::new(image), cx);
|
||||
store.add_image(Arc::new(image), cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -404,16 +425,17 @@ impl MessageEditor {
|
||||
return None;
|
||||
}
|
||||
|
||||
let thread = self.thread.read(cx);
|
||||
let model = thread.configured_model();
|
||||
if !model?.model.supports_max_mode() {
|
||||
let model = LanguageModelRegistry::read_global(cx)
|
||||
.default_model()
|
||||
.map(|default| default.model.clone())?;
|
||||
if !model.supports_max_mode() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let active_completion_mode = thread.completion_mode();
|
||||
let active_completion_mode = self.thread.read(cx).completion_mode();
|
||||
|
||||
Some(
|
||||
IconButton::new("max-mode", IconName::ZedMaxMode)
|
||||
IconButton::new("max-mode", IconName::SquarePlus)
|
||||
.icon_size(IconSize::Small)
|
||||
.toggle_state(active_completion_mode == Some(CompletionMode::Max))
|
||||
.on_click(cx.listener(move |this, _event, _window, cx| {
|
||||
@@ -424,7 +446,7 @@ impl MessageEditor {
|
||||
});
|
||||
});
|
||||
}))
|
||||
.tooltip(Tooltip::text("Toggle Max Mode"))
|
||||
.tooltip(Tooltip::text("Max Mode"))
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
@@ -437,21 +459,24 @@ impl MessageEditor {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Div {
|
||||
let thread = self.thread.read(cx);
|
||||
let model = thread.configured_model();
|
||||
|
||||
let editor_bg_color = cx.theme().colors().editor_background;
|
||||
let is_generating = thread.is_generating();
|
||||
let focus_handle = self.editor.focus_handle(cx);
|
||||
|
||||
let is_model_selected = model.is_some();
|
||||
let is_model_selected = self.is_model_selected(cx);
|
||||
let is_editor_empty = self.is_editor_empty(cx);
|
||||
|
||||
let model = LanguageModelRegistry::read_global(cx)
|
||||
.default_model()
|
||||
.map(|default| default.model.clone());
|
||||
|
||||
let incompatible_tools = model
|
||||
.as_ref()
|
||||
.map(|model| {
|
||||
self.incompatible_tools_state.update(cx, |state, cx| {
|
||||
state
|
||||
.incompatible_tools(&model.model, cx)
|
||||
.incompatible_tools(model, cx)
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
@@ -495,34 +520,32 @@ impl MessageEditor {
|
||||
.items_start()
|
||||
.justify_between()
|
||||
.child(self.context_strip.clone())
|
||||
.when(focus_handle.is_focused(window), |this| {
|
||||
this.child(
|
||||
IconButton::new("toggle-height", expand_icon)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
let expand_label = if is_editor_expanded {
|
||||
"Minimize Message Editor".to_string()
|
||||
} else {
|
||||
"Expand Message Editor".to_string()
|
||||
};
|
||||
.child(
|
||||
IconButton::new("toggle-height", expand_icon)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
let expand_label = if is_editor_expanded {
|
||||
"Minimize Message Editor".to_string()
|
||||
} else {
|
||||
"Expand Message Editor".to_string()
|
||||
};
|
||||
|
||||
Tooltip::for_action_in(
|
||||
expand_label,
|
||||
&ExpandMessageEditor,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(|_, _, window, cx| {
|
||||
window.dispatch_action(Box::new(ExpandMessageEditor), cx);
|
||||
})),
|
||||
)
|
||||
}),
|
||||
Tooltip::for_action_in(
|
||||
expand_label,
|
||||
&ExpandMessageEditor,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(|_, _, window, cx| {
|
||||
window.dispatch_action(Box::new(ExpandMessageEditor), cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
@@ -641,31 +664,31 @@ impl MessageEditor {
|
||||
})
|
||||
.when(!is_editor_empty, |parent| {
|
||||
parent.child(
|
||||
IconButton::new(
|
||||
"send-message",
|
||||
IconName::Send,
|
||||
)
|
||||
.icon_color(Color::Accent)
|
||||
.style(ButtonStyle::Filled)
|
||||
.disabled(!is_model_selected)
|
||||
.on_click({
|
||||
let focus_handle =
|
||||
focus_handle.clone();
|
||||
move |_event, window, cx| {
|
||||
focus_handle.dispatch_action(
|
||||
&Chat, window, cx,
|
||||
);
|
||||
}
|
||||
})
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::for_action(
|
||||
"Stop and Send New Message",
|
||||
&Chat,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
IconButton::new("send-message", IconName::Send)
|
||||
.icon_color(Color::Accent)
|
||||
.style(ButtonStyle::Filled)
|
||||
.disabled(
|
||||
!is_model_selected
|
||||
|| self
|
||||
.waiting_for_summaries_to_send,
|
||||
)
|
||||
.on_click({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |_event, window, cx| {
|
||||
focus_handle.dispatch_action(
|
||||
&Chat, window, cx,
|
||||
);
|
||||
}
|
||||
})
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::for_action(
|
||||
"Stop and Send New Message",
|
||||
&Chat,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
} else {
|
||||
parent.child(
|
||||
@@ -673,7 +696,10 @@ impl MessageEditor {
|
||||
.icon_color(Color::Accent)
|
||||
.style(ButtonStyle::Filled)
|
||||
.disabled(
|
||||
is_editor_empty || !is_model_selected,
|
||||
is_editor_empty
|
||||
|| !is_model_selected
|
||||
|| self
|
||||
.waiting_for_summaries_to_send,
|
||||
)
|
||||
.on_click({
|
||||
let focus_handle = focus_handle.clone();
|
||||
@@ -724,12 +750,9 @@ impl MessageEditor {
|
||||
let border_color = cx.theme().colors().border;
|
||||
let active_color = cx.theme().colors().element_selected;
|
||||
let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
|
||||
|
||||
let is_edit_changes_expanded = self.edits_expanded;
|
||||
let is_generating = self.thread.read(cx).is_generating();
|
||||
|
||||
v_flex()
|
||||
.mt_1()
|
||||
.mx_2()
|
||||
.bg(bg_edit_files_disclosure)
|
||||
.border_1()
|
||||
@@ -764,44 +787,25 @@ impl MessageEditor {
|
||||
cx.notify();
|
||||
})),
|
||||
)
|
||||
.map(|this| {
|
||||
if is_generating {
|
||||
this.child(
|
||||
AnimatedLabel::new(format!(
|
||||
"Editing {} {}",
|
||||
changed_buffers.len(),
|
||||
if changed_buffers.len() == 1 {
|
||||
"file"
|
||||
} else {
|
||||
"files"
|
||||
}
|
||||
))
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
} else {
|
||||
this.child(
|
||||
Label::new("Edits")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Label::new("•").size(LabelSize::XSmall).color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Label::new(format!(
|
||||
"{} {}",
|
||||
changed_buffers.len(),
|
||||
if changed_buffers.len() == 1 {
|
||||
"file"
|
||||
} else {
|
||||
"files"
|
||||
}
|
||||
))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
}
|
||||
}),
|
||||
.child(
|
||||
Label::new("Edits")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Label::new("•").size(LabelSize::XSmall).color(Color::Muted))
|
||||
.child(
|
||||
Label::new(format!(
|
||||
"{} {}",
|
||||
changed_buffers.len(),
|
||||
if changed_buffers.len() == 1 {
|
||||
"file"
|
||||
} else {
|
||||
"files"
|
||||
}
|
||||
))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Button::new("review", "Review Changes")
|
||||
@@ -891,7 +895,7 @@ impl MessageEditor {
|
||||
.justify_between()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.hover(|style| style.bg(hover_color))
|
||||
.when(index < changed_buffers.len() - 1, |parent| {
|
||||
.when(index + 1 < changed_buffers.len(), |parent| {
|
||||
parent.border_color(border_color).border_b_1()
|
||||
})
|
||||
.child(
|
||||
@@ -907,9 +911,9 @@ impl MessageEditor {
|
||||
.gap_0p5()
|
||||
.children(name_label)
|
||||
.children(parent_label),
|
||||
), // TODO: Implement line diff
|
||||
// .child(Label::new("+").color(Color::Created))
|
||||
// .child(Label::new("-").color(Color::Deleted)),
|
||||
) // TODO: show lines changed
|
||||
.child(Label::new("+").color(Color::Created))
|
||||
.child(Label::new("-").color(Color::Deleted)),
|
||||
)
|
||||
.child(
|
||||
div().visible_on_hover("edited-code").child(
|
||||
@@ -1037,8 +1041,16 @@ impl MessageEditor {
|
||||
self.update_token_count_task.is_some()
|
||||
}
|
||||
|
||||
fn reload_context(&mut self, cx: &mut Context<Self>) -> Task<Option<ContextLoadResult>> {
|
||||
fn handle_message_changed(&mut self, cx: &mut Context<Self>) {
|
||||
self.message_or_context_changed(true, cx);
|
||||
}
|
||||
|
||||
fn start_context_load(&mut self, cx: &mut Context<Self>) -> Shared<Task<()>> {
|
||||
let summaries_task = self.wait_for_summaries(cx);
|
||||
let load_task = cx.spawn(async move |this, cx| {
|
||||
// Waits for detailed summaries before `load_context`, as it directly reads these from
|
||||
// the thread. TODO: Would be cleaner to have context loading await on summarization.
|
||||
summaries_task.await;
|
||||
let Ok(load_task) = this.update(cx, |this, cx| {
|
||||
let new_context = this.context_store.read_with(cx, |context_store, cx| {
|
||||
context_store.new_context_for_thread(this.thread.read(cx))
|
||||
@@ -1050,31 +1062,32 @@ impl MessageEditor {
|
||||
let result = load_task.await;
|
||||
this.update(cx, |this, cx| {
|
||||
this.last_loaded_context = Some(result);
|
||||
this.load_context_task = None;
|
||||
this.context_load_task = None;
|
||||
this.message_or_context_changed(false, cx);
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
// Replace existing load task, if any, causing it to be cancelled.
|
||||
let load_task = load_task.shared();
|
||||
self.load_context_task = Some(load_task.clone());
|
||||
self.context_load_task = Some(load_task.clone());
|
||||
load_task
|
||||
}
|
||||
|
||||
fn load_context(&mut self, cx: &mut Context<Self>) -> Task<Option<ContextLoadResult>> {
|
||||
let context_load_task = self.start_context_load(cx);
|
||||
cx.spawn(async move |this, cx| {
|
||||
load_task.await;
|
||||
context_load_task.await;
|
||||
this.read_with(cx, |this, _cx| this.last_loaded_context.clone())
|
||||
.ok()
|
||||
.flatten()
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_message_changed(&mut self, cx: &mut Context<Self>) {
|
||||
self.message_or_context_changed(true, cx);
|
||||
}
|
||||
|
||||
fn message_or_context_changed(&mut self, debounce: bool, cx: &mut Context<Self>) {
|
||||
cx.emit(MessageEditorEvent::Changed);
|
||||
self.update_token_count_task.take();
|
||||
|
||||
let Some(model) = self.thread.read(cx).configured_model() else {
|
||||
let Some(default_model) = LanguageModelRegistry::read_global(cx).default_model() else {
|
||||
self.last_estimated_token_count.take();
|
||||
return;
|
||||
};
|
||||
@@ -1088,64 +1101,57 @@ impl MessageEditor {
|
||||
.await;
|
||||
}
|
||||
|
||||
let token_count = if let Some(task) = this
|
||||
.update(cx, |this, cx| {
|
||||
let loaded_context = this
|
||||
.last_loaded_context
|
||||
.as_ref()
|
||||
.map(|context_load_result| &context_load_result.loaded_context);
|
||||
let message_text = editor.read(cx).text(cx);
|
||||
let token_count = if let Some(task) = this.update(cx, |this, cx| {
|
||||
let loaded_context = this
|
||||
.last_loaded_context
|
||||
.as_ref()
|
||||
.map(|context_load_result| &context_load_result.loaded_context);
|
||||
let message_text = editor.read(cx).text(cx);
|
||||
|
||||
if message_text.is_empty()
|
||||
&& loaded_context.map_or(true, |loaded_context| loaded_context.is_empty())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
if message_text.is_empty()
|
||||
&& loaded_context.map_or(true, |loaded_context| loaded_context.is_empty())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut request_message = LanguageModelRequestMessage {
|
||||
role: language_model::Role::User,
|
||||
content: Vec::new(),
|
||||
cache: false,
|
||||
};
|
||||
let mut request_message = LanguageModelRequestMessage {
|
||||
role: language_model::Role::User,
|
||||
content: Vec::new(),
|
||||
cache: false,
|
||||
};
|
||||
|
||||
if let Some(loaded_context) = loaded_context {
|
||||
loaded_context.add_to_request_message(&mut request_message);
|
||||
}
|
||||
if let Some(loaded_context) = loaded_context {
|
||||
loaded_context.add_to_request_message(&mut request_message);
|
||||
}
|
||||
|
||||
if !message_text.is_empty() {
|
||||
request_message
|
||||
.content
|
||||
.push(MessageContent::Text(message_text));
|
||||
}
|
||||
if !message_text.is_empty() {
|
||||
request_message
|
||||
.content
|
||||
.push(MessageContent::Text(message_text));
|
||||
}
|
||||
|
||||
let request = language_model::LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
mode: None,
|
||||
messages: vec![request_message],
|
||||
tools: vec![],
|
||||
stop: vec![],
|
||||
temperature: None,
|
||||
};
|
||||
let request = language_model::LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
mode: None,
|
||||
messages: vec![request_message],
|
||||
tools: vec![],
|
||||
stop: vec![],
|
||||
temperature: None,
|
||||
};
|
||||
|
||||
Some(model.model.count_tokens(request, cx))
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
{
|
||||
task.await.log_err()
|
||||
Some(default_model.model.count_tokens(request, cx))
|
||||
})? {
|
||||
task.await?
|
||||
} else {
|
||||
Some(0)
|
||||
0
|
||||
};
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
if let Some(token_count) = token_count {
|
||||
this.last_estimated_token_count = Some(token_count);
|
||||
cx.emit(MessageEditorEvent::EstimatedTokenCount);
|
||||
}
|
||||
this.last_estimated_token_count = Some(token_count);
|
||||
cx.emit(MessageEditorEvent::EstimatedTokenCount);
|
||||
this.update_token_count_task.take();
|
||||
})
|
||||
.ok();
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -1166,11 +1172,8 @@ impl Focusable for MessageEditor {
|
||||
impl Render for MessageEditor {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let thread = self.thread.read(cx);
|
||||
let token_usage_ratio = thread
|
||||
.total_token_usage()
|
||||
.map_or(TokenUsageRatio::Normal, |total_token_usage| {
|
||||
total_token_usage.ratio()
|
||||
});
|
||||
let total_token_usage = thread.total_token_usage(cx);
|
||||
let token_usage_ratio = total_token_usage.ratio();
|
||||
|
||||
let action_log = self.thread.read(cx).action_log();
|
||||
let changed_buffers = action_log.read(cx).changed_buffers(cx);
|
||||
@@ -1180,6 +1183,41 @@ impl Render for MessageEditor {
|
||||
|
||||
v_flex()
|
||||
.size_full()
|
||||
.when(self.waiting_for_summaries_to_send, |parent| {
|
||||
parent.child(
|
||||
h_flex().py_3().w_full().justify_center().child(
|
||||
h_flex()
|
||||
.flex_none()
|
||||
.px_2()
|
||||
.py_2()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.rounded_lg()
|
||||
.shadow_md()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| {
|
||||
icon.transform(gpui::Transformation::rotate(
|
||||
gpui::percentage(delta),
|
||||
))
|
||||
},
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Label::new("Summarizing context…")
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
.when(changed_buffers.len() > 0, |parent| {
|
||||
parent.child(self.render_changed_buffers(&changed_buffers, window, cx))
|
||||
})
|
||||
|
||||
@@ -68,41 +68,30 @@ impl ProfileSelector {
|
||||
|
||||
menu = menu.header("Profiles");
|
||||
for (profile_id, profile) in self.profiles.clone() {
|
||||
let documentation = match profile.name.to_lowercase().as_str() {
|
||||
"write" => Some("Get help to write anything."),
|
||||
"ask" => Some("Chat about your codebase."),
|
||||
"manual" => Some("Chat about anything; no tools."),
|
||||
_ => None,
|
||||
};
|
||||
menu = menu.toggleable_entry(
|
||||
profile.name.clone(),
|
||||
profile_id == settings.default_profile,
|
||||
icon_position,
|
||||
None,
|
||||
{
|
||||
let fs = self.fs.clone();
|
||||
let thread_store = self.thread_store.clone();
|
||||
move |_window, cx| {
|
||||
update_settings_file::<AssistantSettings>(fs.clone(), cx, {
|
||||
let profile_id = profile_id.clone();
|
||||
move |settings, _cx| {
|
||||
settings.set_profile(profile_id.clone());
|
||||
}
|
||||
});
|
||||
|
||||
let entry = ContextMenuEntry::new(profile.name.clone())
|
||||
.toggleable(icon_position, profile_id == settings.default_profile);
|
||||
|
||||
let entry = if let Some(doc_text) = documentation {
|
||||
entry.documentation_aside(move |_| Label::new(doc_text).into_any_element())
|
||||
} else {
|
||||
entry
|
||||
};
|
||||
|
||||
menu = menu.item(entry.handler({
|
||||
let fs = self.fs.clone();
|
||||
let thread_store = self.thread_store.clone();
|
||||
let profile_id = profile_id.clone();
|
||||
move |_window, cx| {
|
||||
update_settings_file::<AssistantSettings>(fs.clone(), cx, {
|
||||
let profile_id = profile_id.clone();
|
||||
move |settings, _cx| {
|
||||
settings.set_profile(profile_id.clone());
|
||||
}
|
||||
});
|
||||
|
||||
thread_store
|
||||
.update(cx, |this, cx| {
|
||||
this.load_profile_by_id(profile_id.clone(), cx);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}));
|
||||
thread_store
|
||||
.update(cx, |this, cx| {
|
||||
this.load_profile_by_id(profile_id.clone(), cx);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
menu = menu.separator();
|
||||
@@ -152,7 +141,6 @@ impl Render for ProfileSelector {
|
||||
|
||||
let this = cx.entity().clone();
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
PopoverMenu::new("profile-selector")
|
||||
.menu(move |window, cx| {
|
||||
Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
|
||||
@@ -195,7 +183,7 @@ impl Render for ProfileSelector {
|
||||
)
|
||||
.tooltip(Tooltip::text("The current model does not support tools."))
|
||||
})
|
||||
.anchor(gpui::Corner::BottomRight)
|
||||
.anchor(gpui::Corner::BottomLeft)
|
||||
.with_handle(self.menu_handle.clone())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ impl TerminalInlineAssistant {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let assist_id = prompt_editor.read(cx).id();
|
||||
let assist_id = prompt_editor.read(cx).terminal_inline_assist_id();
|
||||
match event {
|
||||
PromptEditorEvent::StartRequested => {
|
||||
self.start_assist(assist_id, cx);
|
||||
|
||||
@@ -14,21 +14,19 @@ use futures::future::Shared;
|
||||
use futures::{FutureExt, StreamExt as _};
|
||||
use git::repository::DiffType;
|
||||
use gpui::{
|
||||
AnyWindowHandle, App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task,
|
||||
WeakEntity,
|
||||
AnyWindowHandle, App, AppContext, Context, Entity, EventEmitter, SharedString, Task, WeakEntity,
|
||||
};
|
||||
use language_model::{
|
||||
ConfiguredModel, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
|
||||
LanguageModelId, LanguageModelKnownError, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
|
||||
LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent,
|
||||
ModelRequestLimitReachedError, PaymentRequiredError, RequestUsage, Role, SelectedModel,
|
||||
StopReason, TokenUsage,
|
||||
ModelRequestLimitReachedError, PaymentRequiredError, RequestUsage, Role, StopReason,
|
||||
TokenUsage,
|
||||
};
|
||||
use postage::stream::Stream as _;
|
||||
use project::Project;
|
||||
use project::git_store::{GitStore, GitStoreCheckpoint, RepositoryState};
|
||||
use prompt_store::{ModelContext, PromptBuilder};
|
||||
use prompt_store::PromptBuilder;
|
||||
use proto::Plan;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -38,11 +36,10 @@ use util::{ResultExt as _, TryFutureExt as _, post_inc};
|
||||
use uuid::Uuid;
|
||||
use zed_llm_client::CompletionMode;
|
||||
|
||||
use crate::ThreadStore;
|
||||
use crate::context::{AgentContext, ContextLoadResult, LoadedContext};
|
||||
use crate::thread_store::{
|
||||
SerializedLanguageModel, SerializedMessage, SerializedMessageSegment, SerializedThread,
|
||||
SerializedToolResult, SerializedToolUse, SharedProjectContext,
|
||||
SerializedMessage, SerializedMessageSegment, SerializedThread, SerializedToolResult,
|
||||
SerializedToolUse, SharedProjectContext,
|
||||
};
|
||||
use crate::tool_use::{PendingToolUse, ToolUse, ToolUseMetadata, ToolUseState};
|
||||
|
||||
@@ -246,16 +243,6 @@ pub enum DetailedSummaryState {
|
||||
},
|
||||
}
|
||||
|
||||
impl DetailedSummaryState {
|
||||
fn text(&self) -> Option<SharedString> {
|
||||
if let Self::Generated { text, .. } = self {
|
||||
Some(text.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct TotalTokenUsage {
|
||||
pub total: usize,
|
||||
@@ -272,11 +259,7 @@ impl TotalTokenUsage {
|
||||
#[cfg(not(debug_assertions))]
|
||||
let warning_threshold: f32 = 0.8;
|
||||
|
||||
// When the maximum is unknown because there is no selected model,
|
||||
// avoid showing the token limit warning.
|
||||
if self.max == 0 {
|
||||
TokenUsageRatio::Normal
|
||||
} else if self.total >= self.max {
|
||||
if self.total >= self.max {
|
||||
TokenUsageRatio::Exceeded
|
||||
} else if self.total as f32 / self.max as f32 >= warning_threshold {
|
||||
TokenUsageRatio::Warning
|
||||
@@ -307,9 +290,7 @@ pub struct Thread {
|
||||
updated_at: DateTime<Utc>,
|
||||
summary: Option<SharedString>,
|
||||
pending_summary: Task<Option<()>>,
|
||||
detailed_summary_task: Task<Option<()>>,
|
||||
detailed_summary_tx: postage::watch::Sender<DetailedSummaryState>,
|
||||
detailed_summary_rx: postage::watch::Receiver<DetailedSummaryState>,
|
||||
detailed_summary_state: DetailedSummaryState,
|
||||
completion_mode: Option<CompletionMode>,
|
||||
messages: Vec<Message>,
|
||||
next_message_id: MessageId,
|
||||
@@ -336,7 +317,6 @@ pub struct Thread {
|
||||
Box<dyn FnMut(&LanguageModelRequest, &[Result<LanguageModelCompletionEvent, String>])>,
|
||||
>,
|
||||
remaining_turns: u32,
|
||||
configured_model: Option<ConfiguredModel>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -355,17 +335,12 @@ impl Thread {
|
||||
system_prompt: SharedProjectContext,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let (detailed_summary_tx, detailed_summary_rx) = postage::watch::channel();
|
||||
let configured_model = LanguageModelRegistry::read_global(cx).default_model();
|
||||
|
||||
Self {
|
||||
id: ThreadId::new(),
|
||||
updated_at: Utc::now(),
|
||||
summary: None,
|
||||
pending_summary: Task::ready(None),
|
||||
detailed_summary_task: Task::ready(None),
|
||||
detailed_summary_tx,
|
||||
detailed_summary_rx,
|
||||
detailed_summary_state: DetailedSummaryState::NotGenerated,
|
||||
completion_mode: None,
|
||||
messages: Vec::new(),
|
||||
next_message_id: MessageId(0),
|
||||
@@ -395,7 +370,6 @@ impl Thread {
|
||||
last_auto_capture_at: None,
|
||||
request_callback: None,
|
||||
remaining_turns: u32::MAX,
|
||||
configured_model,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -416,30 +390,13 @@ impl Thread {
|
||||
.unwrap_or(0),
|
||||
);
|
||||
let tool_use = ToolUseState::from_serialized_messages(tools.clone(), &serialized.messages);
|
||||
let (detailed_summary_tx, detailed_summary_rx) =
|
||||
postage::watch::channel_with(serialized.detailed_summary_state);
|
||||
|
||||
let configured_model = LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
|
||||
serialized
|
||||
.model
|
||||
.and_then(|model| {
|
||||
let model = SelectedModel {
|
||||
provider: model.provider.clone().into(),
|
||||
model: model.model.clone().into(),
|
||||
};
|
||||
registry.select_model(&model, cx)
|
||||
})
|
||||
.or_else(|| registry.default_model())
|
||||
});
|
||||
|
||||
Self {
|
||||
id,
|
||||
updated_at: serialized.updated_at,
|
||||
summary: Some(serialized.summary),
|
||||
pending_summary: Task::ready(None),
|
||||
detailed_summary_task: Task::ready(None),
|
||||
detailed_summary_tx,
|
||||
detailed_summary_rx,
|
||||
detailed_summary_state: serialized.detailed_summary_state,
|
||||
completion_mode: None,
|
||||
messages: serialized
|
||||
.messages
|
||||
@@ -489,7 +446,6 @@ impl Thread {
|
||||
last_auto_capture_at: None,
|
||||
request_callback: None,
|
||||
remaining_turns: u32::MAX,
|
||||
configured_model,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -529,22 +485,6 @@ impl Thread {
|
||||
self.project_context.clone()
|
||||
}
|
||||
|
||||
pub fn get_or_init_configured_model(&mut self, cx: &App) -> Option<ConfiguredModel> {
|
||||
if self.configured_model.is_none() {
|
||||
self.configured_model = LanguageModelRegistry::read_global(cx).default_model();
|
||||
}
|
||||
self.configured_model.clone()
|
||||
}
|
||||
|
||||
pub fn configured_model(&self) -> Option<ConfiguredModel> {
|
||||
self.configured_model.clone()
|
||||
}
|
||||
|
||||
pub fn set_configured_model(&mut self, model: Option<ConfiguredModel>, cx: &mut Context<Self>) {
|
||||
self.configured_model = model;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub const DEFAULT_SUMMARY: SharedString = SharedString::new_static("New Thread");
|
||||
|
||||
pub fn summary_or_default(&self) -> SharedString {
|
||||
@@ -569,6 +509,19 @@ impl Thread {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn latest_detailed_summary_or_text(&self) -> SharedString {
|
||||
self.latest_detailed_summary()
|
||||
.unwrap_or_else(|| self.text().into())
|
||||
}
|
||||
|
||||
fn latest_detailed_summary(&self) -> Option<SharedString> {
|
||||
if let DetailedSummaryState::Generated { text, .. } = &self.detailed_summary_state {
|
||||
Some(text.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn completion_mode(&self) -> Option<CompletionMode> {
|
||||
self.completion_mode
|
||||
}
|
||||
@@ -787,32 +740,6 @@ impl Thread {
|
||||
self.tool_use.tool_result_card(id).cloned()
|
||||
}
|
||||
|
||||
/// Return tools that are both enabled and supported by the model
|
||||
pub fn available_tools(
|
||||
&self,
|
||||
cx: &App,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
) -> Vec<LanguageModelRequestTool> {
|
||||
if model.supports_tools() {
|
||||
self.tools()
|
||||
.read(cx)
|
||||
.enabled_tools(cx)
|
||||
.into_iter()
|
||||
.filter_map(|tool| {
|
||||
// Skip tools that cannot be supported
|
||||
let input_schema = tool.input_schema(model.tool_input_format()).ok()?;
|
||||
Some(LanguageModelRequestTool {
|
||||
name: tool.name(),
|
||||
description: tool.description(),
|
||||
input_schema,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
Vec::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_user_message(
|
||||
&mut self,
|
||||
text: impl Into<String>,
|
||||
@@ -988,15 +915,8 @@ impl Thread {
|
||||
initial_project_snapshot,
|
||||
cumulative_token_usage: this.cumulative_token_usage,
|
||||
request_token_usage: this.request_token_usage.clone(),
|
||||
detailed_summary_state: this.detailed_summary_rx.borrow().clone(),
|
||||
detailed_summary_state: this.detailed_summary_state.clone(),
|
||||
exceeded_window_error: this.exceeded_window_error.clone(),
|
||||
model: this
|
||||
.configured_model
|
||||
.as_ref()
|
||||
.map(|model| SerializedLanguageModel {
|
||||
provider: model.provider.id().0.to_string(),
|
||||
model: model.model.id().0.to_string(),
|
||||
}),
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1021,7 +941,30 @@ impl Thread {
|
||||
|
||||
self.remaining_turns -= 1;
|
||||
|
||||
let request = self.to_completion_request(model.clone(), cx);
|
||||
let mut request = self.to_completion_request(cx);
|
||||
request.mode = if model.supports_max_mode() {
|
||||
self.completion_mode
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if model.supports_tools() {
|
||||
request.tools = self
|
||||
.tools()
|
||||
.read(cx)
|
||||
.enabled_tools(cx)
|
||||
.into_iter()
|
||||
.filter_map(|tool| {
|
||||
// Skip tools that cannot be supported
|
||||
let input_schema = tool.input_schema(model.tool_input_format()).ok()?;
|
||||
Some(LanguageModelRequestTool {
|
||||
name: tool.name(),
|
||||
description: tool.description(),
|
||||
input_schema,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
self.stream_completion(request, model, window, cx);
|
||||
}
|
||||
@@ -1038,11 +981,7 @@ impl Thread {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn to_completion_request(
|
||||
&self,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> LanguageModelRequest {
|
||||
pub fn to_completion_request(&self, cx: &mut Context<Self>) -> LanguageModelRequest {
|
||||
let mut request = LanguageModelRequest {
|
||||
thread_id: Some(self.id.to_string()),
|
||||
prompt_id: Some(self.last_prompt_id.to_string()),
|
||||
@@ -1053,20 +992,10 @@ impl Thread {
|
||||
temperature: None,
|
||||
};
|
||||
|
||||
let available_tools = self.available_tools(cx, model.clone());
|
||||
let available_tool_names = available_tools
|
||||
.iter()
|
||||
.map(|tool| tool.name.clone())
|
||||
.collect();
|
||||
|
||||
let model_context = &ModelContext {
|
||||
available_tools: available_tool_names,
|
||||
};
|
||||
|
||||
if let Some(project_context) = self.project_context.borrow().as_ref() {
|
||||
match self
|
||||
.prompt_builder
|
||||
.generate_assistant_system_prompt(project_context, model_context)
|
||||
.generate_assistant_system_prompt(project_context)
|
||||
{
|
||||
Err(err) => {
|
||||
let message = format!("{err:?}").into();
|
||||
@@ -1146,13 +1075,6 @@ impl Thread {
|
||||
|
||||
self.attached_tracked_files_state(&mut request.messages, cx);
|
||||
|
||||
request.tools = available_tools;
|
||||
request.mode = if model.supports_max_mode() {
|
||||
self.completion_mode
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
request
|
||||
}
|
||||
|
||||
@@ -1454,7 +1376,7 @@ impl Thread {
|
||||
match result.as_ref() {
|
||||
Ok(stop_reason) => match stop_reason {
|
||||
StopReason::ToolUse => {
|
||||
let tool_uses = thread.use_pending_tools(window, cx, model.clone());
|
||||
let tool_uses = thread.use_pending_tools(window, cx);
|
||||
cx.emit(ThreadEvent::UsePendingTools { tool_uses });
|
||||
}
|
||||
StopReason::EndTurn => {}
|
||||
@@ -1594,34 +1516,25 @@ impl Thread {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn start_generating_detailed_summary_if_needed(
|
||||
&mut self,
|
||||
thread_store: WeakEntity<ThreadStore>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(last_message_id) = self.messages.last().map(|message| message.id) else {
|
||||
return;
|
||||
};
|
||||
pub fn generate_detailed_summary(&mut self, cx: &mut Context<Self>) -> Option<Task<()>> {
|
||||
let last_message_id = self.messages.last().map(|message| message.id)?;
|
||||
|
||||
match &*self.detailed_summary_rx.borrow() {
|
||||
match &self.detailed_summary_state {
|
||||
DetailedSummaryState::Generating { message_id, .. }
|
||||
| DetailedSummaryState::Generated { message_id, .. }
|
||||
if *message_id == last_message_id =>
|
||||
{
|
||||
// Already up-to-date
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let Some(ConfiguredModel { model, provider }) =
|
||||
LanguageModelRegistry::read_global(cx).thread_summary_model()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let ConfiguredModel { model, provider } =
|
||||
LanguageModelRegistry::read_global(cx).thread_summary_model()?;
|
||||
|
||||
if !provider.is_authenticated(cx) {
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
|
||||
let added_user_message = "Generate a detailed summary of this conversation. Include:\n\
|
||||
@@ -1633,24 +1546,16 @@ impl Thread {
|
||||
|
||||
let request = self.to_summarize_request(added_user_message.into());
|
||||
|
||||
*self.detailed_summary_tx.borrow_mut() = DetailedSummaryState::Generating {
|
||||
message_id: last_message_id,
|
||||
};
|
||||
|
||||
// Replace the detailed summarization task if there is one, cancelling it. It would probably
|
||||
// be better to allow the old task to complete, but this would require logic for choosing
|
||||
// which result to prefer (the old task could complete after the new one, resulting in a
|
||||
// stale summary).
|
||||
self.detailed_summary_task = cx.spawn(async move |thread, cx| {
|
||||
let task = cx.spawn(async move |thread, cx| {
|
||||
let stream = model.stream_completion_text(request, &cx);
|
||||
let Some(mut messages) = stream.await.log_err() else {
|
||||
thread
|
||||
.update(cx, |thread, _cx| {
|
||||
*thread.detailed_summary_tx.borrow_mut() =
|
||||
DetailedSummaryState::NotGenerated;
|
||||
.update(cx, |this, _cx| {
|
||||
this.detailed_summary_state = DetailedSummaryState::NotGenerated;
|
||||
})
|
||||
.ok()?;
|
||||
return None;
|
||||
.log_err();
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
let mut new_detailed_summary = String::new();
|
||||
@@ -1662,56 +1567,25 @@ impl Thread {
|
||||
}
|
||||
|
||||
thread
|
||||
.update(cx, |thread, _cx| {
|
||||
*thread.detailed_summary_tx.borrow_mut() = DetailedSummaryState::Generated {
|
||||
.update(cx, |this, _cx| {
|
||||
this.detailed_summary_state = DetailedSummaryState::Generated {
|
||||
text: new_detailed_summary.into(),
|
||||
message_id: last_message_id,
|
||||
};
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
// Save thread so its summary can be reused later
|
||||
if let Some(thread) = thread.upgrade() {
|
||||
if let Ok(Ok(save_task)) = cx.update(|cx| {
|
||||
thread_store
|
||||
.update(cx, |thread_store, cx| thread_store.save_thread(&thread, cx))
|
||||
}) {
|
||||
save_task.await.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
Some(())
|
||||
.log_err();
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn wait_for_detailed_summary_or_text(
|
||||
this: &Entity<Self>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Option<SharedString> {
|
||||
let mut detailed_summary_rx = this
|
||||
.read_with(cx, |this, _cx| this.detailed_summary_rx.clone())
|
||||
.ok()?;
|
||||
loop {
|
||||
match detailed_summary_rx.recv().await? {
|
||||
DetailedSummaryState::Generating { .. } => {}
|
||||
DetailedSummaryState::NotGenerated => {
|
||||
return this.read_with(cx, |this, _cx| this.text().into()).ok();
|
||||
}
|
||||
DetailedSummaryState::Generated { text, .. } => return Some(text),
|
||||
}
|
||||
}
|
||||
}
|
||||
self.detailed_summary_state = DetailedSummaryState::Generating {
|
||||
message_id: last_message_id,
|
||||
};
|
||||
|
||||
pub fn latest_detailed_summary_or_text(&self) -> SharedString {
|
||||
self.detailed_summary_rx
|
||||
.borrow()
|
||||
.text()
|
||||
.unwrap_or_else(|| self.text().into())
|
||||
Some(task)
|
||||
}
|
||||
|
||||
pub fn is_generating_detailed_summary(&self) -> bool {
|
||||
matches!(
|
||||
&*self.detailed_summary_rx.borrow(),
|
||||
self.detailed_summary_state,
|
||||
DetailedSummaryState::Generating { .. }
|
||||
)
|
||||
}
|
||||
@@ -1720,10 +1594,9 @@ impl Thread {
|
||||
&mut self,
|
||||
window: Option<AnyWindowHandle>,
|
||||
cx: &mut Context<Self>,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
) -> Vec<PendingToolUse> {
|
||||
self.auto_capture_telemetry(cx);
|
||||
let request = self.to_completion_request(model, cx);
|
||||
let request = self.to_completion_request(cx);
|
||||
let messages = Arc::new(request.messages);
|
||||
let pending_tool_uses = self
|
||||
.tool_use
|
||||
@@ -1778,7 +1651,7 @@ impl Thread {
|
||||
tool_use_id.clone(),
|
||||
tool_name,
|
||||
Err(anyhow!("Error parsing input JSON: {error}")),
|
||||
self.configured_model.as_ref(),
|
||||
cx,
|
||||
);
|
||||
let ui_text = if let Some(pending_tool_use) = &pending_tool_use {
|
||||
pending_tool_use.ui_text.clone()
|
||||
@@ -1853,7 +1726,7 @@ impl Thread {
|
||||
tool_use_id.clone(),
|
||||
tool_name,
|
||||
output,
|
||||
thread.configured_model.as_ref(),
|
||||
cx,
|
||||
);
|
||||
thread.tool_finished(tool_use_id, pending_tool_use, false, window, cx);
|
||||
})
|
||||
@@ -1871,9 +1744,10 @@ impl Thread {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.all_tools_finished() {
|
||||
if let Some(ConfiguredModel { model, .. }) = self.configured_model.as_ref() {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
if let Some(ConfiguredModel { model, .. }) = model_registry.default_model() {
|
||||
if !canceled {
|
||||
self.send_to_model(model.clone(), window, cx);
|
||||
self.send_to_model(model, window, cx);
|
||||
}
|
||||
self.auto_capture_telemetry(cx);
|
||||
}
|
||||
@@ -1910,14 +1784,6 @@ impl Thread {
|
||||
canceled
|
||||
}
|
||||
|
||||
/// Signals that any in-progress editing should be canceled.
|
||||
///
|
||||
/// This method is used to notify listeners (like ActiveThread) that
|
||||
/// they should cancel any editing operations.
|
||||
pub fn cancel_editing(&mut self, cx: &mut Context<Self>) {
|
||||
cx.emit(ThreadEvent::CancelEditing);
|
||||
}
|
||||
|
||||
pub fn feedback(&self) -> Option<ThreadFeedback> {
|
||||
self.feedback
|
||||
}
|
||||
@@ -2298,8 +2164,8 @@ impl Thread {
|
||||
self.cumulative_token_usage
|
||||
}
|
||||
|
||||
pub fn token_usage_up_to_message(&self, message_id: MessageId) -> TotalTokenUsage {
|
||||
let Some(model) = self.configured_model.as_ref() else {
|
||||
pub fn token_usage_up_to_message(&self, message_id: MessageId, cx: &App) -> TotalTokenUsage {
|
||||
let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
|
||||
return TotalTokenUsage::default();
|
||||
};
|
||||
|
||||
@@ -2327,17 +2193,20 @@ impl Thread {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn total_token_usage(&self) -> Option<TotalTokenUsage> {
|
||||
let model = self.configured_model.as_ref()?;
|
||||
pub fn total_token_usage(&self, cx: &App) -> TotalTokenUsage {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
let Some(model) = model_registry.default_model() else {
|
||||
return TotalTokenUsage::default();
|
||||
};
|
||||
|
||||
let max = model.model.max_token_count();
|
||||
|
||||
if let Some(exceeded_error) = &self.exceeded_window_error {
|
||||
if model.model.id() == exceeded_error.model_id {
|
||||
return Some(TotalTokenUsage {
|
||||
return TotalTokenUsage {
|
||||
total: exceeded_error.token_count,
|
||||
max,
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2346,7 +2215,7 @@ impl Thread {
|
||||
.unwrap_or_default()
|
||||
.total_tokens() as usize;
|
||||
|
||||
Some(TotalTokenUsage { total, max })
|
||||
TotalTokenUsage { total, max }
|
||||
}
|
||||
|
||||
fn token_usage_at_last_message(&self) -> Option<TokenUsage> {
|
||||
@@ -2377,12 +2246,8 @@ impl Thread {
|
||||
"Permission to run tool action denied by user"
|
||||
));
|
||||
|
||||
self.tool_use.insert_tool_output(
|
||||
tool_use_id.clone(),
|
||||
tool_name,
|
||||
err,
|
||||
self.configured_model.as_ref(),
|
||||
);
|
||||
self.tool_use
|
||||
.insert_tool_output(tool_use_id.clone(), tool_name, err, cx);
|
||||
self.tool_finished(tool_use_id.clone(), None, true, window, cx);
|
||||
}
|
||||
}
|
||||
@@ -2437,7 +2302,6 @@ pub enum ThreadEvent {
|
||||
},
|
||||
CheckpointChanged,
|
||||
ToolConfirmationNeeded,
|
||||
CancelEditing,
|
||||
}
|
||||
|
||||
impl EventEmitter<ThreadEvent> for Thread {}
|
||||
@@ -2452,11 +2316,9 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::{ThreadStore, context::load_context, context_store::ContextStore, thread_store};
|
||||
use assistant_settings::AssistantSettings;
|
||||
use assistant_tool::ToolRegistry;
|
||||
use context_server::ContextServerSettings;
|
||||
use editor::EditorSettings;
|
||||
use gpui::TestAppContext;
|
||||
use language_model::fake_provider::FakeLanguageModel;
|
||||
use project::{FakeFs, Project};
|
||||
use prompt_store::PromptBuilder;
|
||||
use serde_json::json;
|
||||
@@ -2476,7 +2338,7 @@ mod tests {
|
||||
)
|
||||
.await;
|
||||
|
||||
let (_workspace, _thread_store, thread, context_store, model) =
|
||||
let (_workspace, _thread_store, thread, context_store) =
|
||||
setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
add_file_to_context(&project, &context_store, "test/code.rs", cx)
|
||||
@@ -2527,9 +2389,7 @@ fn main() {{
|
||||
assert_eq!(message.loaded_context.text, expected_context);
|
||||
|
||||
// Check message in request
|
||||
let request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(model.clone(), cx)
|
||||
});
|
||||
let request = thread.update(cx, |thread, cx| thread.to_completion_request(cx));
|
||||
|
||||
assert_eq!(request.messages.len(), 2);
|
||||
let expected_full_message = format!("{}Please explain this code", expected_context);
|
||||
@@ -2550,7 +2410,7 @@ fn main() {{
|
||||
)
|
||||
.await;
|
||||
|
||||
let (_, _thread_store, thread, context_store, model) =
|
||||
let (_, _thread_store, thread, context_store) =
|
||||
setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// First message with context 1
|
||||
@@ -2621,9 +2481,7 @@ fn main() {{
|
||||
assert!(message3.loaded_context.text.contains("file3.rs"));
|
||||
|
||||
// Check entire request to make sure all contexts are properly included
|
||||
let request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(model.clone(), cx)
|
||||
});
|
||||
let request = thread.update(cx, |thread, cx| thread.to_completion_request(cx));
|
||||
|
||||
// The request should contain all 3 messages
|
||||
assert_eq!(request.messages.len(), 4);
|
||||
@@ -2652,7 +2510,7 @@ fn main() {{
|
||||
)
|
||||
.await;
|
||||
|
||||
let (_, _thread_store, thread, _context_store, model) =
|
||||
let (_, _thread_store, thread, _context_store) =
|
||||
setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Insert user message without any context (empty context vector)
|
||||
@@ -2678,9 +2536,7 @@ fn main() {{
|
||||
assert_eq!(message.loaded_context.text, "");
|
||||
|
||||
// Check message in request
|
||||
let request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(model.clone(), cx)
|
||||
});
|
||||
let request = thread.update(cx, |thread, cx| thread.to_completion_request(cx));
|
||||
|
||||
assert_eq!(request.messages.len(), 2);
|
||||
assert_eq!(
|
||||
@@ -2703,9 +2559,7 @@ fn main() {{
|
||||
assert_eq!(message2.loaded_context.text, "");
|
||||
|
||||
// Check that both messages appear in the request
|
||||
let request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(model.clone(), cx)
|
||||
});
|
||||
let request = thread.update(cx, |thread, cx| thread.to_completion_request(cx));
|
||||
|
||||
assert_eq!(request.messages.len(), 3);
|
||||
assert_eq!(
|
||||
@@ -2728,7 +2582,7 @@ fn main() {{
|
||||
)
|
||||
.await;
|
||||
|
||||
let (_workspace, _thread_store, thread, context_store, model) =
|
||||
let (_workspace, _thread_store, thread, context_store) =
|
||||
setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Open buffer and add it to context
|
||||
@@ -2747,9 +2601,7 @@ fn main() {{
|
||||
});
|
||||
|
||||
// Create a request and check that it doesn't have a stale buffer warning yet
|
||||
let initial_request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(model.clone(), cx)
|
||||
});
|
||||
let initial_request = thread.update(cx, |thread, cx| thread.to_completion_request(cx));
|
||||
|
||||
// Make sure we don't have a stale file warning yet
|
||||
let has_stale_warning = initial_request.messages.iter().any(|msg| {
|
||||
@@ -2782,9 +2634,7 @@ fn main() {{
|
||||
});
|
||||
|
||||
// Create a new request and check for the stale buffer warning
|
||||
let new_request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(model.clone(), cx)
|
||||
});
|
||||
let new_request = thread.update(cx, |thread, cx| thread.to_completion_request(cx));
|
||||
|
||||
// We should have a stale file warning as the last message
|
||||
let last_message = new_request
|
||||
@@ -2814,11 +2664,9 @@ fn main() {{
|
||||
prompt_store::init(cx);
|
||||
thread_store::init(cx);
|
||||
workspace::init_settings(cx);
|
||||
language_model::init_settings(cx);
|
||||
ThemeSettings::register(cx);
|
||||
ContextServerSettings::register(cx);
|
||||
EditorSettings::register(cx);
|
||||
ToolRegistry::default_global(cx);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2840,7 +2688,6 @@ fn main() {{
|
||||
Entity<ThreadStore>,
|
||||
Entity<Thread>,
|
||||
Entity<ContextStore>,
|
||||
Arc<dyn LanguageModel>,
|
||||
) {
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
@@ -2861,10 +2708,7 @@ fn main() {{
|
||||
let thread = thread_store.update(cx, |store, cx| store.create_thread(cx));
|
||||
let context_store = cx.new(|_cx| ContextStore::new(project.downgrade(), None));
|
||||
|
||||
let model = FakeLanguageModel::default();
|
||||
let model: Arc<dyn LanguageModel> = Arc::new(model);
|
||||
|
||||
(workspace, thread_store, thread, context_store, model)
|
||||
(workspace, thread_store, thread, context_store)
|
||||
}
|
||||
|
||||
async fn add_file_to_context(
|
||||
|
||||
@@ -270,9 +270,9 @@ impl ThreadHistory {
|
||||
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(entry) = self.get_match(self.selected_index) {
|
||||
let task_result = match entry {
|
||||
HistoryEntry::Thread(thread) => self.assistant_panel.update(cx, move |this, cx| {
|
||||
this.open_thread_by_id(&thread.id, window, cx)
|
||||
}),
|
||||
HistoryEntry::Thread(thread) => self
|
||||
.assistant_panel
|
||||
.update(cx, move |this, cx| this.open_thread(&thread.id, window, cx)),
|
||||
HistoryEntry::Context(context) => {
|
||||
self.assistant_panel.update(cx, move |this, cx| {
|
||||
this.open_saved_prompt_editor(context.path.clone(), window, cx)
|
||||
@@ -525,8 +525,7 @@ impl RenderOnce for PastThread {
|
||||
move |_event, window, cx| {
|
||||
assistant_panel
|
||||
.update(cx, |this, cx| {
|
||||
this.open_thread_by_id(&id, window, cx)
|
||||
.detach_and_log_err(cx);
|
||||
this.open_thread(&id, window, cx).detach_and_log_err(cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
@@ -640,14 +640,6 @@ pub struct SerializedThread {
|
||||
pub detailed_summary_state: DetailedSummaryState,
|
||||
#[serde(default)]
|
||||
pub exceeded_window_error: Option<ExceededWindowError>,
|
||||
#[serde(default)]
|
||||
pub model: Option<SerializedLanguageModel>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct SerializedLanguageModel {
|
||||
pub provider: String,
|
||||
pub model: String,
|
||||
}
|
||||
|
||||
impl SerializedThread {
|
||||
@@ -782,7 +774,6 @@ impl LegacySerializedThread {
|
||||
request_token_usage: Vec::new(),
|
||||
detailed_summary_state: DetailedSummaryState::default(),
|
||||
exceeded_window_error: None,
|
||||
model: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use futures::FutureExt as _;
|
||||
use futures::future::Shared;
|
||||
use gpui::{App, Entity, SharedString, Task};
|
||||
use language_model::{
|
||||
ConfiguredModel, LanguageModel, LanguageModelRequestMessage, LanguageModelToolResult,
|
||||
LanguageModel, LanguageModelRegistry, LanguageModelRequestMessage, LanguageModelToolResult,
|
||||
LanguageModelToolUse, LanguageModelToolUseId, MessageContent, Role,
|
||||
};
|
||||
use ui::IconName;
|
||||
@@ -353,7 +353,7 @@ impl ToolUseState {
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
tool_name: Arc<str>,
|
||||
output: Result<String>,
|
||||
configured_model: Option<&ConfiguredModel>,
|
||||
cx: &App,
|
||||
) -> Option<PendingToolUse> {
|
||||
let metadata = self.tool_use_metadata_by_id.remove(&tool_use_id);
|
||||
|
||||
@@ -373,10 +373,13 @@ impl ToolUseState {
|
||||
|
||||
match output {
|
||||
Ok(tool_result) => {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
|
||||
const BYTES_PER_TOKEN_ESTIMATE: usize = 3;
|
||||
|
||||
// Protect from clearly large output
|
||||
let tool_output_limit = configured_model
|
||||
let tool_output_limit = model_registry
|
||||
.default_model()
|
||||
.map(|model| model.model.max_token_count() * BYTES_PER_TOKEN_ESTIMATE)
|
||||
.unwrap_or(usize::MAX);
|
||||
|
||||
|
||||
@@ -1,23 +1,13 @@
|
||||
use std::{ops::Range, path::Path, rc::Rc, sync::Arc, time::Duration};
|
||||
use std::{rc::Rc, time::Duration};
|
||||
|
||||
use file_icons::FileIcons;
|
||||
use futures::FutureExt as _;
|
||||
use gpui::{
|
||||
Animation, AnimationExt as _, AnyView, ClickEvent, Entity, Image, MouseButton, Task,
|
||||
pulsating_between,
|
||||
};
|
||||
use language_model::LanguageModelImage;
|
||||
use gpui::{Animation, AnimationExt as _, ClickEvent, Entity, MouseButton, pulsating_between};
|
||||
use project::Project;
|
||||
use prompt_store::PromptStore;
|
||||
use rope::Point;
|
||||
use text::OffsetRangeExt;
|
||||
use ui::{IconButtonShape, Tooltip, prelude::*, tooltip_container};
|
||||
|
||||
use crate::context::{
|
||||
AgentContext, AgentContextHandle, ContextId, ContextKind, DirectoryContext,
|
||||
DirectoryContextHandle, FetchedUrlContext, FileContext, FileContextHandle, ImageContext,
|
||||
ImageStatus, RulesContext, RulesContextHandle, SelectionContext, SelectionContextHandle,
|
||||
SymbolContext, SymbolContextHandle, ThreadContext, ThreadContextHandle,
|
||||
};
|
||||
use crate::context::{AgentContext, ContextKind, ImageStatus};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub enum ContextPill {
|
||||
@@ -82,7 +72,7 @@ impl ContextPill {
|
||||
|
||||
pub fn id(&self) -> ElementId {
|
||||
match self {
|
||||
Self::Added { context, .. } => context.handle.element_id("context-pill".into()),
|
||||
Self::Added { context, .. } => context.context.element_id("context-pill".into()),
|
||||
Self::Suggested { .. } => "suggested-context-pill".into(),
|
||||
}
|
||||
}
|
||||
@@ -175,11 +165,16 @@ impl RenderOnce for ContextPill {
|
||||
.map(|element| match &context.status {
|
||||
ContextStatus::Ready => element
|
||||
.when_some(
|
||||
context.render_hover.as_ref(),
|
||||
|element, render_hover| {
|
||||
let render_hover = render_hover.clone();
|
||||
element.hoverable_tooltip(move |window, cx| {
|
||||
render_hover(window, cx)
|
||||
context.render_preview.as_ref(),
|
||||
|element, render_preview| {
|
||||
element.hoverable_tooltip({
|
||||
let render_preview = render_preview.clone();
|
||||
move |_, cx| {
|
||||
cx.new(|_| ContextPillPreview {
|
||||
render_preview: render_preview.clone(),
|
||||
})
|
||||
.into()
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
@@ -202,7 +197,7 @@ impl RenderOnce for ContextPill {
|
||||
.when_some(on_remove.as_ref(), |element, on_remove| {
|
||||
element.child(
|
||||
IconButton::new(
|
||||
context.handle.element_id("remove".into()),
|
||||
context.context.element_id("remove".into()),
|
||||
IconName::Close,
|
||||
)
|
||||
.shape(IconButtonShape::Square)
|
||||
@@ -267,16 +262,18 @@ pub enum ContextStatus {
|
||||
Error { message: SharedString },
|
||||
}
|
||||
|
||||
#[derive(RegisterComponent)]
|
||||
// TODO: Component commented out due to new dependency on `Project`.
|
||||
//
|
||||
// #[derive(RegisterComponent)]
|
||||
pub struct AddedContext {
|
||||
pub handle: AgentContextHandle,
|
||||
pub context: AgentContext,
|
||||
pub kind: ContextKind,
|
||||
pub name: SharedString,
|
||||
pub parent: Option<SharedString>,
|
||||
pub tooltip: Option<SharedString>,
|
||||
pub icon_path: Option<SharedString>,
|
||||
pub status: ContextStatus,
|
||||
pub render_hover: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyView + 'static>>,
|
||||
pub render_preview: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyElement + 'static>>,
|
||||
}
|
||||
|
||||
impl AddedContext {
|
||||
@@ -284,430 +281,221 @@ impl AddedContext {
|
||||
/// `None` if `DirectoryContext` or `RulesContext` no longer exist.
|
||||
///
|
||||
/// TODO: `None` cases are unremovable from `ContextStore` and so are a very minor memory leak.
|
||||
pub fn new_pending(
|
||||
handle: AgentContextHandle,
|
||||
pub fn new(
|
||||
context: AgentContext,
|
||||
prompt_store: Option<&Entity<PromptStore>>,
|
||||
project: &Project,
|
||||
cx: &App,
|
||||
) -> Option<AddedContext> {
|
||||
match handle {
|
||||
AgentContextHandle::File(handle) => Self::pending_file(handle, cx),
|
||||
AgentContextHandle::Directory(handle) => Self::pending_directory(handle, project, cx),
|
||||
AgentContextHandle::Symbol(handle) => Self::pending_symbol(handle, cx),
|
||||
AgentContextHandle::Selection(handle) => Self::pending_selection(handle, cx),
|
||||
AgentContextHandle::FetchedUrl(handle) => Some(Self::fetched_url(handle)),
|
||||
AgentContextHandle::Thread(handle) => Some(Self::pending_thread(handle, cx)),
|
||||
AgentContextHandle::Rules(handle) => Self::pending_rules(handle, prompt_store, cx),
|
||||
AgentContextHandle::Image(handle) => Some(Self::image(handle)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_attached(context: &AgentContext, cx: &App) -> AddedContext {
|
||||
match context {
|
||||
AgentContext::File(context) => Self::attached_file(context, cx),
|
||||
AgentContext::Directory(context) => Self::attached_directory(context),
|
||||
AgentContext::Symbol(context) => Self::attached_symbol(context, cx),
|
||||
AgentContext::Selection(context) => Self::attached_selection(context, cx),
|
||||
AgentContext::FetchedUrl(context) => Self::fetched_url(context.clone()),
|
||||
AgentContext::Thread(context) => Self::attached_thread(context),
|
||||
AgentContext::Rules(context) => Self::attached_rules(context),
|
||||
AgentContext::Image(context) => Self::image(context.clone()),
|
||||
}
|
||||
}
|
||||
AgentContext::File(ref file_context) => {
|
||||
let full_path = file_context.buffer.read(cx).file()?.full_path(cx);
|
||||
let full_path_string: SharedString =
|
||||
full_path.to_string_lossy().into_owned().into();
|
||||
let name = full_path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned().into())
|
||||
.unwrap_or_else(|| full_path_string.clone());
|
||||
let parent = full_path
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.map(|n| n.to_string_lossy().into_owned().into());
|
||||
Some(AddedContext {
|
||||
kind: ContextKind::File,
|
||||
name,
|
||||
parent,
|
||||
tooltip: Some(full_path_string),
|
||||
icon_path: FileIcons::get_icon(&full_path, cx),
|
||||
status: ContextStatus::Ready,
|
||||
render_preview: None,
|
||||
context,
|
||||
})
|
||||
}
|
||||
|
||||
fn pending_file(handle: FileContextHandle, cx: &App) -> Option<AddedContext> {
|
||||
let full_path = handle.buffer.read(cx).file()?.full_path(cx);
|
||||
Some(Self::file(handle, &full_path, cx))
|
||||
}
|
||||
AgentContext::Directory(ref directory_context) => {
|
||||
let worktree = project
|
||||
.worktree_for_entry(directory_context.entry_id, cx)?
|
||||
.read(cx);
|
||||
let entry = worktree.entry_for_id(directory_context.entry_id)?;
|
||||
let full_path = worktree.full_path(&entry.path);
|
||||
let full_path_string: SharedString =
|
||||
full_path.to_string_lossy().into_owned().into();
|
||||
let name = full_path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned().into())
|
||||
.unwrap_or_else(|| full_path_string.clone());
|
||||
let parent = full_path
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.map(|n| n.to_string_lossy().into_owned().into());
|
||||
Some(AddedContext {
|
||||
kind: ContextKind::Directory,
|
||||
name,
|
||||
parent,
|
||||
tooltip: Some(full_path_string),
|
||||
icon_path: None,
|
||||
status: ContextStatus::Ready,
|
||||
render_preview: None,
|
||||
context,
|
||||
})
|
||||
}
|
||||
|
||||
fn attached_file(context: &FileContext, cx: &App) -> AddedContext {
|
||||
Self::file(context.handle.clone(), &context.full_path, cx)
|
||||
}
|
||||
AgentContext::Symbol(ref symbol_context) => Some(AddedContext {
|
||||
kind: ContextKind::Symbol,
|
||||
name: symbol_context.symbol.clone(),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: ContextStatus::Ready,
|
||||
render_preview: None,
|
||||
context,
|
||||
}),
|
||||
|
||||
fn file(handle: FileContextHandle, full_path: &Path, cx: &App) -> AddedContext {
|
||||
let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into();
|
||||
let name = full_path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned().into())
|
||||
.unwrap_or_else(|| full_path_string.clone());
|
||||
let parent = full_path
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.map(|n| n.to_string_lossy().into_owned().into());
|
||||
AddedContext {
|
||||
kind: ContextKind::File,
|
||||
name,
|
||||
parent,
|
||||
tooltip: Some(full_path_string),
|
||||
icon_path: FileIcons::get_icon(&full_path, cx),
|
||||
status: ContextStatus::Ready,
|
||||
render_hover: None,
|
||||
handle: AgentContextHandle::File(handle),
|
||||
}
|
||||
}
|
||||
AgentContext::Selection(ref selection_context) => {
|
||||
let buffer = selection_context.buffer.read(cx);
|
||||
let full_path = buffer.file()?.full_path(cx);
|
||||
let mut full_path_string = full_path.to_string_lossy().into_owned();
|
||||
let mut name = full_path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| full_path_string.clone());
|
||||
|
||||
fn pending_directory(
|
||||
handle: DirectoryContextHandle,
|
||||
project: &Project,
|
||||
cx: &App,
|
||||
) -> Option<AddedContext> {
|
||||
let worktree = project.worktree_for_entry(handle.entry_id, cx)?.read(cx);
|
||||
let entry = worktree.entry_for_id(handle.entry_id)?;
|
||||
let full_path = worktree.full_path(&entry.path);
|
||||
Some(Self::directory(handle, &full_path))
|
||||
}
|
||||
let line_range = selection_context.range.to_point(&buffer.snapshot());
|
||||
|
||||
fn attached_directory(context: &DirectoryContext) -> AddedContext {
|
||||
Self::directory(context.handle.clone(), &context.full_path)
|
||||
}
|
||||
let line_range_text =
|
||||
format!(" ({}-{})", line_range.start.row + 1, line_range.end.row + 1);
|
||||
|
||||
fn directory(handle: DirectoryContextHandle, full_path: &Path) -> AddedContext {
|
||||
let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into();
|
||||
let name = full_path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned().into())
|
||||
.unwrap_or_else(|| full_path_string.clone());
|
||||
let parent = full_path
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.map(|n| n.to_string_lossy().into_owned().into());
|
||||
AddedContext {
|
||||
kind: ContextKind::Directory,
|
||||
name,
|
||||
parent,
|
||||
tooltip: Some(full_path_string),
|
||||
icon_path: None,
|
||||
status: ContextStatus::Ready,
|
||||
render_hover: None,
|
||||
handle: AgentContextHandle::Directory(handle),
|
||||
}
|
||||
}
|
||||
full_path_string.push_str(&line_range_text);
|
||||
name.push_str(&line_range_text);
|
||||
|
||||
fn pending_symbol(handle: SymbolContextHandle, cx: &App) -> Option<AddedContext> {
|
||||
let excerpt =
|
||||
ContextFileExcerpt::new(&handle.full_path(cx)?, handle.enclosing_line_range(cx), cx);
|
||||
Some(AddedContext {
|
||||
kind: ContextKind::Symbol,
|
||||
name: handle.symbol.clone(),
|
||||
parent: Some(excerpt.file_name_and_range.clone()),
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: ContextStatus::Ready,
|
||||
render_hover: {
|
||||
let handle = handle.clone();
|
||||
Some(Rc::new(move |_, cx| {
|
||||
excerpt.hover_view(handle.text(cx), cx).into()
|
||||
}))
|
||||
},
|
||||
handle: AgentContextHandle::Symbol(handle),
|
||||
})
|
||||
}
|
||||
let parent = full_path
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.map(|n| n.to_string_lossy().into_owned().into());
|
||||
|
||||
fn attached_symbol(context: &SymbolContext, cx: &App) -> AddedContext {
|
||||
let excerpt = ContextFileExcerpt::new(&context.full_path, context.line_range.clone(), cx);
|
||||
AddedContext {
|
||||
kind: ContextKind::Symbol,
|
||||
name: context.handle.symbol.clone(),
|
||||
parent: Some(excerpt.file_name_and_range.clone()),
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: ContextStatus::Ready,
|
||||
render_hover: {
|
||||
let text = context.text.clone();
|
||||
Some(Rc::new(move |_, cx| {
|
||||
excerpt.hover_view(text.clone(), cx).into()
|
||||
}))
|
||||
},
|
||||
handle: AgentContextHandle::Symbol(context.handle.clone()),
|
||||
}
|
||||
}
|
||||
Some(AddedContext {
|
||||
kind: ContextKind::Selection,
|
||||
name: name.into(),
|
||||
parent,
|
||||
tooltip: None,
|
||||
icon_path: FileIcons::get_icon(&full_path, cx),
|
||||
status: ContextStatus::Ready,
|
||||
render_preview: None,
|
||||
/*
|
||||
render_preview: Some(Rc::new({
|
||||
let content = selection_context.text.clone();
|
||||
move |_, cx| {
|
||||
div()
|
||||
.id("context-pill-selection-preview")
|
||||
.overflow_scroll()
|
||||
.max_w_128()
|
||||
.max_h_96()
|
||||
.child(Label::new(content.clone()).buffer_font(cx))
|
||||
.into_any_element()
|
||||
}
|
||||
})),
|
||||
*/
|
||||
context,
|
||||
})
|
||||
}
|
||||
|
||||
fn pending_selection(handle: SelectionContextHandle, cx: &App) -> Option<AddedContext> {
|
||||
let excerpt = ContextFileExcerpt::new(&handle.full_path(cx)?, handle.line_range(cx), cx);
|
||||
Some(AddedContext {
|
||||
kind: ContextKind::Selection,
|
||||
name: excerpt.file_name_and_range.clone(),
|
||||
parent: excerpt.parent_name.clone(),
|
||||
tooltip: None,
|
||||
icon_path: excerpt.icon_path.clone(),
|
||||
status: ContextStatus::Ready,
|
||||
render_hover: {
|
||||
let handle = handle.clone();
|
||||
Some(Rc::new(move |_, cx| {
|
||||
excerpt.hover_view(handle.text(cx), cx).into()
|
||||
}))
|
||||
},
|
||||
handle: AgentContextHandle::Selection(handle),
|
||||
})
|
||||
}
|
||||
AgentContext::FetchedUrl(ref fetched_url_context) => Some(AddedContext {
|
||||
kind: ContextKind::FetchedUrl,
|
||||
name: fetched_url_context.url.clone(),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: ContextStatus::Ready,
|
||||
render_preview: None,
|
||||
context,
|
||||
}),
|
||||
|
||||
fn attached_selection(context: &SelectionContext, cx: &App) -> AddedContext {
|
||||
let excerpt = ContextFileExcerpt::new(&context.full_path, context.line_range.clone(), cx);
|
||||
AddedContext {
|
||||
kind: ContextKind::Selection,
|
||||
name: excerpt.file_name_and_range.clone(),
|
||||
parent: excerpt.parent_name.clone(),
|
||||
tooltip: None,
|
||||
icon_path: excerpt.icon_path.clone(),
|
||||
status: ContextStatus::Ready,
|
||||
render_hover: {
|
||||
let text = context.text.clone();
|
||||
Some(Rc::new(move |_, cx| {
|
||||
excerpt.hover_view(text.clone(), cx).into()
|
||||
}))
|
||||
},
|
||||
handle: AgentContextHandle::Selection(context.handle.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn fetched_url(context: FetchedUrlContext) -> AddedContext {
|
||||
AddedContext {
|
||||
kind: ContextKind::FetchedUrl,
|
||||
name: context.url.clone(),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: ContextStatus::Ready,
|
||||
render_hover: None,
|
||||
handle: AgentContextHandle::FetchedUrl(context),
|
||||
}
|
||||
}
|
||||
|
||||
fn pending_thread(handle: ThreadContextHandle, cx: &App) -> AddedContext {
|
||||
AddedContext {
|
||||
kind: ContextKind::Thread,
|
||||
name: handle.title(cx),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: if handle.thread.read(cx).is_generating_detailed_summary() {
|
||||
ContextStatus::Loading {
|
||||
message: "Summarizing…".into(),
|
||||
}
|
||||
} else {
|
||||
ContextStatus::Ready
|
||||
},
|
||||
render_hover: {
|
||||
let thread = handle.thread.clone();
|
||||
Some(Rc::new(move |_, cx| {
|
||||
let text = thread.read(cx).latest_detailed_summary_or_text();
|
||||
ContextPillHover::new_text(text.clone(), cx).into()
|
||||
}))
|
||||
},
|
||||
handle: AgentContextHandle::Thread(handle),
|
||||
}
|
||||
}
|
||||
|
||||
fn attached_thread(context: &ThreadContext) -> AddedContext {
|
||||
AddedContext {
|
||||
kind: ContextKind::Thread,
|
||||
name: context.title.clone(),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: ContextStatus::Ready,
|
||||
render_hover: {
|
||||
let text = context.text.clone();
|
||||
Some(Rc::new(move |_, cx| {
|
||||
ContextPillHover::new_text(text.clone(), cx).into()
|
||||
}))
|
||||
},
|
||||
handle: AgentContextHandle::Thread(context.handle.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn pending_rules(
|
||||
handle: RulesContextHandle,
|
||||
prompt_store: Option<&Entity<PromptStore>>,
|
||||
cx: &App,
|
||||
) -> Option<AddedContext> {
|
||||
let title = prompt_store
|
||||
.as_ref()?
|
||||
.read(cx)
|
||||
.metadata(handle.prompt_id.into())?
|
||||
.title
|
||||
.unwrap_or_else(|| "Unnamed Rule".into());
|
||||
Some(AddedContext {
|
||||
kind: ContextKind::Rules,
|
||||
name: title.clone(),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: ContextStatus::Ready,
|
||||
render_hover: None,
|
||||
handle: AgentContextHandle::Rules(handle),
|
||||
})
|
||||
}
|
||||
|
||||
fn attached_rules(context: &RulesContext) -> AddedContext {
|
||||
let title = context
|
||||
.title
|
||||
.clone()
|
||||
.unwrap_or_else(|| "Unnamed Rule".into());
|
||||
AddedContext {
|
||||
kind: ContextKind::Rules,
|
||||
name: title,
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: ContextStatus::Ready,
|
||||
render_hover: {
|
||||
let text = context.text.clone();
|
||||
Some(Rc::new(move |_, cx| {
|
||||
ContextPillHover::new_text(text.clone(), cx).into()
|
||||
}))
|
||||
},
|
||||
handle: AgentContextHandle::Rules(context.handle.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn image(context: ImageContext) -> AddedContext {
|
||||
AddedContext {
|
||||
kind: ContextKind::Image,
|
||||
name: "Image".into(),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: match context.status() {
|
||||
ImageStatus::Loading => ContextStatus::Loading {
|
||||
message: "Loading…".into(),
|
||||
AgentContext::Thread(ref thread_context) => Some(AddedContext {
|
||||
kind: ContextKind::Thread,
|
||||
name: thread_context.name(cx),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: if thread_context
|
||||
.thread
|
||||
.read(cx)
|
||||
.is_generating_detailed_summary()
|
||||
{
|
||||
ContextStatus::Loading {
|
||||
message: "Summarizing…".into(),
|
||||
}
|
||||
} else {
|
||||
ContextStatus::Ready
|
||||
},
|
||||
ImageStatus::Error => ContextStatus::Error {
|
||||
message: "Failed to load image".into(),
|
||||
render_preview: None,
|
||||
context,
|
||||
}),
|
||||
|
||||
AgentContext::Rules(ref user_rules_context) => {
|
||||
let name = prompt_store
|
||||
.as_ref()?
|
||||
.read(cx)
|
||||
.metadata(user_rules_context.prompt_id.into())?
|
||||
.title?;
|
||||
Some(AddedContext {
|
||||
kind: ContextKind::Rules,
|
||||
name: name.clone(),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: ContextStatus::Ready,
|
||||
render_preview: None,
|
||||
context,
|
||||
})
|
||||
}
|
||||
|
||||
AgentContext::Image(ref image_context) => Some(AddedContext {
|
||||
kind: ContextKind::Image,
|
||||
name: "Image".into(),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: match image_context.status() {
|
||||
ImageStatus::Loading => ContextStatus::Loading {
|
||||
message: "Loading…".into(),
|
||||
},
|
||||
ImageStatus::Error => ContextStatus::Error {
|
||||
message: "Failed to load image".into(),
|
||||
},
|
||||
ImageStatus::Ready => ContextStatus::Ready,
|
||||
},
|
||||
ImageStatus::Ready => ContextStatus::Ready,
|
||||
},
|
||||
render_hover: Some(Rc::new({
|
||||
let image = context.original_image.clone();
|
||||
move |_, cx| {
|
||||
let image = image.clone();
|
||||
ContextPillHover::new(cx, move |_, _| {
|
||||
render_preview: Some(Rc::new({
|
||||
let image = image_context.original_image.clone();
|
||||
move |_, _| {
|
||||
gpui::img(image.clone())
|
||||
.max_w_96()
|
||||
.max_h_96()
|
||||
.into_any_element()
|
||||
})
|
||||
.into()
|
||||
}
|
||||
})),
|
||||
handle: AgentContextHandle::Image(context),
|
||||
}
|
||||
})),
|
||||
context,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct ContextFileExcerpt {
|
||||
pub file_name_and_range: SharedString,
|
||||
pub full_path_and_range: SharedString,
|
||||
pub parent_name: Option<SharedString>,
|
||||
pub icon_path: Option<SharedString>,
|
||||
struct ContextPillPreview {
|
||||
render_preview: Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>,
|
||||
}
|
||||
|
||||
impl ContextFileExcerpt {
|
||||
pub fn new(full_path: &Path, line_range: Range<Point>, cx: &App) -> Self {
|
||||
let full_path_string = full_path.to_string_lossy().into_owned();
|
||||
let file_name = full_path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| full_path_string.clone());
|
||||
|
||||
let line_range_text = format!(" ({}-{})", line_range.start.row + 1, line_range.end.row + 1);
|
||||
let mut full_path_and_range = full_path_string;
|
||||
full_path_and_range.push_str(&line_range_text);
|
||||
let mut file_name_and_range = file_name;
|
||||
file_name_and_range.push_str(&line_range_text);
|
||||
|
||||
let parent_name = full_path
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.map(|n| n.to_string_lossy().into_owned().into());
|
||||
|
||||
let icon_path = FileIcons::get_icon(&full_path, cx);
|
||||
|
||||
ContextFileExcerpt {
|
||||
file_name_and_range: file_name_and_range.into(),
|
||||
full_path_and_range: full_path_and_range.into(),
|
||||
parent_name,
|
||||
icon_path,
|
||||
}
|
||||
}
|
||||
|
||||
fn hover_view(&self, text: SharedString, cx: &mut App) -> Entity<ContextPillHover> {
|
||||
let icon_path = self.icon_path.clone();
|
||||
let full_path_and_range = self.full_path_and_range.clone();
|
||||
ContextPillHover::new(cx, move |_, cx| {
|
||||
v_flex()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.w_full()
|
||||
.max_w_full()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border.opacity(0.6))
|
||||
.children(
|
||||
icon_path
|
||||
.clone()
|
||||
.map(Icon::from_path)
|
||||
.map(|icon| icon.color(Color::Muted).size(IconSize::XSmall)),
|
||||
)
|
||||
.child(
|
||||
// TODO: make this truncate on the left.
|
||||
Label::new(full_path_and_range.clone())
|
||||
.size(LabelSize::Small)
|
||||
.ml_1(),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("context-pill-hover-contents")
|
||||
.overflow_scroll()
|
||||
.max_w_128()
|
||||
.max_h_96()
|
||||
.child(Label::new(text.clone()).buffer_font(cx)),
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct ContextPillHover {
|
||||
render_hover: Box<dyn Fn(&mut Window, &mut App) -> AnyElement>,
|
||||
}
|
||||
|
||||
impl ContextPillHover {
|
||||
fn new(
|
||||
cx: &mut App,
|
||||
render_hover: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
|
||||
) -> Entity<Self> {
|
||||
cx.new(|_| Self {
|
||||
render_hover: Box::new(render_hover),
|
||||
})
|
||||
}
|
||||
|
||||
fn new_text(content: SharedString, cx: &mut App) -> Entity<Self> {
|
||||
Self::new(cx, move |_, _| {
|
||||
div()
|
||||
.id("context-pill-hover-contents")
|
||||
.overflow_scroll()
|
||||
.max_w_128()
|
||||
.max_h_96()
|
||||
.child(content.clone())
|
||||
.into_any_element()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ContextPillHover {
|
||||
impl Render for ContextPillPreview {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
tooltip_container(window, cx, move |this, window, cx| {
|
||||
this.occlude()
|
||||
.on_mouse_move(|_, _, cx| cx.stop_propagation())
|
||||
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
||||
.child((self.render_hover)(window, cx))
|
||||
.child((self.render_preview)(window, cx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Component commented out due to new dependency on `Project`.
|
||||
/*
|
||||
impl Component for AddedContext {
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::Agent
|
||||
@@ -717,41 +505,47 @@ impl Component for AddedContext {
|
||||
"AddedContext"
|
||||
}
|
||||
|
||||
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
|
||||
let mut next_context_id = ContextId::zero();
|
||||
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
|
||||
let next_context_id = ContextId::zero();
|
||||
let image_ready = (
|
||||
"Ready",
|
||||
AddedContext::image(ImageContext {
|
||||
context_id: next_context_id.post_inc(),
|
||||
project_path: None,
|
||||
original_image: Arc::new(Image::empty()),
|
||||
image_task: Task::ready(Some(LanguageModelImage::empty())).shared(),
|
||||
}),
|
||||
AddedContext::new(
|
||||
AgentContext::Image(ImageContext {
|
||||
context_id: next_context_id.post_inc(),
|
||||
original_image: Arc::new(Image::empty()),
|
||||
image_task: Task::ready(Some(LanguageModelImage::empty())).shared(),
|
||||
}),
|
||||
cx,
|
||||
),
|
||||
);
|
||||
|
||||
let image_loading = (
|
||||
"Loading",
|
||||
AddedContext::image(ImageContext {
|
||||
context_id: next_context_id.post_inc(),
|
||||
project_path: None,
|
||||
original_image: Arc::new(Image::empty()),
|
||||
image_task: cx
|
||||
.background_spawn(async move {
|
||||
smol::Timer::after(Duration::from_secs(60 * 5)).await;
|
||||
Some(LanguageModelImage::empty())
|
||||
})
|
||||
.shared(),
|
||||
}),
|
||||
AddedContext::new(
|
||||
AgentContext::Image(ImageContext {
|
||||
context_id: next_context_id.post_inc(),
|
||||
original_image: Arc::new(Image::empty()),
|
||||
image_task: cx
|
||||
.background_spawn(async move {
|
||||
smol::Timer::after(Duration::from_secs(60 * 5)).await;
|
||||
Some(LanguageModelImage::empty())
|
||||
})
|
||||
.shared(),
|
||||
}),
|
||||
cx,
|
||||
),
|
||||
);
|
||||
|
||||
let image_error = (
|
||||
"Error",
|
||||
AddedContext::image(ImageContext {
|
||||
context_id: next_context_id.post_inc(),
|
||||
project_path: None,
|
||||
original_image: Arc::new(Image::empty()),
|
||||
image_task: Task::ready(None).shared(),
|
||||
}),
|
||||
AddedContext::new(
|
||||
AgentContext::Image(ImageContext {
|
||||
context_id: next_context_id.post_inc(),
|
||||
original_image: Arc::new(Image::empty()),
|
||||
image_task: Task::ready(None).shared(),
|
||||
}),
|
||||
cx,
|
||||
),
|
||||
);
|
||||
|
||||
Some(
|
||||
@@ -769,5 +563,8 @@ impl Component for AddedContext {
|
||||
)
|
||||
.into_any(),
|
||||
)
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -33,7 +33,6 @@ use settings::{Settings, update_settings_file};
|
||||
use smol::stream::StreamExt;
|
||||
|
||||
use std::ops::Range;
|
||||
use std::path::Path;
|
||||
use std::{ops::ControlFlow, path::PathBuf, sync::Arc};
|
||||
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
|
||||
use ui::{ContextMenu, PopoverMenu, Tooltip, prelude::*};
|
||||
@@ -1081,7 +1080,7 @@ impl AssistantPanel {
|
||||
|
||||
pub fn open_saved_context(
|
||||
&mut self,
|
||||
path: Arc<Path>,
|
||||
path: PathBuf,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
@@ -1392,7 +1391,7 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
|
||||
fn open_saved_context(
|
||||
&self,
|
||||
workspace: &mut Workspace,
|
||||
path: Arc<Path>,
|
||||
path: PathBuf,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> Task<Result<()>> {
|
||||
|
||||
@@ -37,7 +37,7 @@ use language_model::{
|
||||
ConfiguredModel, LanguageModel, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, LanguageModelTextStream, Role, report_assistant_event,
|
||||
};
|
||||
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
|
||||
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu, ModelType};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use parking_lot::Mutex;
|
||||
use project::{CodeAction, LspAction, ProjectTransaction};
|
||||
@@ -1759,7 +1759,6 @@ impl PromptEditor {
|
||||
language_model_selector: cx.new(|cx| {
|
||||
let fs = fs.clone();
|
||||
LanguageModelSelector::new(
|
||||
|cx| LanguageModelRegistry::read_global(cx).default_model(),
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
@@ -1767,6 +1766,7 @@ impl PromptEditor {
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
},
|
||||
ModelType::Default,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -19,7 +19,7 @@ use language_model::{
|
||||
ConfiguredModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
Role, report_assistant_event,
|
||||
};
|
||||
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
|
||||
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu, ModelType};
|
||||
use prompt_store::PromptBuilder;
|
||||
use settings::{Settings, update_settings_file};
|
||||
use std::{
|
||||
@@ -749,7 +749,6 @@ impl PromptEditor {
|
||||
language_model_selector: cx.new(|cx| {
|
||||
let fs = fs.clone();
|
||||
LanguageModelSelector::new(
|
||||
|cx| LanguageModelRegistry::read_global(cx).default_model(),
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
@@ -757,6 +756,7 @@ impl PromptEditor {
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
},
|
||||
ModelType::Default,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -35,7 +35,7 @@ use std::{
|
||||
fmt::Debug,
|
||||
iter, mem,
|
||||
ops::Range,
|
||||
path::Path,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr as _,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
@@ -46,7 +46,7 @@ use ui::IconName;
|
||||
use util::{ResultExt, TryFutureExt, post_inc};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
#[derive(Clone, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
pub struct ContextId(String);
|
||||
|
||||
impl ContextId {
|
||||
@@ -648,7 +648,7 @@ pub struct AssistantContext {
|
||||
pending_token_count: Task<Option<()>>,
|
||||
pending_save: Task<Result<()>>,
|
||||
pending_cache_warming_task: Task<Option<()>>,
|
||||
path: Option<Arc<Path>>,
|
||||
path: Option<PathBuf>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
@@ -839,7 +839,7 @@ impl AssistantContext {
|
||||
|
||||
pub fn deserialize(
|
||||
saved_context: SavedContext,
|
||||
path: Arc<Path>,
|
||||
path: PathBuf,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
slash_commands: Arc<SlashCommandWorkingSet>,
|
||||
@@ -1147,8 +1147,8 @@ impl AssistantContext {
|
||||
self.prompt_builder.clone()
|
||||
}
|
||||
|
||||
pub fn path(&self) -> Option<&Arc<Path>> {
|
||||
self.path.as_ref()
|
||||
pub fn path(&self) -> Option<&Path> {
|
||||
self.path.as_deref()
|
||||
}
|
||||
|
||||
pub fn summary(&self) -> Option<&ContextSummary> {
|
||||
@@ -3181,7 +3181,7 @@ impl AssistantContext {
|
||||
fs.atomic_write(new_path.clone(), serde_json::to_string(&context).unwrap())
|
||||
.await?;
|
||||
if let Some(old_path) = old_path {
|
||||
if new_path.as_path() != old_path.as_ref() {
|
||||
if new_path != old_path {
|
||||
fs.remove_file(
|
||||
&old_path,
|
||||
RemoveOptions {
|
||||
@@ -3193,7 +3193,7 @@ impl AssistantContext {
|
||||
}
|
||||
}
|
||||
|
||||
this.update(cx, |this, _| this.path = Some(new_path.into()))?;
|
||||
this.update(cx, |this, _| this.path = Some(new_path))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -3589,6 +3589,6 @@ impl SavedContextV0_1_0 {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SavedContextMetadata {
|
||||
pub title: String,
|
||||
pub path: Arc<Path>,
|
||||
pub path: PathBuf,
|
||||
pub mtime: chrono::DateTime<chrono::Local>,
|
||||
}
|
||||
|
||||
@@ -959,7 +959,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
let deserialized_context = cx.new(|cx| {
|
||||
AssistantContext::deserialize(
|
||||
serialized_context,
|
||||
Path::new("").into(),
|
||||
Default::default(),
|
||||
registry.clone(),
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
@@ -1120,7 +1120,7 @@ async fn test_serialization(cx: &mut TestAppContext) {
|
||||
let deserialized_context = cx.new(|cx| {
|
||||
AssistantContext::deserialize(
|
||||
serialized_context,
|
||||
Path::new("").into(),
|
||||
Default::default(),
|
||||
registry.clone(),
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
|
||||
@@ -39,7 +39,7 @@ use language_model::{
|
||||
Role,
|
||||
};
|
||||
use language_model_selector::{
|
||||
LanguageModelSelector, LanguageModelSelectorPopoverMenu, ToggleModelSelector,
|
||||
LanguageModelSelector, LanguageModelSelectorPopoverMenu, ModelType, ToggleModelSelector,
|
||||
};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use picker::Picker;
|
||||
@@ -48,14 +48,7 @@ use project::{Project, Worktree};
|
||||
use rope::Point;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsStore, update_settings_file};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
cmp,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use std::{any::TypeId, cmp, ops::Range, path::PathBuf, sync::Arc, time::Duration};
|
||||
use text::SelectionGoal;
|
||||
use ui::{
|
||||
ButtonLike, Disclosure, ElevationIndex, KeyBinding, PopoverMenuHandle, TintColor, Tooltip,
|
||||
@@ -146,7 +139,7 @@ pub trait AssistantPanelDelegate {
|
||||
fn open_saved_context(
|
||||
&self,
|
||||
workspace: &mut Workspace,
|
||||
path: Arc<Path>,
|
||||
path: PathBuf,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> Task<Result<()>>;
|
||||
@@ -298,7 +291,6 @@ impl ContextEditor {
|
||||
dragged_file_worktrees: Vec::new(),
|
||||
language_model_selector: cx.new(|cx| {
|
||||
LanguageModelSelector::new(
|
||||
|cx| LanguageModelRegistry::read_global(cx).default_model(),
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
@@ -306,6 +298,7 @@ impl ContextEditor {
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
},
|
||||
ModelType::Default,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -20,7 +20,14 @@ use prompt_store::PromptBuilder;
|
||||
use regex::Regex;
|
||||
use rpc::AnyProtoClient;
|
||||
use std::sync::LazyLock;
|
||||
use std::{cmp::Reverse, ffi::OsStr, mem, path::Path, sync::Arc, time::Duration};
|
||||
use std::{
|
||||
cmp::Reverse,
|
||||
ffi::OsStr,
|
||||
mem,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
|
||||
pub(crate) fn init(client: &AnyProtoClient) {
|
||||
@@ -423,7 +430,7 @@ impl ContextStore {
|
||||
|
||||
pub fn open_local_context(
|
||||
&mut self,
|
||||
path: Arc<Path>,
|
||||
path: PathBuf,
|
||||
cx: &Context<Self>,
|
||||
) -> Task<Result<Entity<AssistantContext>>> {
|
||||
if let Some(existing_context) = self.loaded_context_for_path(&path, cx) {
|
||||
@@ -471,7 +478,7 @@ impl ContextStore {
|
||||
|
||||
pub fn delete_local_context(
|
||||
&mut self,
|
||||
path: Arc<Path>,
|
||||
path: PathBuf,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let fs = self.fs.clone();
|
||||
@@ -494,7 +501,7 @@ impl ContextStore {
|
||||
!= Some(&path)
|
||||
});
|
||||
this.contexts_metadata
|
||||
.retain(|context| context.path.as_ref() != path.as_ref());
|
||||
.retain(|context| context.path != path);
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
@@ -504,7 +511,7 @@ impl ContextStore {
|
||||
fn loaded_context_for_path(&self, path: &Path, cx: &App) -> Option<Entity<AssistantContext>> {
|
||||
self.contexts.iter().find_map(|context| {
|
||||
let context = context.upgrade()?;
|
||||
if context.read(cx).path().map(Arc::as_ref) == Some(path) {
|
||||
if context.read(cx).path() == Some(path) {
|
||||
Some(context)
|
||||
} else {
|
||||
None
|
||||
@@ -787,7 +794,7 @@ impl ContextStore {
|
||||
{
|
||||
contexts.push(SavedContextMetadata {
|
||||
title: title.to_string(),
|
||||
path: path.into(),
|
||||
path,
|
||||
mtime: metadata.mtime.timestamp_for_user().into(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -51,13 +51,6 @@ impl ToolUseStatus {
|
||||
ToolUseStatus::Error(out) => out.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn error(&self) -> Option<SharedString> {
|
||||
match self {
|
||||
ToolUseStatus::Error(out) => Some(out.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of running a tool, containing both the asynchronous output
|
||||
|
||||
@@ -34,9 +34,6 @@ regex.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
task.workspace = true
|
||||
terminal.workspace = true
|
||||
terminal_view.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
web_search.workspace = true
|
||||
@@ -54,7 +51,6 @@ project = { workspace = true, features = ["test-support"] }
|
||||
rand.workspace = true
|
||||
pretty_assertions.workspace = true
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
task = { workspace = true, features = ["test-support"]}
|
||||
tree-sitter-rust.workspace = true
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
unindent.workspace = true
|
||||
|
||||
@@ -11,7 +11,6 @@ use gpui::{
|
||||
};
|
||||
use language::{
|
||||
Anchor, Buffer, Capability, LanguageRegistry, LineEnding, OffsetRangeExt, Rope, TextBuffer,
|
||||
language_settings::SoftWrap,
|
||||
};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
@@ -25,8 +24,6 @@ use ui::{Disclosure, Tooltip, Window, prelude::*};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub struct EditFileTool;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct EditFileToolInput {
|
||||
/// A user-friendly markdown description of what's being replaced. This will be shown in the UI.
|
||||
@@ -78,6 +75,8 @@ struct PartialInput {
|
||||
new_string: String,
|
||||
}
|
||||
|
||||
pub struct EditFileTool;
|
||||
|
||||
const DEFAULT_UI_TEXT: &str = "Editing file";
|
||||
|
||||
impl Tool for EditFileTool {
|
||||
@@ -275,9 +274,7 @@ pub struct EditFileToolCard {
|
||||
project: Entity<Project>,
|
||||
diff_task: Option<Task<Result<()>>>,
|
||||
preview_expanded: bool,
|
||||
error_expanded: bool,
|
||||
full_height_expanded: bool,
|
||||
total_lines: Option<u32>,
|
||||
editor_unique_id: EntityId,
|
||||
}
|
||||
|
||||
@@ -296,13 +293,11 @@ impl EditFileToolCard {
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
editor.set_show_scrollbars(false, cx);
|
||||
editor.set_show_gutter(false, cx);
|
||||
editor.disable_inline_diagnostics();
|
||||
editor.disable_scrolling(cx);
|
||||
editor.disable_expand_excerpt_buttons(cx);
|
||||
editor.set_soft_wrap_mode(SoftWrap::None, cx);
|
||||
editor.scroll_manager.set_forbid_vertical_scroll(true);
|
||||
editor.set_show_scrollbars(false, cx);
|
||||
editor.set_read_only(true);
|
||||
editor.set_show_breakpoints(false, cx);
|
||||
editor.set_show_code_actions(false, cx);
|
||||
editor.set_show_git_diff_gutter(false, cx);
|
||||
@@ -317,9 +312,7 @@ impl EditFileToolCard {
|
||||
multibuffer,
|
||||
diff_task: None,
|
||||
preview_expanded: true,
|
||||
error_expanded: false,
|
||||
full_height_expanded: false,
|
||||
total_lines: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -336,7 +329,7 @@ impl EditFileToolCard {
|
||||
let buffer_diff = build_buffer_diff(old_text, &buffer, &language_registry, cx).await?;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.total_lines = this.multibuffer.update(cx, |multibuffer, cx| {
|
||||
this.multibuffer.update(cx, |multibuffer, cx| {
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let diff = buffer_diff.read(cx);
|
||||
let diff_hunk_ranges = diff
|
||||
@@ -352,10 +345,7 @@ impl EditFileToolCard {
|
||||
);
|
||||
debug_assert!(is_newly_added);
|
||||
multibuffer.add_diff(buffer_diff, cx);
|
||||
let end = multibuffer.len(cx);
|
||||
Some(multibuffer.snapshot(cx).offset_to_point(end).row + 1)
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
})
|
||||
}));
|
||||
@@ -370,10 +360,7 @@ impl ToolCard for EditFileToolCard {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let (failed, error_message) = match status {
|
||||
ToolUseStatus::Error(err) => (true, Some(err.to_string())),
|
||||
_ => (false, None),
|
||||
};
|
||||
let failed = matches!(status, ToolUseStatus::Error(_));
|
||||
|
||||
let path_label_button = h_flex()
|
||||
.id(("edit-tool-path-label-button", self.editor_unique_id))
|
||||
@@ -465,26 +452,9 @@ impl ToolCard for EditFileToolCard {
|
||||
.map(|container| {
|
||||
if failed {
|
||||
container.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::new(IconName::Close)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Error),
|
||||
)
|
||||
.child(
|
||||
Disclosure::new(
|
||||
("edit-file-error-disclosure", self.editor_unique_id),
|
||||
self.error_expanded,
|
||||
)
|
||||
.opened_icon(IconName::ChevronUp)
|
||||
.closed_icon(IconName::ChevronDown)
|
||||
.on_click(cx.listener(
|
||||
move |this, _event, _window, _cx| {
|
||||
this.error_expanded = !this.error_expanded;
|
||||
},
|
||||
)),
|
||||
),
|
||||
Icon::new(IconName::Close)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Error),
|
||||
)
|
||||
} else {
|
||||
container.child(
|
||||
@@ -503,14 +473,8 @@ impl ToolCard for EditFileToolCard {
|
||||
}
|
||||
});
|
||||
|
||||
let (editor, editor_line_height) = self.editor.update(cx, |editor, cx| {
|
||||
let line_height = editor
|
||||
.style()
|
||||
.map(|style| style.text.line_height_in_pixels(window.rem_size()))
|
||||
.unwrap_or_default();
|
||||
|
||||
let element = editor.render(window, cx);
|
||||
(element.into_any_element(), line_height)
|
||||
let editor = self.editor.update(cx, |editor, cx| {
|
||||
editor.render(window, cx).into_any_element()
|
||||
});
|
||||
|
||||
let (full_height_icon, full_height_tooltip_label) = if self.full_height_expanded {
|
||||
@@ -534,9 +498,6 @@ impl ToolCard for EditFileToolCard {
|
||||
|
||||
let border_color = cx.theme().colors().border.opacity(0.6);
|
||||
|
||||
const DEFAULT_COLLAPSED_LINES: u32 = 10;
|
||||
let is_collapsible = self.total_lines.unwrap_or(0) > DEFAULT_COLLAPSED_LINES;
|
||||
|
||||
v_flex()
|
||||
.mb_2()
|
||||
.border_1()
|
||||
@@ -545,79 +506,50 @@ impl ToolCard for EditFileToolCard {
|
||||
.rounded_lg()
|
||||
.overflow_hidden()
|
||||
.child(codeblock_header)
|
||||
.when(failed && self.error_expanded, |card| {
|
||||
card.child(
|
||||
v_flex()
|
||||
.p_2()
|
||||
.gap_1()
|
||||
.border_t_1()
|
||||
.border_dashed()
|
||||
.border_color(border_color)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.rounded_b_md()
|
||||
.child(
|
||||
Label::new("Error")
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Error),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.rounded_md()
|
||||
.text_ui_sm(cx)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.children(
|
||||
error_message
|
||||
.map(|error| div().child(error).into_any_element()),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
.when(!failed && self.preview_expanded, |card| {
|
||||
card.child(
|
||||
v_flex()
|
||||
.relative()
|
||||
.map(|editor_container| {
|
||||
if self.full_height_expanded {
|
||||
editor_container.h_full()
|
||||
} else {
|
||||
editor_container
|
||||
.h(DEFAULT_COLLAPSED_LINES as f32 * editor_line_height)
|
||||
}
|
||||
})
|
||||
.overflow_hidden()
|
||||
.border_t_1()
|
||||
.border_color(border_color)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.map(|editor_container| {
|
||||
if self.full_height_expanded {
|
||||
editor_container.h_full()
|
||||
} else {
|
||||
editor_container.max_h_64()
|
||||
}
|
||||
})
|
||||
.child(div().pl_1().child(editor))
|
||||
.when(
|
||||
!self.full_height_expanded && is_collapsible,
|
||||
|editor_container| editor_container.child(gradient_overlay),
|
||||
),
|
||||
.when(!self.full_height_expanded, |editor_container| {
|
||||
editor_container.child(gradient_overlay)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.when(!failed && self.preview_expanded, |card| {
|
||||
card.child(
|
||||
h_flex()
|
||||
.id(("edit-tool-card-inner-hflex", self.editor_unique_id))
|
||||
.flex_none()
|
||||
.cursor_pointer()
|
||||
.h_5()
|
||||
.justify_center()
|
||||
.rounded_b_md()
|
||||
.border_t_1()
|
||||
.border_color(border_color)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.hover(|style| style.bg(cx.theme().colors().element_hover.opacity(0.1)))
|
||||
.child(
|
||||
Icon::new(full_height_icon)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.tooltip(Tooltip::text(full_height_tooltip_label))
|
||||
.on_click(cx.listener(move |this, _event, _window, _cx| {
|
||||
this.full_height_expanded = !this.full_height_expanded;
|
||||
})),
|
||||
)
|
||||
.when(is_collapsible, |editor_container| {
|
||||
editor_container.child(
|
||||
h_flex()
|
||||
.id(("expand-button", self.editor_unique_id))
|
||||
.flex_none()
|
||||
.cursor_pointer()
|
||||
.h_5()
|
||||
.justify_center()
|
||||
.rounded_b_md()
|
||||
.border_t_1()
|
||||
.border_color(border_color)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.hover(|style| style.bg(cx.theme().colors().element_hover.opacity(0.1)))
|
||||
.child(
|
||||
Icon::new(full_height_icon)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.tooltip(Tooltip::text(full_height_tooltip_label))
|
||||
.on_click(cx.listener(move |this, _event, _window, _cx| {
|
||||
this.full_height_expanded = !this.full_height_expanded;
|
||||
})),
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -695,6 +627,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn still_streaming_ui_text_with_path() {
|
||||
let tool = EditFileTool;
|
||||
let input = json!({
|
||||
"path": "src/main.rs",
|
||||
"display_description": "",
|
||||
@@ -702,11 +635,12 @@ mod tests {
|
||||
"new_string": "new code"
|
||||
});
|
||||
|
||||
assert_eq!(EditFileTool.still_streaming_ui_text(&input), "src/main.rs");
|
||||
assert_eq!(tool.still_streaming_ui_text(&input), "src/main.rs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn still_streaming_ui_text_with_description() {
|
||||
let tool = EditFileTool;
|
||||
let input = json!({
|
||||
"path": "",
|
||||
"display_description": "Fix error handling",
|
||||
@@ -714,14 +648,12 @@ mod tests {
|
||||
"new_string": "new code"
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
EditFileTool.still_streaming_ui_text(&input),
|
||||
"Fix error handling",
|
||||
);
|
||||
assert_eq!(tool.still_streaming_ui_text(&input), "Fix error handling");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn still_streaming_ui_text_with_path_and_description() {
|
||||
let tool = EditFileTool;
|
||||
let input = json!({
|
||||
"path": "src/main.rs",
|
||||
"display_description": "Fix error handling",
|
||||
@@ -729,14 +661,12 @@ mod tests {
|
||||
"new_string": "new code"
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
EditFileTool.still_streaming_ui_text(&input),
|
||||
"Fix error handling",
|
||||
);
|
||||
assert_eq!(tool.still_streaming_ui_text(&input), "Fix error handling");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn still_streaming_ui_text_no_path_or_description() {
|
||||
let tool = EditFileTool;
|
||||
let input = json!({
|
||||
"path": "",
|
||||
"display_description": "",
|
||||
@@ -744,19 +674,14 @@ mod tests {
|
||||
"new_string": "new code"
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
EditFileTool.still_streaming_ui_text(&input),
|
||||
DEFAULT_UI_TEXT,
|
||||
);
|
||||
assert_eq!(tool.still_streaming_ui_text(&input), DEFAULT_UI_TEXT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn still_streaming_ui_text_with_null() {
|
||||
let tool = EditFileTool;
|
||||
let input = serde_json::Value::Null;
|
||||
|
||||
assert_eq!(
|
||||
EditFileTool.still_streaming_ui_text(&input),
|
||||
DEFAULT_UI_TEXT,
|
||||
);
|
||||
assert_eq!(tool.still_streaming_ui_text(&input), DEFAULT_UI_TEXT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use futures::StreamExt;
|
||||
use gpui::{AnyWindowHandle, App, Entity, Task};
|
||||
use language::{OffsetRangeExt, ParseStatus, Point};
|
||||
use language::OffsetRangeExt;
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::{
|
||||
Project,
|
||||
@@ -13,7 +13,6 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{cmp, fmt::Write, sync::Arc};
|
||||
use ui::IconName;
|
||||
use util::RangeExt;
|
||||
use util::markdown::MarkdownInlineCode;
|
||||
use util::paths::PathMatcher;
|
||||
|
||||
@@ -103,7 +102,6 @@ impl Tool for GrepTool {
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
const CONTEXT_LINES: u32 = 2;
|
||||
const MAX_ANCESTOR_LINES: u32 = 10;
|
||||
|
||||
let input = match serde_json::from_value::<GrepToolInput>(input) {
|
||||
Ok(input) => input,
|
||||
@@ -142,7 +140,7 @@ impl Tool for GrepTool {
|
||||
|
||||
let results = project.update(cx, |project, cx| project.search(query, cx));
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
cx.spawn(async move|cx| {
|
||||
futures::pin_mut!(results);
|
||||
|
||||
let mut output = String::new();
|
||||
@@ -150,113 +148,68 @@ impl Tool for GrepTool {
|
||||
let mut matches_found = 0;
|
||||
let mut has_more_matches = false;
|
||||
|
||||
'outer: while let Some(SearchResult::Buffer { buffer, ranges }) = results.next().await {
|
||||
while let Some(SearchResult::Buffer { buffer, ranges }) = results.next().await {
|
||||
if ranges.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let (Some(path), mut parse_status) = buffer.read_with(cx, |buffer, cx| {
|
||||
(buffer.file().map(|file| file.full_path(cx)), buffer.parse_status())
|
||||
})? else {
|
||||
continue;
|
||||
};
|
||||
buffer.read_with(cx, |buffer, cx| -> Result<(), anyhow::Error> {
|
||||
if let Some(path) = buffer.file().map(|file| file.full_path(cx)) {
|
||||
let mut file_header_written = false;
|
||||
let mut ranges = ranges
|
||||
.into_iter()
|
||||
.map(|range| {
|
||||
let mut point_range = range.to_point(buffer);
|
||||
point_range.start.row =
|
||||
point_range.start.row.saturating_sub(CONTEXT_LINES);
|
||||
point_range.start.column = 0;
|
||||
point_range.end.row = cmp::min(
|
||||
buffer.max_point().row,
|
||||
point_range.end.row + CONTEXT_LINES,
|
||||
);
|
||||
point_range.end.column = buffer.line_len(point_range.end.row);
|
||||
point_range
|
||||
})
|
||||
.peekable();
|
||||
|
||||
|
||||
while *parse_status.borrow() != ParseStatus::Idle {
|
||||
parse_status.changed().await?;
|
||||
}
|
||||
|
||||
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
||||
|
||||
let mut ranges = ranges
|
||||
.into_iter()
|
||||
.map(|range| {
|
||||
let matched = range.to_point(&snapshot);
|
||||
let matched_end_line_len = snapshot.line_len(matched.end.row);
|
||||
let full_lines = Point::new(matched.start.row, 0)..Point::new(matched.end.row, matched_end_line_len);
|
||||
let symbols = snapshot.symbols_containing(matched.start, None);
|
||||
|
||||
if let Some(ancestor_node) = snapshot.syntax_ancestor(full_lines.clone()) {
|
||||
let full_ancestor_range = ancestor_node.byte_range().to_point(&snapshot);
|
||||
let end_row = full_ancestor_range.end.row.min(full_ancestor_range.start.row + MAX_ANCESTOR_LINES);
|
||||
let end_col = snapshot.line_len(end_row);
|
||||
let capped_ancestor_range = Point::new(full_ancestor_range.start.row, 0)..Point::new(end_row, end_col);
|
||||
|
||||
if capped_ancestor_range.contains_inclusive(&full_lines) {
|
||||
return (capped_ancestor_range, Some(full_ancestor_range), symbols)
|
||||
while let Some(mut range) = ranges.next() {
|
||||
if skips_remaining > 0 {
|
||||
skips_remaining -= 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let mut matched = matched;
|
||||
matched.start.column = 0;
|
||||
matched.start.row =
|
||||
matched.start.row.saturating_sub(CONTEXT_LINES);
|
||||
matched.end.row = cmp::min(
|
||||
snapshot.max_point().row,
|
||||
matched.end.row + CONTEXT_LINES,
|
||||
);
|
||||
matched.end.column = snapshot.line_len(matched.end.row);
|
||||
// We'd already found a full page of matches, and we just found one more.
|
||||
if matches_found >= RESULTS_PER_PAGE {
|
||||
has_more_matches = true;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
(matched, None, symbols)
|
||||
})
|
||||
.peekable();
|
||||
while let Some(next_range) = ranges.peek() {
|
||||
if range.end.row >= next_range.start.row {
|
||||
range.end = next_range.end;
|
||||
ranges.next();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let mut file_header_written = false;
|
||||
if !file_header_written {
|
||||
writeln!(output, "\n## Matches in {}", path.display())?;
|
||||
file_header_written = true;
|
||||
}
|
||||
|
||||
while let Some((mut range, ancestor_range, parent_symbols)) = ranges.next(){
|
||||
if skips_remaining > 0 {
|
||||
skips_remaining -= 1;
|
||||
continue;
|
||||
}
|
||||
let start_line = range.start.row + 1;
|
||||
let end_line = range.end.row + 1;
|
||||
writeln!(output, "\n### Lines {start_line}-{end_line}\n```")?;
|
||||
output.extend(buffer.text_for_range(range));
|
||||
output.push_str("\n```\n");
|
||||
|
||||
// We'd already found a full page of matches, and we just found one more.
|
||||
if matches_found >= RESULTS_PER_PAGE {
|
||||
has_more_matches = true;
|
||||
break 'outer;
|
||||
}
|
||||
|
||||
while let Some((next_range, _, _)) = ranges.peek() {
|
||||
if range.end.row >= next_range.start.row {
|
||||
range.end = next_range.end;
|
||||
ranges.next();
|
||||
} else {
|
||||
break;
|
||||
matches_found += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if !file_header_written {
|
||||
writeln!(output, "\n## Matches in {}", path.display())?;
|
||||
file_header_written = true;
|
||||
}
|
||||
|
||||
let end_row = range.end.row;
|
||||
output.push_str("\n### ");
|
||||
|
||||
if let Some(parent_symbols) = &parent_symbols {
|
||||
for symbol in parent_symbols {
|
||||
write!(output, "{} › ", symbol.text)?;
|
||||
}
|
||||
}
|
||||
|
||||
if range.start.row == end_row {
|
||||
writeln!(output, "L{}", range.start.row + 1)?;
|
||||
} else {
|
||||
writeln!(output, "L{}-{}", range.start.row + 1, end_row + 1)?;
|
||||
}
|
||||
|
||||
output.push_str("```\n");
|
||||
output.extend(snapshot.text_for_range(range));
|
||||
output.push_str("\n```\n");
|
||||
|
||||
if let Some(ancestor_range) = ancestor_range {
|
||||
if end_row < ancestor_range.end.row {
|
||||
let remaining_lines = ancestor_range.end.row - end_row;
|
||||
writeln!(output, "\n{} lines remaining in ancestor node. Read the file to see all.", remaining_lines)?;
|
||||
}
|
||||
}
|
||||
|
||||
matches_found += 1;
|
||||
}
|
||||
Ok(())
|
||||
})??;
|
||||
}
|
||||
|
||||
if matches_found == 0 {
|
||||
@@ -280,16 +233,13 @@ mod tests {
|
||||
use super::*;
|
||||
use assistant_tool::Tool;
|
||||
use gpui::{AppContext, TestAppContext};
|
||||
use language::{Language, LanguageConfig, LanguageMatcher};
|
||||
use project::{FakeFs, Project};
|
||||
use settings::SettingsStore;
|
||||
use unindent::Unindent;
|
||||
use util::path;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_grep_tool_with_include_pattern(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
fs.insert_tree(
|
||||
@@ -377,7 +327,6 @@ mod tests {
|
||||
#[gpui::test]
|
||||
async fn test_grep_tool_with_case_sensitivity(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
fs.insert_tree(
|
||||
@@ -452,290 +401,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Helper function to set up a syntax test environment
|
||||
async fn setup_syntax_test(cx: &mut TestAppContext) -> Entity<Project> {
|
||||
use unindent::Unindent;
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
|
||||
// Create test file with syntax structures
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
serde_json::json!({
|
||||
"test_syntax.rs": r#"
|
||||
fn top_level_function() {
|
||||
println!("This is at the top level");
|
||||
}
|
||||
|
||||
mod feature_module {
|
||||
pub mod nested_module {
|
||||
pub fn nested_function(
|
||||
first_arg: String,
|
||||
second_arg: i32,
|
||||
) {
|
||||
println!("Function in nested module");
|
||||
println!("{first_arg}");
|
||||
println!("{second_arg}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MyStruct {
|
||||
field1: String,
|
||||
field2: i32,
|
||||
}
|
||||
|
||||
impl MyStruct {
|
||||
fn method_with_block() {
|
||||
let condition = true;
|
||||
if condition {
|
||||
println!("Inside if block");
|
||||
}
|
||||
}
|
||||
|
||||
fn long_function() {
|
||||
println!("Line 1");
|
||||
println!("Line 2");
|
||||
println!("Line 3");
|
||||
println!("Line 4");
|
||||
println!("Line 5");
|
||||
println!("Line 6");
|
||||
println!("Line 7");
|
||||
println!("Line 8");
|
||||
println!("Line 9");
|
||||
println!("Line 10");
|
||||
println!("Line 11");
|
||||
println!("Line 12");
|
||||
}
|
||||
}
|
||||
|
||||
trait Processor {
|
||||
fn process(&self, input: &str) -> String;
|
||||
}
|
||||
|
||||
impl Processor for MyStruct {
|
||||
fn process(&self, input: &str) -> String {
|
||||
format!("Processed: {}", input)
|
||||
}
|
||||
}
|
||||
"#.unindent().trim(),
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
|
||||
project.update(cx, |project, _cx| {
|
||||
project.languages().add(rust_lang().into())
|
||||
});
|
||||
|
||||
project
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_grep_top_level_function(cx: &mut TestAppContext) {
|
||||
let project = setup_syntax_test(cx).await;
|
||||
|
||||
// Test: Line at the top level of the file
|
||||
let input = serde_json::to_value(GrepToolInput {
|
||||
regex: "This is at the top level".to_string(),
|
||||
include_pattern: Some("**/*.rs".to_string()),
|
||||
offset: 0,
|
||||
case_sensitive: false,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let result = run_grep_tool(input, project.clone(), cx).await;
|
||||
let expected = r#"
|
||||
Found 1 matches:
|
||||
|
||||
## Matches in root/test_syntax.rs
|
||||
|
||||
### fn top_level_function › L1-3
|
||||
```
|
||||
fn top_level_function() {
|
||||
println!("This is at the top level");
|
||||
}
|
||||
```
|
||||
"#
|
||||
.unindent();
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_grep_function_body(cx: &mut TestAppContext) {
|
||||
let project = setup_syntax_test(cx).await;
|
||||
|
||||
// Test: Line inside a function body
|
||||
let input = serde_json::to_value(GrepToolInput {
|
||||
regex: "Function in nested module".to_string(),
|
||||
include_pattern: Some("**/*.rs".to_string()),
|
||||
offset: 0,
|
||||
case_sensitive: false,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let result = run_grep_tool(input, project.clone(), cx).await;
|
||||
let expected = r#"
|
||||
Found 1 matches:
|
||||
|
||||
## Matches in root/test_syntax.rs
|
||||
|
||||
### mod feature_module › pub mod nested_module › pub fn nested_function › L10-14
|
||||
```
|
||||
) {
|
||||
println!("Function in nested module");
|
||||
println!("{first_arg}");
|
||||
println!("{second_arg}");
|
||||
}
|
||||
```
|
||||
"#
|
||||
.unindent();
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_grep_function_args_and_body(cx: &mut TestAppContext) {
|
||||
let project = setup_syntax_test(cx).await;
|
||||
|
||||
// Test: Line with a function argument
|
||||
let input = serde_json::to_value(GrepToolInput {
|
||||
regex: "second_arg".to_string(),
|
||||
include_pattern: Some("**/*.rs".to_string()),
|
||||
offset: 0,
|
||||
case_sensitive: false,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let result = run_grep_tool(input, project.clone(), cx).await;
|
||||
let expected = r#"
|
||||
Found 1 matches:
|
||||
|
||||
## Matches in root/test_syntax.rs
|
||||
|
||||
### mod feature_module › pub mod nested_module › pub fn nested_function › L7-14
|
||||
```
|
||||
pub fn nested_function(
|
||||
first_arg: String,
|
||||
second_arg: i32,
|
||||
) {
|
||||
println!("Function in nested module");
|
||||
println!("{first_arg}");
|
||||
println!("{second_arg}");
|
||||
}
|
||||
```
|
||||
"#
|
||||
.unindent();
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_grep_if_block(cx: &mut TestAppContext) {
|
||||
use unindent::Unindent;
|
||||
let project = setup_syntax_test(cx).await;
|
||||
|
||||
// Test: Line inside an if block
|
||||
let input = serde_json::to_value(GrepToolInput {
|
||||
regex: "Inside if block".to_string(),
|
||||
include_pattern: Some("**/*.rs".to_string()),
|
||||
offset: 0,
|
||||
case_sensitive: false,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let result = run_grep_tool(input, project.clone(), cx).await;
|
||||
let expected = r#"
|
||||
Found 1 matches:
|
||||
|
||||
## Matches in root/test_syntax.rs
|
||||
|
||||
### impl MyStruct › fn method_with_block › L26-28
|
||||
```
|
||||
if condition {
|
||||
println!("Inside if block");
|
||||
}
|
||||
```
|
||||
"#
|
||||
.unindent();
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_grep_long_function_top(cx: &mut TestAppContext) {
|
||||
use unindent::Unindent;
|
||||
let project = setup_syntax_test(cx).await;
|
||||
|
||||
// Test: Line in the middle of a long function - should show message about remaining lines
|
||||
let input = serde_json::to_value(GrepToolInput {
|
||||
regex: "Line 5".to_string(),
|
||||
include_pattern: Some("**/*.rs".to_string()),
|
||||
offset: 0,
|
||||
case_sensitive: false,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let result = run_grep_tool(input, project.clone(), cx).await;
|
||||
let expected = r#"
|
||||
Found 1 matches:
|
||||
|
||||
## Matches in root/test_syntax.rs
|
||||
|
||||
### impl MyStruct › fn long_function › L31-41
|
||||
```
|
||||
fn long_function() {
|
||||
println!("Line 1");
|
||||
println!("Line 2");
|
||||
println!("Line 3");
|
||||
println!("Line 4");
|
||||
println!("Line 5");
|
||||
println!("Line 6");
|
||||
println!("Line 7");
|
||||
println!("Line 8");
|
||||
println!("Line 9");
|
||||
println!("Line 10");
|
||||
```
|
||||
|
||||
3 lines remaining in ancestor node. Read the file to see all.
|
||||
"#
|
||||
.unindent();
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_grep_long_function_bottom(cx: &mut TestAppContext) {
|
||||
use unindent::Unindent;
|
||||
let project = setup_syntax_test(cx).await;
|
||||
|
||||
// Test: Line in the long function
|
||||
let input = serde_json::to_value(GrepToolInput {
|
||||
regex: "Line 12".to_string(),
|
||||
include_pattern: Some("**/*.rs".to_string()),
|
||||
offset: 0,
|
||||
case_sensitive: false,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let result = run_grep_tool(input, project.clone(), cx).await;
|
||||
let expected = r#"
|
||||
Found 1 matches:
|
||||
|
||||
## Matches in root/test_syntax.rs
|
||||
|
||||
### impl MyStruct › fn long_function › L41-45
|
||||
```
|
||||
println!("Line 10");
|
||||
println!("Line 11");
|
||||
println!("Line 12");
|
||||
}
|
||||
}
|
||||
```
|
||||
"#
|
||||
.unindent();
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
async fn run_grep_tool(
|
||||
input: serde_json::Value,
|
||||
project: Entity<Project>,
|
||||
@@ -746,13 +411,7 @@ mod tests {
|
||||
let task = cx.update(|cx| tool.run(input, &[], project, action_log, None, cx));
|
||||
|
||||
match task.output.await {
|
||||
Ok(result) => {
|
||||
if cfg!(windows) {
|
||||
result.replace("root\\", "root/")
|
||||
} else {
|
||||
result
|
||||
}
|
||||
}
|
||||
Ok(result) => result,
|
||||
Err(e) => panic!("Failed to run grep tool: {}", e),
|
||||
}
|
||||
}
|
||||
@@ -765,20 +424,4 @@ mod tests {
|
||||
Project::init_settings(cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn rust_lang() -> Language {
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::LANGUAGE.into()),
|
||||
)
|
||||
.with_outline_query(include_str!("../../languages/src/rust/outline.scm"))
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,23 @@
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolUseStatus};
|
||||
use gpui::{
|
||||
Animation, AnimationExt, AnyWindowHandle, App, AppContext, Empty, Entity, EntityId, Task,
|
||||
Transformation, WeakEntity, Window, percentage,
|
||||
};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use futures::io::BufReader;
|
||||
use futures::{AsyncBufReadExt, AsyncReadExt, FutureExt};
|
||||
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::{Project, terminals::TerminalKind};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
env,
|
||||
path::{Path, PathBuf},
|
||||
process::ExitStatus,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use terminal_view::TerminalView;
|
||||
use ui::{Disclosure, IconName, Tooltip, prelude::*};
|
||||
use util::{
|
||||
get_system_shell, markdown::MarkdownInlineCode, size::format_file_size,
|
||||
time::duration_alt_display,
|
||||
};
|
||||
use workspace::Workspace;
|
||||
use std::future;
|
||||
use util::get_system_shell;
|
||||
|
||||
const COMMAND_OUTPUT_LIMIT: usize = 16 * 1024;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use ui::IconName;
|
||||
use util::command::new_smol_command;
|
||||
use util::markdown::MarkdownInlineCode;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct TerminalToolInput {
|
||||
/// The one-liner command to execute.
|
||||
command: String,
|
||||
@@ -84,426 +75,308 @@ impl Tool for TerminalTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
window: Option<AnyWindowHandle>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let Some(window) = window else {
|
||||
return Task::ready(Err(anyhow!("no window options"))).into();
|
||||
};
|
||||
|
||||
let input: TerminalToolInput = match serde_json::from_value(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
||||
};
|
||||
|
||||
let project = project.read(cx);
|
||||
let input_path = Path::new(&input.cd);
|
||||
let working_dir = match working_dir(cx, &input, &project, input_path) {
|
||||
Ok(dir) => dir,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
||||
};
|
||||
let terminal = project.update(cx, |project, cx| {
|
||||
project.create_terminal(
|
||||
TerminalKind::Task(task::SpawnInTerminal {
|
||||
command: get_system_shell(),
|
||||
args: vec!["-c".into(), input.command.clone()],
|
||||
cwd: working_dir.clone(),
|
||||
..Default::default()
|
||||
}),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let working_dir = if input.cd == "." {
|
||||
// Accept "." as meaning "the one worktree" if we only have one worktree.
|
||||
let mut worktrees = project.worktrees(cx);
|
||||
|
||||
let card = cx.new(|cx| {
|
||||
TerminalToolCard::new(input.command.clone(), working_dir.clone(), cx.entity_id())
|
||||
});
|
||||
let only_worktree = match worktrees.next() {
|
||||
Some(worktree) => worktree,
|
||||
None => {
|
||||
return Task::ready(Err(anyhow!("No worktrees found in the project"))).into();
|
||||
}
|
||||
};
|
||||
|
||||
let output = cx.spawn({
|
||||
let card = card.clone();
|
||||
async move |cx| {
|
||||
let terminal = terminal.await?;
|
||||
let workspace = window
|
||||
.downcast::<Workspace>()
|
||||
.and_then(|handle| handle.entity(cx).ok())
|
||||
.context("no workspace entity in root of window")?;
|
||||
|
||||
let terminal_view = window.update(cx, |_, window, cx| {
|
||||
cx.new(|cx| {
|
||||
TerminalView::new(
|
||||
terminal.clone(),
|
||||
workspace.downgrade(),
|
||||
None,
|
||||
project.downgrade(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})?;
|
||||
let _ = card.update(cx, |card, _| {
|
||||
card.terminal = Some(terminal_view.clone());
|
||||
card.start_instant = Instant::now();
|
||||
});
|
||||
|
||||
let exit_status = terminal
|
||||
.update(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
|
||||
.await;
|
||||
let (content, content_line_count) = terminal.update(cx, |terminal, _| {
|
||||
(terminal.get_content(), terminal.total_lines())
|
||||
})?;
|
||||
|
||||
let previous_len = content.len();
|
||||
let (processed_content, finished_with_empty_output) =
|
||||
process_content(content, &input.command, exit_status);
|
||||
|
||||
let _ = card.update(cx, |card, _| {
|
||||
card.command_finished = true;
|
||||
card.exit_status = exit_status;
|
||||
card.was_content_truncated = processed_content.len() < previous_len;
|
||||
card.original_content_len = previous_len;
|
||||
card.content_line_count = content_line_count;
|
||||
card.finished_with_empty_output = finished_with_empty_output;
|
||||
card.elapsed_time = Some(card.start_instant.elapsed());
|
||||
});
|
||||
|
||||
Ok(processed_content)
|
||||
if worktrees.next().is_some() {
|
||||
return Task::ready(Err(anyhow!(
|
||||
"'.' is ambiguous in multi-root workspaces. Please specify a root directory explicitly."
|
||||
))).into();
|
||||
}
|
||||
});
|
||||
|
||||
ToolResult {
|
||||
output,
|
||||
card: Some(card.into()),
|
||||
}
|
||||
only_worktree.read(cx).abs_path()
|
||||
} else if input_path.is_absolute() {
|
||||
// Absolute paths are allowed, but only if they're in one of the project's worktrees.
|
||||
if !project
|
||||
.worktrees(cx)
|
||||
.any(|worktree| input_path.starts_with(&worktree.read(cx).abs_path()))
|
||||
{
|
||||
return Task::ready(Err(anyhow!(
|
||||
"The absolute path must be within one of the project's worktrees"
|
||||
)))
|
||||
.into();
|
||||
}
|
||||
|
||||
input_path.into()
|
||||
} else {
|
||||
let Some(worktree) = project.worktree_for_root_name(&input.cd, cx) else {
|
||||
return Task::ready(Err(anyhow!(
|
||||
"`cd` directory {} not found in the project",
|
||||
&input.cd
|
||||
)))
|
||||
.into();
|
||||
};
|
||||
|
||||
worktree.read(cx).abs_path()
|
||||
};
|
||||
|
||||
cx.background_spawn(run_command_limited(working_dir, input.command))
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
fn process_content(
|
||||
content: String,
|
||||
command: &str,
|
||||
exit_status: Option<ExitStatus>,
|
||||
) -> (String, bool) {
|
||||
let should_truncate = content.len() > COMMAND_OUTPUT_LIMIT;
|
||||
const LIMIT: usize = 16 * 1024;
|
||||
|
||||
let content = if should_truncate {
|
||||
let mut end_ix = COMMAND_OUTPUT_LIMIT.min(content.len());
|
||||
while !content.is_char_boundary(end_ix) {
|
||||
end_ix -= 1;
|
||||
}
|
||||
// Don't truncate mid-line, clear the remainder of the last line
|
||||
end_ix = content[..end_ix].rfind('\n').unwrap_or(end_ix);
|
||||
&content[..end_ix]
|
||||
} else {
|
||||
content.as_str()
|
||||
};
|
||||
let is_empty = content.trim().is_empty();
|
||||
async fn run_command_limited(working_dir: Arc<Path>, command: String) -> Result<String> {
|
||||
let shell = get_system_shell();
|
||||
|
||||
let content = format!(
|
||||
"```\n{}{}```",
|
||||
content,
|
||||
if content.ends_with('\n') { "" } else { "\n" }
|
||||
let mut cmd = new_smol_command(&shell)
|
||||
.arg("-c")
|
||||
.arg(&command)
|
||||
.current_dir(working_dir)
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.spawn()
|
||||
.context("Failed to execute terminal command")?;
|
||||
|
||||
let mut combined_buffer = String::with_capacity(LIMIT + 1);
|
||||
|
||||
let mut out_reader = BufReader::new(cmd.stdout.take().context("Failed to get stdout")?);
|
||||
let mut out_tmp_buffer = String::with_capacity(512);
|
||||
let mut err_reader = BufReader::new(cmd.stderr.take().context("Failed to get stderr")?);
|
||||
let mut err_tmp_buffer = String::with_capacity(512);
|
||||
|
||||
let mut out_line = Box::pin(
|
||||
out_reader
|
||||
.read_line(&mut out_tmp_buffer)
|
||||
.left_future()
|
||||
.fuse(),
|
||||
);
|
||||
let mut err_line = Box::pin(
|
||||
err_reader
|
||||
.read_line(&mut err_tmp_buffer)
|
||||
.left_future()
|
||||
.fuse(),
|
||||
);
|
||||
|
||||
let content = if should_truncate {
|
||||
format!(
|
||||
"Command output too long. The first {} bytes:\n\n{}",
|
||||
content.len(),
|
||||
content,
|
||||
)
|
||||
} else {
|
||||
content
|
||||
};
|
||||
|
||||
let content = match exit_status {
|
||||
Some(exit_status) if exit_status.success() => {
|
||||
if is_empty {
|
||||
"Command executed successfully.".to_string()
|
||||
} else {
|
||||
content.to_string()
|
||||
}
|
||||
}
|
||||
Some(exit_status) => {
|
||||
let code = exit_status.code().unwrap_or(-1);
|
||||
if is_empty {
|
||||
format!("Command \"{command}\" failed with exit code {code}.")
|
||||
} else {
|
||||
format!("Command \"{command}\" failed with exit code {code}.\n\n{content}")
|
||||
}
|
||||
}
|
||||
None => {
|
||||
format!(
|
||||
"Command failed or was interrupted.\nPartial output captured:\n\n{}",
|
||||
content,
|
||||
)
|
||||
}
|
||||
};
|
||||
(content, is_empty)
|
||||
}
|
||||
|
||||
fn working_dir(
|
||||
cx: &mut App,
|
||||
input: &TerminalToolInput,
|
||||
project: &Entity<Project>,
|
||||
input_path: &Path,
|
||||
) -> Result<Option<PathBuf>, &'static str> {
|
||||
let project = project.read(cx);
|
||||
|
||||
if input.cd == "." {
|
||||
// Accept "." as meaning "the one worktree" if we only have one worktree.
|
||||
let mut worktrees = project.worktrees(cx);
|
||||
|
||||
match worktrees.next() {
|
||||
Some(worktree) => {
|
||||
if worktrees.next().is_some() {
|
||||
return Err(
|
||||
"'.' is ambiguous in multi-root workspaces. Please specify a root directory explicitly.",
|
||||
);
|
||||
}
|
||||
Ok(Some(worktree.read(cx).abs_path().to_path_buf()))
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
} else if input_path.is_absolute() {
|
||||
// Absolute paths are allowed, but only if they're in one of the project's worktrees.
|
||||
if !project
|
||||
.worktrees(cx)
|
||||
.any(|worktree| input_path.starts_with(&worktree.read(cx).abs_path()))
|
||||
{
|
||||
return Err("The absolute path must be within one of the project's worktrees");
|
||||
}
|
||||
|
||||
Ok(Some(input_path.into()))
|
||||
} else {
|
||||
let Some(worktree) = project.worktree_for_root_name(&input.cd, cx) else {
|
||||
return Err("`cd` directory {} not found in the project");
|
||||
};
|
||||
|
||||
Ok(Some(worktree.read(cx).abs_path().to_path_buf()))
|
||||
}
|
||||
}
|
||||
|
||||
struct TerminalToolCard {
|
||||
input_command: String,
|
||||
working_dir: Option<PathBuf>,
|
||||
entity_id: EntityId,
|
||||
exit_status: Option<ExitStatus>,
|
||||
terminal: Option<Entity<TerminalView>>,
|
||||
command_finished: bool,
|
||||
was_content_truncated: bool,
|
||||
finished_with_empty_output: bool,
|
||||
content_line_count: usize,
|
||||
original_content_len: usize,
|
||||
preview_expanded: bool,
|
||||
start_instant: Instant,
|
||||
elapsed_time: Option<Duration>,
|
||||
}
|
||||
|
||||
impl TerminalToolCard {
|
||||
pub fn new(input_command: String, working_dir: Option<PathBuf>, entity_id: EntityId) -> Self {
|
||||
Self {
|
||||
input_command,
|
||||
working_dir,
|
||||
entity_id,
|
||||
exit_status: None,
|
||||
terminal: None,
|
||||
command_finished: false,
|
||||
was_content_truncated: false,
|
||||
finished_with_empty_output: false,
|
||||
original_content_len: 0,
|
||||
content_line_count: 0,
|
||||
preview_expanded: true,
|
||||
start_instant: Instant::now(),
|
||||
elapsed_time: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolCard for TerminalToolCard {
|
||||
fn render(
|
||||
&mut self,
|
||||
status: &ToolUseStatus,
|
||||
_window: &mut Window,
|
||||
_workspace: WeakEntity<Workspace>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let Some(terminal) = self.terminal.as_ref() else {
|
||||
return Empty.into_any();
|
||||
};
|
||||
|
||||
let tool_failed = matches!(status, ToolUseStatus::Error(_));
|
||||
let command_failed =
|
||||
self.command_finished && self.exit_status.is_none_or(|code| !code.success());
|
||||
if (tool_failed || command_failed) && self.elapsed_time.is_none() {
|
||||
self.elapsed_time = Some(self.start_instant.elapsed());
|
||||
}
|
||||
let time_elapsed = self
|
||||
.elapsed_time
|
||||
.unwrap_or_else(|| self.start_instant.elapsed());
|
||||
let should_hide_terminal =
|
||||
tool_failed || self.finished_with_empty_output || !self.preview_expanded;
|
||||
|
||||
let border_color = cx.theme().colors().border.opacity(0.6);
|
||||
let header_bg = cx
|
||||
.theme()
|
||||
.colors()
|
||||
.element_background
|
||||
.blend(cx.theme().colors().editor_foreground.opacity(0.025));
|
||||
|
||||
let header_label = h_flex()
|
||||
.w_full()
|
||||
.max_w_full()
|
||||
.px_1()
|
||||
.gap_0p5()
|
||||
.opacity(0.8)
|
||||
.child(
|
||||
h_flex()
|
||||
.child(
|
||||
Icon::new(IconName::Terminal)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id(("terminal-tool-header-input-command", self.entity_id))
|
||||
.text_size(rems(0.8125))
|
||||
.font_buffer(cx)
|
||||
.child(self.input_command.clone())
|
||||
.ml_1p5()
|
||||
.mr_0p5()
|
||||
.tooltip({
|
||||
let path = self
|
||||
.working_dir
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.or_else(|| env::current_dir().ok())
|
||||
.map(|path| format!("\"{}\"", path.display()))
|
||||
.unwrap_or_else(|| "current directory".to_string());
|
||||
Tooltip::text(if self.command_finished {
|
||||
format!("Ran in {path}")
|
||||
} else {
|
||||
format!("Running in {path}")
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
.into_any_element();
|
||||
|
||||
let header = h_flex()
|
||||
.flex_none()
|
||||
.p_1()
|
||||
.gap_1()
|
||||
.justify_between()
|
||||
.rounded_t_md()
|
||||
.bg(header_bg)
|
||||
.child(header_label)
|
||||
.map(|header| {
|
||||
let header = header
|
||||
.when(self.was_content_truncated, |header| {
|
||||
let tooltip =
|
||||
if self.content_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES {
|
||||
"Output exceeded terminal max lines and was \
|
||||
truncated, the model received the first 16 KB."
|
||||
.to_string()
|
||||
} else {
|
||||
format!(
|
||||
"Output is {} long, to avoid unexpected token usage, \
|
||||
only 16 KB was sent back to the model.",
|
||||
format_file_size(self.original_content_len as u64, true),
|
||||
)
|
||||
};
|
||||
header.child(
|
||||
div()
|
||||
.id(("terminal-tool-truncated-label", self.entity_id))
|
||||
.tooltip(Tooltip::text(tooltip))
|
||||
.child(
|
||||
Label::new("(truncated)")
|
||||
.color(Color::Disabled)
|
||||
.size(LabelSize::Small),
|
||||
),
|
||||
)
|
||||
})
|
||||
.when(time_elapsed > Duration::from_secs(10), |header| {
|
||||
header.child(
|
||||
Label::new(format!("({})", duration_alt_display(time_elapsed)))
|
||||
.buffer_font(cx)
|
||||
.color(Color::Disabled)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
});
|
||||
|
||||
if tool_failed || command_failed {
|
||||
header.child(
|
||||
div()
|
||||
.id(("terminal-tool-error-code-indicator", self.entity_id))
|
||||
.child(
|
||||
Icon::new(IconName::Close)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Error),
|
||||
)
|
||||
.when(command_failed && self.exit_status.is_some(), |this| {
|
||||
this.tooltip(Tooltip::text(format!(
|
||||
"Exited with code {}",
|
||||
self.exit_status
|
||||
.and_then(|status| status.code())
|
||||
.unwrap_or(-1),
|
||||
)))
|
||||
})
|
||||
.when(
|
||||
!command_failed && tool_failed && status.error().is_some(),
|
||||
|this| {
|
||||
this.tooltip(Tooltip::text(format!(
|
||||
"Error: {}",
|
||||
status.error().unwrap(),
|
||||
)))
|
||||
},
|
||||
),
|
||||
)
|
||||
} else if self.command_finished {
|
||||
header.child(
|
||||
Icon::new(IconName::Check)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Success),
|
||||
)
|
||||
let mut has_stdout = true;
|
||||
let mut has_stderr = true;
|
||||
while (has_stdout || has_stderr) && combined_buffer.len() < LIMIT + 1 {
|
||||
futures::select_biased! {
|
||||
read = out_line => {
|
||||
drop(out_line);
|
||||
combined_buffer.extend(out_tmp_buffer.drain(..));
|
||||
if read? == 0 {
|
||||
out_line = Box::pin(future::pending().right_future().fuse());
|
||||
has_stdout = false;
|
||||
} else {
|
||||
header.child(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Info)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| {
|
||||
icon.transform(Transformation::rotate(percentage(delta)))
|
||||
},
|
||||
),
|
||||
)
|
||||
out_line = Box::pin(out_reader.read_line(&mut out_tmp_buffer).left_future().fuse());
|
||||
}
|
||||
})
|
||||
.when(!tool_failed && !self.finished_with_empty_output, |header| {
|
||||
header.child(
|
||||
Disclosure::new(
|
||||
("terminal-tool-disclosure", self.entity_id),
|
||||
self.preview_expanded,
|
||||
)
|
||||
.opened_icon(IconName::ChevronUp)
|
||||
.closed_icon(IconName::ChevronDown)
|
||||
.on_click(cx.listener(
|
||||
move |this, _event, _window, _cx| {
|
||||
this.preview_expanded = !this.preview_expanded;
|
||||
},
|
||||
)),
|
||||
)
|
||||
});
|
||||
}
|
||||
read = err_line => {
|
||||
drop(err_line);
|
||||
combined_buffer.extend(err_tmp_buffer.drain(..));
|
||||
if read? == 0 {
|
||||
err_line = Box::pin(future::pending().right_future().fuse());
|
||||
has_stderr = false;
|
||||
} else {
|
||||
err_line = Box::pin(err_reader.read_line(&mut err_tmp_buffer).left_future().fuse());
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
v_flex()
|
||||
.mb_2()
|
||||
.border_1()
|
||||
.when(tool_failed || command_failed, |card| card.border_dashed())
|
||||
.border_color(border_color)
|
||||
.rounded_lg()
|
||||
.overflow_hidden()
|
||||
.child(header)
|
||||
.when(!should_hide_terminal, |this| {
|
||||
this.child(div().child(terminal.clone()).min_h(px(250.0)))
|
||||
})
|
||||
.into_any()
|
||||
drop((out_line, err_line));
|
||||
|
||||
let truncated = combined_buffer.len() > LIMIT;
|
||||
combined_buffer.truncate(LIMIT);
|
||||
|
||||
consume_reader(out_reader, truncated).await?;
|
||||
consume_reader(err_reader, truncated).await?;
|
||||
|
||||
// Handle potential errors during status retrieval, including interruption.
|
||||
match cmd.status().await {
|
||||
Ok(status) => {
|
||||
let output_string = if truncated {
|
||||
// Valid to find `\n` in UTF-8 since 0-127 ASCII characters are not used in
|
||||
// multi-byte characters.
|
||||
let last_line_ix = combined_buffer.bytes().rposition(|b| b == b'\n');
|
||||
let buffer_content =
|
||||
&combined_buffer[..last_line_ix.unwrap_or(combined_buffer.len())];
|
||||
|
||||
format!(
|
||||
"Command output too long. The first {} bytes:\n\n{}",
|
||||
buffer_content.len(),
|
||||
output_block(buffer_content),
|
||||
)
|
||||
} else {
|
||||
output_block(&combined_buffer)
|
||||
};
|
||||
|
||||
let output_with_status = if status.success() {
|
||||
if output_string.is_empty() {
|
||||
"Command executed successfully.".to_string()
|
||||
} else {
|
||||
output_string
|
||||
}
|
||||
} else {
|
||||
format!(
|
||||
"Command failed with exit code {} (shell: {}).\n\n{}",
|
||||
status.code().unwrap_or(-1),
|
||||
shell,
|
||||
output_string,
|
||||
)
|
||||
};
|
||||
|
||||
Ok(output_with_status)
|
||||
}
|
||||
Err(err) => {
|
||||
// Error occurred getting status (potential interruption). Include partial output.
|
||||
let partial_output = output_block(&combined_buffer);
|
||||
let error_message = format!(
|
||||
"Command failed or was interrupted.\nPartial output captured:\n\n{}",
|
||||
partial_output
|
||||
);
|
||||
Err(anyhow!(err).context(error_message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn consume_reader<T: AsyncReadExt + Unpin>(
|
||||
mut reader: BufReader<T>,
|
||||
truncated: bool,
|
||||
) -> Result<(), std::io::Error> {
|
||||
loop {
|
||||
let skipped_bytes = reader.fill_buf().await?;
|
||||
if skipped_bytes.is_empty() {
|
||||
break;
|
||||
}
|
||||
let skipped_bytes_len = skipped_bytes.len();
|
||||
reader.consume_unpin(skipped_bytes_len);
|
||||
|
||||
// Should only skip if we went over the limit
|
||||
debug_assert!(truncated);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn output_block(output: &str) -> String {
|
||||
format!(
|
||||
"```\n{}{}```",
|
||||
output,
|
||||
if output.ends_with('\n') { "" } else { "\n" }
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(not(windows))]
|
||||
mod tests {
|
||||
use gpui::TestAppContext;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_run_command_simple(cx: &mut TestAppContext) {
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let result =
|
||||
run_command_limited(Path::new(".").into(), "echo 'Hello, World!'".to_string()).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), "```\nHello, World!\n```");
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_interleaved_stdout_stderr(cx: &mut TestAppContext) {
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let command = "echo 'stdout 1' && sleep 0.01 && echo 'stderr 1' >&2 && sleep 0.01 && echo 'stdout 2' && sleep 0.01 && echo 'stderr 2' >&2";
|
||||
let result = run_command_limited(Path::new(".").into(), command.to_string()).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(
|
||||
result.unwrap(),
|
||||
"```\nstdout 1\nstderr 1\nstdout 2\nstderr 2\n```"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_multiple_output_reads(cx: &mut TestAppContext) {
|
||||
cx.executor().allow_parking();
|
||||
|
||||
// Command with multiple outputs that might require multiple reads
|
||||
let result = run_command_limited(
|
||||
Path::new(".").into(),
|
||||
"echo '1'; sleep 0.01; echo '2'; sleep 0.01; echo '3'".to_string(),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), "```\n1\n2\n3\n```");
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_output_truncation_single_line(cx: &mut TestAppContext) {
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let cmd = format!("echo '{}'; sleep 0.01;", "X".repeat(LIMIT * 2));
|
||||
|
||||
let result = run_command_limited(Path::new(".").into(), cmd).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
let output = result.unwrap();
|
||||
|
||||
let content_start = output.find("```\n").map(|i| i + 4).unwrap_or(0);
|
||||
let content_end = output.rfind("\n```").unwrap_or(output.len());
|
||||
let content_length = content_end - content_start;
|
||||
|
||||
// Output should be exactly the limit
|
||||
assert_eq!(content_length, LIMIT);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_output_truncation_multiline(cx: &mut TestAppContext) {
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let cmd = format!("echo '{}'; ", "X".repeat(120)).repeat(160);
|
||||
let result = run_command_limited(Path::new(".").into(), cmd).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
let output = result.unwrap();
|
||||
|
||||
assert!(output.starts_with("Command output too long. The first 16334 bytes:\n\n"));
|
||||
|
||||
let content_start = output.find("```\n").map(|i| i + 4).unwrap_or(0);
|
||||
let content_end = output.rfind("\n```").unwrap_or(output.len());
|
||||
let content_length = content_end - content_start;
|
||||
|
||||
assert!(content_length <= LIMIT);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_command_failure(cx: &mut TestAppContext) {
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let result = run_command_limited(Path::new(".").into(), "exit 42".to_string()).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
let output = result.unwrap();
|
||||
|
||||
// Extract the shell name from path for cleaner test output
|
||||
let shell_path = std::env::var("SHELL").unwrap_or("bash".to_string());
|
||||
|
||||
let expected_output = format!(
|
||||
"Command failed with exit code 42 (shell: {}).\n\n```\n\n```",
|
||||
shell_path
|
||||
);
|
||||
assert_eq!(output, expected_output);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
Executes a shell one-liner and returns the combined output.
|
||||
|
||||
This tool spawns a process using the user's shell, reads from stdout and stderr (preserving the order of writes), and returns a string with the combined output result.
|
||||
|
||||
The output results will be shown to the user already, only list it again if necessary, avoid being redundant.
|
||||
This tool spawns a process using the user's current shell, combines stdout and stderr into one interleaved stream as they are produced (preserving the order of writes), and captures that stream into a string which is returned.
|
||||
|
||||
Make sure you use the `cd` parameter to navigate to one of the root directories of the project. NEVER do it as part of the `command` itself, otherwise it will error.
|
||||
|
||||
Do not use this tool for commands that run indefinitely, such as servers (like `npm run start`, `npm run dev`, `python -m http.server`, etc) or file watchers that don't terminate on their own.
|
||||
Do not use this tool for commands that run indefinitely, such as servers (e.g., `python -m http.server`) or file watchers that don't terminate on their own.
|
||||
|
||||
Remember that each invocation of this tool will spawn a new shell process, so you can't rely on any state from previous invocations.
|
||||
|
||||
@@ -23,6 +23,7 @@ pub struct WebSearchToolInput {
|
||||
query: String,
|
||||
}
|
||||
|
||||
#[derive(RegisterComponent)]
|
||||
pub struct WebSearchTool;
|
||||
|
||||
impl Tool for WebSearchTool {
|
||||
@@ -83,7 +84,6 @@ impl Tool for WebSearchTool {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(RegisterComponent)]
|
||||
struct WebSearchToolCard {
|
||||
response: Option<Result<WebSearchResponse>>,
|
||||
_task: Task<()>,
|
||||
@@ -185,11 +185,15 @@ impl ToolCard for WebSearchToolCard {
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for WebSearchToolCard {
|
||||
impl Component for WebSearchTool {
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::Agent
|
||||
}
|
||||
|
||||
fn sort_name() -> &'static str {
|
||||
"ToolWebSearch"
|
||||
}
|
||||
|
||||
fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
|
||||
let in_progress_search = cx.new(|cx| WebSearchToolCard {
|
||||
response: None,
|
||||
|
||||
@@ -11,14 +11,12 @@ pub use aws_sdk_bedrockruntime::types::{
|
||||
Tool as BedrockTool, ToolChoice as BedrockToolChoice, ToolConfiguration as BedrockToolConfig,
|
||||
ToolInputSchema as BedrockToolInputSchema, ToolSpecification as BedrockToolSpec,
|
||||
};
|
||||
pub use aws_smithy_types::Blob as BedrockBlob;
|
||||
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,
|
||||
ImageBlock as BedrockImageBlock, Message as BedrockMessage,
|
||||
ReasoningContentBlock as BedrockThinkingBlock, ReasoningTextBlock as BedrockThinkingTextBlock,
|
||||
ResponseStream as BedrockResponseStream, ToolResultBlock as BedrockToolResultBlock,
|
||||
ToolResultContentBlock as BedrockToolResultContentBlock,
|
||||
ToolResultStatus as BedrockToolResultStatus, ToolUseBlock as BedrockToolUseBlock,
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
alter table subscription_usage_meters
|
||||
add column mode text not null default 'normal';
|
||||
|
||||
drop index uix_subscription_usage_meters_on_subscription_usage_model;
|
||||
|
||||
create unique index uix_subscription_usage_meters_on_subscription_usage_model_mode on subscription_usage_meters (subscription_usage_id, model_id, mode);
|
||||
@@ -26,7 +26,6 @@ use crate::api::events::SnowflakeRow;
|
||||
use crate::db::billing_subscription::{
|
||||
StripeCancellationReason, StripeSubscriptionStatus, SubscriptionKind,
|
||||
};
|
||||
use crate::llm::db::subscription_usage_meter::CompletionMode;
|
||||
use crate::llm::{DEFAULT_MAX_MONTHLY_SPEND, FREE_TIER_MONTHLY_SPENDING_LIMIT};
|
||||
use crate::rpc::{ResultExt as _, Server};
|
||||
use crate::{AppState, Cents, Error, Result};
|
||||
@@ -323,35 +322,16 @@ async fn create_billing_subscription(
|
||||
CustomerId::from_str(&existing_customer.stripe_customer_id)
|
||||
.context("failed to parse customer ID")?
|
||||
} else {
|
||||
let existing_customer = if let Some(email) = user.email_address.as_deref() {
|
||||
let customers = Customer::list(
|
||||
&stripe_client,
|
||||
&stripe::ListCustomers {
|
||||
email: Some(email),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let customer = Customer::create(
|
||||
&stripe_client,
|
||||
CreateCustomer {
|
||||
email: user.email_address.as_deref(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
customers.data.first().cloned()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(existing_customer) = existing_customer {
|
||||
existing_customer.id
|
||||
} else {
|
||||
let customer = Customer::create(
|
||||
&stripe_client,
|
||||
CreateCustomer {
|
||||
email: user.email_address.as_deref(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
customer.id
|
||||
}
|
||||
customer.id
|
||||
};
|
||||
|
||||
let success_url = format!(
|
||||
@@ -380,14 +360,11 @@ async fn create_billing_subscription(
|
||||
}
|
||||
}
|
||||
|
||||
let feature_flags = app.db.get_user_flags(user.id).await?;
|
||||
|
||||
stripe_billing
|
||||
.checkout_with_zed_pro_trial(
|
||||
app.config.zed_pro_price_id()?,
|
||||
customer_id,
|
||||
&user.github_login,
|
||||
feature_flags,
|
||||
&success_url,
|
||||
)
|
||||
.await?
|
||||
@@ -397,9 +374,7 @@ async fn create_billing_subscription(
|
||||
zed_llm_client::LanguageModelProvider::Anthropic,
|
||||
"claude-3-7-sonnet",
|
||||
)?;
|
||||
let stripe_model = stripe_billing
|
||||
.register_model_for_token_based_usage(default_model)
|
||||
.await?;
|
||||
let stripe_model = stripe_billing.register_model(default_model).await?;
|
||||
stripe_billing
|
||||
.checkout(customer_id, &user.github_login, &stripe_model, &success_url)
|
||||
.await?
|
||||
@@ -1249,9 +1224,9 @@ async fn find_or_create_billing_customer(
|
||||
Ok(Some(billing_customer))
|
||||
}
|
||||
|
||||
const SYNC_LLM_TOKEN_USAGE_WITH_STRIPE_INTERVAL: Duration = Duration::from_secs(60);
|
||||
const SYNC_LLM_USAGE_WITH_STRIPE_INTERVAL: Duration = Duration::from_secs(60);
|
||||
|
||||
pub fn sync_llm_token_usage_with_stripe_periodically(app: Arc<AppState>) {
|
||||
pub fn sync_llm_usage_with_stripe_periodically(app: Arc<AppState>) {
|
||||
let Some(stripe_billing) = app.stripe_billing.clone() else {
|
||||
log::warn!("failed to retrieve Stripe billing object");
|
||||
return;
|
||||
@@ -1266,19 +1241,17 @@ pub fn sync_llm_token_usage_with_stripe_periodically(app: Arc<AppState>) {
|
||||
let executor = executor.clone();
|
||||
async move {
|
||||
loop {
|
||||
sync_token_usage_with_stripe(&app, &llm_db, &stripe_billing)
|
||||
sync_with_stripe(&app, &llm_db, &stripe_billing)
|
||||
.await
|
||||
.context("failed to sync LLM usage to Stripe")
|
||||
.trace_err();
|
||||
executor
|
||||
.sleep(SYNC_LLM_TOKEN_USAGE_WITH_STRIPE_INTERVAL)
|
||||
.await;
|
||||
executor.sleep(SYNC_LLM_USAGE_WITH_STRIPE_INTERVAL).await;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn sync_token_usage_with_stripe(
|
||||
async fn sync_with_stripe(
|
||||
app: &Arc<AppState>,
|
||||
llm_db: &Arc<LlmDatabase>,
|
||||
stripe_billing: &Arc<StripeBilling>,
|
||||
@@ -1309,128 +1282,15 @@ async fn sync_token_usage_with_stripe(
|
||||
.parse()
|
||||
.context("failed to parse stripe customer id from db")?;
|
||||
|
||||
let stripe_model = stripe_billing
|
||||
.register_model_for_token_based_usage(&model)
|
||||
.await?;
|
||||
let stripe_model = stripe_billing.register_model(&model).await?;
|
||||
stripe_billing
|
||||
.subscribe_to_model(&stripe_subscription_id, &stripe_model)
|
||||
.await?;
|
||||
stripe_billing
|
||||
.bill_model_token_usage(&stripe_customer_id, &stripe_model, &event)
|
||||
.bill_model_usage(&stripe_customer_id, &stripe_model, &event)
|
||||
.await?;
|
||||
llm_db.consume_billing_event(event.id).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const SYNC_LLM_REQUEST_USAGE_WITH_STRIPE_INTERVAL: Duration = Duration::from_secs(60);
|
||||
|
||||
pub fn sync_llm_request_usage_with_stripe_periodically(app: Arc<AppState>) {
|
||||
let Some(stripe_billing) = app.stripe_billing.clone() else {
|
||||
log::warn!("failed to retrieve Stripe billing object");
|
||||
return;
|
||||
};
|
||||
let Some(llm_db) = app.llm_db.clone() else {
|
||||
log::warn!("failed to retrieve LLM database");
|
||||
return;
|
||||
};
|
||||
|
||||
let executor = app.executor.clone();
|
||||
executor.spawn_detached({
|
||||
let executor = executor.clone();
|
||||
async move {
|
||||
loop {
|
||||
sync_model_request_usage_with_stripe(&app, &llm_db, &stripe_billing)
|
||||
.await
|
||||
.context("failed to sync LLM request usage to Stripe")
|
||||
.trace_err();
|
||||
executor
|
||||
.sleep(SYNC_LLM_REQUEST_USAGE_WITH_STRIPE_INTERVAL)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn sync_model_request_usage_with_stripe(
|
||||
app: &Arc<AppState>,
|
||||
llm_db: &Arc<LlmDatabase>,
|
||||
stripe_billing: &Arc<StripeBilling>,
|
||||
) -> anyhow::Result<()> {
|
||||
let usage_meters = llm_db
|
||||
.get_current_subscription_usage_meters(Utc::now())
|
||||
.await?;
|
||||
let user_ids = usage_meters
|
||||
.iter()
|
||||
.map(|(_, usage)| usage.user_id)
|
||||
.collect::<HashSet<UserId>>();
|
||||
let billing_subscriptions = app
|
||||
.db
|
||||
.get_active_zed_pro_billing_subscriptions(user_ids)
|
||||
.await?;
|
||||
|
||||
let claude_3_5_sonnet = stripe_billing
|
||||
.find_price_by_lookup_key("claude-3-5-sonnet-requests")
|
||||
.await?;
|
||||
let claude_3_7_sonnet = stripe_billing
|
||||
.find_price_by_lookup_key("claude-3-7-sonnet-requests")
|
||||
.await?;
|
||||
let claude_3_7_sonnet_max = stripe_billing
|
||||
.find_price_by_lookup_key("claude-3-7-sonnet-requests-max")
|
||||
.await?;
|
||||
|
||||
for (usage_meter, usage) in usage_meters {
|
||||
maybe!(async {
|
||||
let Some((billing_customer, billing_subscription)) =
|
||||
billing_subscriptions.get(&usage.user_id)
|
||||
else {
|
||||
bail!(
|
||||
"Attempted to sync usage meter for user who is not a Stripe customer: {}",
|
||||
usage.user_id
|
||||
);
|
||||
};
|
||||
|
||||
let stripe_customer_id = billing_customer
|
||||
.stripe_customer_id
|
||||
.parse::<stripe::CustomerId>()
|
||||
.context("failed to parse Stripe customer ID from database")?;
|
||||
let stripe_subscription_id = billing_subscription
|
||||
.stripe_subscription_id
|
||||
.parse::<stripe::SubscriptionId>()
|
||||
.context("failed to parse Stripe subscription ID from database")?;
|
||||
|
||||
let model = llm_db.model_by_id(usage_meter.model_id)?;
|
||||
|
||||
let (price_id, meter_event_name) = match model.name.as_str() {
|
||||
"claude-3-5-sonnet" => (&claude_3_5_sonnet.id, "claude_3_5_sonnet/requests"),
|
||||
"claude-3-7-sonnet" => match usage_meter.mode {
|
||||
CompletionMode::Normal => (&claude_3_7_sonnet.id, "claude_3_7_sonnet/requests"),
|
||||
CompletionMode::Max => {
|
||||
(&claude_3_7_sonnet_max.id, "claude_3_7_sonnet/requests/max")
|
||||
}
|
||||
},
|
||||
model_name => {
|
||||
bail!("Attempted to sync usage meter for unsupported model: {model_name:?}")
|
||||
}
|
||||
};
|
||||
|
||||
stripe_billing
|
||||
.subscribe_to_price(&stripe_subscription_id, price_id)
|
||||
.await?;
|
||||
stripe_billing
|
||||
.bill_model_request_usage(
|
||||
&stripe_customer_id,
|
||||
meter_event_name,
|
||||
usage_meter.requests,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -191,38 +191,6 @@ impl Database {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_active_zed_pro_billing_subscriptions(
|
||||
&self,
|
||||
user_ids: HashSet<UserId>,
|
||||
) -> Result<HashMap<UserId, (billing_customer::Model, billing_subscription::Model)>> {
|
||||
self.transaction(|tx| {
|
||||
let user_ids = user_ids.clone();
|
||||
async move {
|
||||
let mut rows = billing_subscription::Entity::find()
|
||||
.inner_join(billing_customer::Entity)
|
||||
.select_also(billing_customer::Entity)
|
||||
.filter(billing_customer::Column::UserId.is_in(user_ids))
|
||||
.filter(
|
||||
billing_subscription::Column::StripeSubscriptionStatus
|
||||
.eq(StripeSubscriptionStatus::Active),
|
||||
)
|
||||
.filter(billing_subscription::Column::Kind.eq(SubscriptionKind::ZedPro))
|
||||
.order_by_asc(billing_subscription::Column::Id)
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
|
||||
let mut subscriptions = HashMap::default();
|
||||
while let Some(row) = rows.next().await {
|
||||
if let (subscription, Some(customer)) = row? {
|
||||
subscriptions.insert(customer.user_id, (customer, subscription));
|
||||
}
|
||||
}
|
||||
Ok(subscriptions)
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns whether the user has an active billing subscription.
|
||||
pub async fn has_active_billing_subscription(&self, user_id: UserId) -> Result<bool> {
|
||||
Ok(self.count_active_billing_subscriptions(user_id).await? > 0)
|
||||
|
||||
@@ -5,8 +5,6 @@ use crate::Cents;
|
||||
|
||||
pub use token::*;
|
||||
|
||||
pub const AGENT_EXTENDED_TRIAL_FEATURE_FLAG: &str = "agent-extended-trial";
|
||||
|
||||
/// The maximum monthly spending an individual user can reach on the free tier
|
||||
/// before they have to pay.
|
||||
pub const FREE_TIER_MONTHLY_SPENDING_LIMIT: Cents = Cents::from_dollars(10);
|
||||
|
||||
@@ -2,6 +2,5 @@ use super::*;
|
||||
|
||||
pub mod billing_events;
|
||||
pub mod providers;
|
||||
pub mod subscription_usage_meters;
|
||||
pub mod subscription_usages;
|
||||
pub mod usages;
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
use crate::llm::db::queries::subscription_usages::convert_chrono_to_time;
|
||||
|
||||
use super::*;
|
||||
|
||||
impl LlmDatabase {
|
||||
/// Returns all current subscription usage meters as of the given timestamp.
|
||||
pub async fn get_current_subscription_usage_meters(
|
||||
&self,
|
||||
now: DateTimeUtc,
|
||||
) -> Result<Vec<(subscription_usage_meter::Model, subscription_usage::Model)>> {
|
||||
let now = convert_chrono_to_time(now)?;
|
||||
|
||||
self.transaction(|tx| async move {
|
||||
let result = subscription_usage_meter::Entity::find()
|
||||
.inner_join(subscription_usage::Entity)
|
||||
.filter(
|
||||
subscription_usage::Column::PeriodStartAt
|
||||
.lte(now)
|
||||
.and(subscription_usage::Column::PeriodEndAt.gte(now)),
|
||||
)
|
||||
.select_also(subscription_usage::Entity)
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
|
||||
let result = result
|
||||
.into_iter()
|
||||
.filter_map(|(meter, usage)| {
|
||||
let usage = usage?;
|
||||
Some((meter, usage))
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(result)
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ use crate::db::{UserId, billing_subscription};
|
||||
|
||||
use super::*;
|
||||
|
||||
pub fn convert_chrono_to_time(datetime: DateTimeUtc) -> anyhow::Result<PrimitiveDateTime> {
|
||||
fn convert_chrono_to_time(datetime: DateTimeUtc) -> anyhow::Result<PrimitiveDateTime> {
|
||||
use chrono::{Datelike as _, Timelike as _};
|
||||
|
||||
let date = time::Date::from_calendar_date(
|
||||
|
||||
@@ -3,6 +3,5 @@ pub mod model;
|
||||
pub mod monthly_usage;
|
||||
pub mod provider;
|
||||
pub mod subscription_usage;
|
||||
pub mod subscription_usage_meter;
|
||||
pub mod usage;
|
||||
pub mod usage_measure;
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::llm::db::ModelId;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "subscription_usage_meters")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub subscription_usage_id: i32,
|
||||
pub model_id: ModelId,
|
||||
pub mode: CompletionMode,
|
||||
pub requests: i32,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::subscription_usage::Entity",
|
||||
from = "Column::SubscriptionUsageId",
|
||||
to = "super::subscription_usage::Column::Id"
|
||||
)]
|
||||
SubscriptionUsage,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::model::Entity",
|
||||
from = "Column::ModelId",
|
||||
to = "super::model::Column::Id"
|
||||
)]
|
||||
Model,
|
||||
}
|
||||
|
||||
impl Related<super::subscription_usage::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::SubscriptionUsage.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::model::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Model.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Hash, Serialize)]
|
||||
#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CompletionMode {
|
||||
#[sea_orm(string_value = "normal")]
|
||||
Normal,
|
||||
#[sea_orm(string_value = "max")]
|
||||
Max,
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
use crate::Cents;
|
||||
use crate::db::billing_subscription::SubscriptionKind;
|
||||
use crate::db::{billing_subscription, user};
|
||||
use crate::llm::{
|
||||
AGENT_EXTENDED_TRIAL_FEATURE_FLAG, DEFAULT_MAX_MONTHLY_SPEND, FREE_TIER_MONTHLY_SPENDING_LIMIT,
|
||||
};
|
||||
use crate::llm::{DEFAULT_MAX_MONTHLY_SPEND, FREE_TIER_MONTHLY_SPENDING_LIMIT};
|
||||
use crate::{Config, db::billing_preference};
|
||||
use anyhow::{Result, anyhow};
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
@@ -34,8 +32,6 @@ pub struct LlmTokenClaims {
|
||||
pub custom_llm_monthly_allowance_in_cents: Option<u32>,
|
||||
pub plan: Plan,
|
||||
#[serde(default)]
|
||||
pub has_extended_trial: bool,
|
||||
#[serde(default)]
|
||||
pub subscription_period: Option<(NaiveDateTime, NaiveDateTime)>,
|
||||
#[serde(default)]
|
||||
pub enable_model_request_overages: bool,
|
||||
@@ -98,9 +94,6 @@ impl LlmTokenClaims {
|
||||
SubscriptionKind::ZedPro => Plan::ZedPro,
|
||||
SubscriptionKind::ZedProTrial => Plan::ZedProTrial,
|
||||
}),
|
||||
has_extended_trial: feature_flags
|
||||
.iter()
|
||||
.any(|flag| flag == AGENT_EXTENDED_TRIAL_FEATURE_FLAG),
|
||||
subscription_period: maybe!({
|
||||
let subscription = subscription?;
|
||||
let period_start_at = subscription.current_period_start_at()?;
|
||||
|
||||
@@ -8,9 +8,7 @@ use axum::{
|
||||
};
|
||||
|
||||
use collab::api::CloudflareIpCountryHeader;
|
||||
use collab::api::billing::{
|
||||
sync_llm_request_usage_with_stripe_periodically, sync_llm_token_usage_with_stripe_periodically,
|
||||
};
|
||||
use collab::api::billing::sync_llm_usage_with_stripe_periodically;
|
||||
use collab::llm::db::LlmDatabase;
|
||||
use collab::migrations::run_database_migrations;
|
||||
use collab::user_backfiller::spawn_user_backfiller;
|
||||
@@ -154,8 +152,7 @@ async fn main() -> Result<()> {
|
||||
|
||||
if let Some(mut llm_db) = llm_db {
|
||||
llm_db.initialize().await?;
|
||||
sync_llm_request_usage_with_stripe_periodically(state.clone());
|
||||
sync_llm_token_usage_with_stripe_periodically(state.clone());
|
||||
sync_llm_usage_with_stripe_periodically(state.clone());
|
||||
}
|
||||
|
||||
app = app
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::llm::{self, AGENT_EXTENDED_TRIAL_FEATURE_FLAG};
|
||||
use crate::{Cents, Result};
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use crate::{Cents, Result, llm};
|
||||
use anyhow::Context as _;
|
||||
use chrono::{Datelike, Utc};
|
||||
use collections::HashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use stripe::PriceId;
|
||||
use tokio::sync::RwLock;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct StripeBilling {
|
||||
state: RwLock<StripeBillingState>,
|
||||
@@ -19,10 +17,9 @@ pub struct StripeBilling {
|
||||
struct StripeBillingState {
|
||||
meters_by_event_name: HashMap<String, StripeMeter>,
|
||||
price_ids_by_meter_id: HashMap<String, stripe::PriceId>,
|
||||
prices_by_lookup_key: HashMap<String, stripe::Price>,
|
||||
}
|
||||
|
||||
pub struct StripeModelTokenPrices {
|
||||
pub struct StripeModel {
|
||||
input_tokens_price: StripeBillingPrice,
|
||||
input_cache_creation_tokens_price: StripeBillingPrice,
|
||||
input_cache_read_tokens_price: StripeBillingPrice,
|
||||
@@ -65,10 +62,6 @@ impl StripeBilling {
|
||||
}
|
||||
|
||||
for price in prices.data {
|
||||
if let Some(lookup_key) = price.lookup_key.clone() {
|
||||
state.prices_by_lookup_key.insert(lookup_key, price.clone());
|
||||
}
|
||||
|
||||
if let Some(recurring) = price.recurring {
|
||||
if let Some(meter) = recurring.meter {
|
||||
state.price_ids_by_meter_id.insert(meter, price.id);
|
||||
@@ -81,49 +74,36 @@ impl StripeBilling {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn find_price_by_lookup_key(&self, lookup_key: &str) -> Result<stripe::Price> {
|
||||
self.state
|
||||
.read()
|
||||
.await
|
||||
.prices_by_lookup_key
|
||||
.get(lookup_key)
|
||||
.cloned()
|
||||
.ok_or_else(|| crate::Error::Internal(anyhow!("no price ID found for {lookup_key:?}")))
|
||||
}
|
||||
|
||||
pub async fn register_model_for_token_based_usage(
|
||||
&self,
|
||||
model: &llm::db::model::Model,
|
||||
) -> Result<StripeModelTokenPrices> {
|
||||
pub async fn register_model(&self, model: &llm::db::model::Model) -> Result<StripeModel> {
|
||||
let input_tokens_price = self
|
||||
.get_or_insert_token_price(
|
||||
.get_or_insert_price(
|
||||
&format!("model_{}/input_tokens", model.id),
|
||||
&format!("{} (Input Tokens)", model.name),
|
||||
Cents::new(model.price_per_million_input_tokens as u32),
|
||||
)
|
||||
.await?;
|
||||
let input_cache_creation_tokens_price = self
|
||||
.get_or_insert_token_price(
|
||||
.get_or_insert_price(
|
||||
&format!("model_{}/input_cache_creation_tokens", model.id),
|
||||
&format!("{} (Input Cache Creation Tokens)", model.name),
|
||||
Cents::new(model.price_per_million_cache_creation_input_tokens as u32),
|
||||
)
|
||||
.await?;
|
||||
let input_cache_read_tokens_price = self
|
||||
.get_or_insert_token_price(
|
||||
.get_or_insert_price(
|
||||
&format!("model_{}/input_cache_read_tokens", model.id),
|
||||
&format!("{} (Input Cache Read Tokens)", model.name),
|
||||
Cents::new(model.price_per_million_cache_read_input_tokens as u32),
|
||||
)
|
||||
.await?;
|
||||
let output_tokens_price = self
|
||||
.get_or_insert_token_price(
|
||||
.get_or_insert_price(
|
||||
&format!("model_{}/output_tokens", model.id),
|
||||
&format!("{} (Output Tokens)", model.name),
|
||||
Cents::new(model.price_per_million_output_tokens as u32),
|
||||
)
|
||||
.await?;
|
||||
Ok(StripeModelTokenPrices {
|
||||
Ok(StripeModel {
|
||||
input_tokens_price,
|
||||
input_cache_creation_tokens_price,
|
||||
input_cache_read_tokens_price,
|
||||
@@ -131,7 +111,7 @@ impl StripeBilling {
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_or_insert_token_price(
|
||||
async fn get_or_insert_price(
|
||||
&self,
|
||||
meter_event_name: &str,
|
||||
price_description: &str,
|
||||
@@ -227,43 +207,10 @@ impl StripeBilling {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn subscribe_to_price(
|
||||
&self,
|
||||
subscription_id: &stripe::SubscriptionId,
|
||||
price_id: &stripe::PriceId,
|
||||
) -> Result<()> {
|
||||
let subscription =
|
||||
stripe::Subscription::retrieve(&self.client, &subscription_id, &[]).await?;
|
||||
|
||||
if subscription_contains_price(&subscription, price_id) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
stripe::Subscription::update(
|
||||
&self.client,
|
||||
subscription_id,
|
||||
stripe::UpdateSubscription {
|
||||
items: Some(vec![stripe::UpdateSubscriptionItems {
|
||||
price: Some(price_id.to_string()),
|
||||
..Default::default()
|
||||
}]),
|
||||
trial_settings: Some(stripe::UpdateSubscriptionTrialSettings {
|
||||
end_behavior: stripe::UpdateSubscriptionTrialSettingsEndBehavior {
|
||||
missing_payment_method: stripe::UpdateSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::Cancel,
|
||||
},
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn subscribe_to_model(
|
||||
&self,
|
||||
subscription_id: &stripe::SubscriptionId,
|
||||
model: &StripeModelTokenPrices,
|
||||
model: &StripeModel,
|
||||
) -> Result<()> {
|
||||
let subscription =
|
||||
stripe::Subscription::retrieve(&self.client, &subscription_id, &[]).await?;
|
||||
@@ -321,10 +268,10 @@ impl StripeBilling {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn bill_model_token_usage(
|
||||
pub async fn bill_model_usage(
|
||||
&self,
|
||||
customer_id: &stripe::CustomerId,
|
||||
model: &StripeModelTokenPrices,
|
||||
model: &StripeModel,
|
||||
event: &llm::db::billing_event::Model,
|
||||
) -> Result<()> {
|
||||
let timestamp = Utc::now().timestamp();
|
||||
@@ -396,37 +343,11 @@ impl StripeBilling {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn bill_model_request_usage(
|
||||
&self,
|
||||
customer_id: &stripe::CustomerId,
|
||||
event_name: &str,
|
||||
requests: i32,
|
||||
) -> Result<()> {
|
||||
let timestamp = Utc::now().timestamp();
|
||||
let idempotency_key = Uuid::new_v4();
|
||||
|
||||
StripeMeterEvent::create(
|
||||
&self.client,
|
||||
StripeCreateMeterEventParams {
|
||||
identifier: &format!("model_requests/{}", idempotency_key),
|
||||
event_name,
|
||||
payload: StripeCreateMeterEventPayload {
|
||||
value: requests as u64,
|
||||
stripe_customer_id: customer_id,
|
||||
},
|
||||
timestamp: Some(timestamp),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn checkout(
|
||||
&self,
|
||||
customer_id: stripe::CustomerId,
|
||||
github_login: &str,
|
||||
model: &StripeModelTokenPrices,
|
||||
model: &StripeModel,
|
||||
success_url: &str,
|
||||
) -> Result<String> {
|
||||
let first_of_next_month = Utc::now()
|
||||
@@ -490,36 +411,16 @@ impl StripeBilling {
|
||||
zed_pro_price_id: PriceId,
|
||||
customer_id: stripe::CustomerId,
|
||||
github_login: &str,
|
||||
feature_flags: Vec<String>,
|
||||
success_url: &str,
|
||||
) -> Result<String> {
|
||||
let eligible_for_extended_trial = feature_flags
|
||||
.iter()
|
||||
.any(|flag| flag == AGENT_EXTENDED_TRIAL_FEATURE_FLAG);
|
||||
|
||||
let trial_period_days = if eligible_for_extended_trial { 60 } else { 14 };
|
||||
|
||||
let mut subscription_metadata = std::collections::HashMap::new();
|
||||
if eligible_for_extended_trial {
|
||||
subscription_metadata.insert(
|
||||
"promo_feature_flag".to_string(),
|
||||
AGENT_EXTENDED_TRIAL_FEATURE_FLAG.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let mut params = stripe::CreateCheckoutSession::new();
|
||||
params.subscription_data = Some(stripe::CreateCheckoutSessionSubscriptionData {
|
||||
trial_period_days: Some(trial_period_days),
|
||||
trial_period_days: Some(14),
|
||||
trial_settings: Some(stripe::CreateCheckoutSessionSubscriptionDataTrialSettings {
|
||||
end_behavior: stripe::CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehavior {
|
||||
missing_payment_method: stripe::CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehaviorMissingPaymentMethod::Pause,
|
||||
}
|
||||
}),
|
||||
metadata: if !subscription_metadata.is_empty() {
|
||||
Some(subscription_metadata)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
params.mode = Some(stripe::CheckoutSessionMode::Subscription);
|
||||
|
||||
@@ -393,23 +393,6 @@ impl ChannelView {
|
||||
buffer.acknowledge_buffer_version(cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn get_channel(&self, cx: &App) -> (SharedString, Option<SharedString>) {
|
||||
if let Some(channel) = self.channel(cx) {
|
||||
let status = match (
|
||||
self.channel_buffer.read(cx).buffer().read(cx).read_only(),
|
||||
self.channel_buffer.read(cx).is_connected(),
|
||||
) {
|
||||
(false, true) => None,
|
||||
(true, true) => Some("read-only"),
|
||||
(_, false) => Some("disconnected"),
|
||||
};
|
||||
|
||||
(channel.name.clone(), status.map(Into::into))
|
||||
} else {
|
||||
("<unknown>".into(), Some("disconnected".into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<EditorEvent> for ChannelView {}
|
||||
@@ -457,21 +440,26 @@ impl Item for ChannelView {
|
||||
Some(Icon::new(icon))
|
||||
}
|
||||
|
||||
fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString {
|
||||
let (name, status) = self.get_channel(cx);
|
||||
if let Some(status) = status {
|
||||
format!("{name} - {status}").into()
|
||||
} else {
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
fn tab_content(&self, params: TabContentParams, _: &Window, cx: &App) -> gpui::AnyElement {
|
||||
let (name, status) = self.get_channel(cx);
|
||||
let (channel_name, status) = if let Some(channel) = self.channel(cx) {
|
||||
let status = match (
|
||||
self.channel_buffer.read(cx).buffer().read(cx).read_only(),
|
||||
self.channel_buffer.read(cx).is_connected(),
|
||||
) {
|
||||
(false, true) => None,
|
||||
(true, true) => Some("read-only"),
|
||||
(_, false) => Some("disconnected"),
|
||||
};
|
||||
|
||||
(channel.name.clone(), status)
|
||||
} else {
|
||||
("<unknown>".into(), Some("disconnected"))
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Label::new(name)
|
||||
Label::new(channel_name)
|
||||
.color(params.text_color())
|
||||
.when(params.preview, |this| this.italic()),
|
||||
)
|
||||
@@ -552,6 +540,10 @@ impl Item for ChannelView {
|
||||
fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
|
||||
Editor::to_item_events(event, f)
|
||||
}
|
||||
|
||||
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
|
||||
"Channels".into()
|
||||
}
|
||||
}
|
||||
|
||||
impl FollowableItem for ChannelView {
|
||||
|
||||
@@ -122,7 +122,7 @@ mod tests {
|
||||
#[gpui::test]
|
||||
async fn test_saves_and_retrieves_command_invocation() {
|
||||
let db =
|
||||
CommandPaletteDB::open_test_db("test_saves_and_retrieves_command_invocation").await;
|
||||
CommandPaletteDB(db::open_test_db("test_saves_and_retrieves_command_invocation").await);
|
||||
|
||||
let retrieved_cmd = db.get_last_invoked("editor: backspace").unwrap();
|
||||
|
||||
@@ -142,7 +142,7 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_gets_usage_history() {
|
||||
let db = CommandPaletteDB::open_test_db("test_gets_usage_history").await;
|
||||
let db = CommandPaletteDB(db::open_test_db("test_gets_usage_history").await);
|
||||
db.write_command_invocation("go to line: toggle", "200")
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -167,7 +167,7 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_lists_ordered_by_usage() {
|
||||
let db = CommandPaletteDB::open_test_db("test_lists_ordered_by_usage").await;
|
||||
let db = CommandPaletteDB(db::open_test_db("test_lists_ordered_by_usage").await);
|
||||
|
||||
let empty_commands = db.list_commands_used();
|
||||
match &empty_commands {
|
||||
@@ -200,7 +200,7 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_handles_max_invocation_entries() {
|
||||
let db = CommandPaletteDB::open_test_db("test_handles_max_invocation_entries").await;
|
||||
let db = CommandPaletteDB(db::open_test_db("test_handles_max_invocation_entries").await);
|
||||
|
||||
for i in 1..=1001 {
|
||||
db.write_command_invocation("some-command", &i.to_string())
|
||||
|
||||
@@ -23,7 +23,7 @@ use project::Project;
|
||||
use ui::{Divider, HighlightedLabel, ListItem, ListSubHeader, prelude::*};
|
||||
|
||||
use ui_input::SingleLineInput;
|
||||
use workspace::{AppState, ItemId, SerializableItem, delete_unloaded_items};
|
||||
use workspace::{AppState, ItemId, SerializableItem};
|
||||
use workspace::{Item, Workspace, WorkspaceId, item::ItemEvent};
|
||||
|
||||
pub fn init(app_state: Arc<AppState>, cx: &mut App) {
|
||||
@@ -860,13 +860,11 @@ impl SerializableItem for ComponentPreview {
|
||||
_window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<gpui::Result<()>> {
|
||||
delete_unloaded_items(
|
||||
alive_items,
|
||||
workspace_id,
|
||||
"component_previews",
|
||||
&COMPONENT_PREVIEW_DB,
|
||||
cx,
|
||||
)
|
||||
cx.background_spawn(async move {
|
||||
COMPONENT_PREVIEW_DB
|
||||
.delete_unloaded_items(workspace_id, alive_items)
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
fn serialize(
|
||||
|
||||
@@ -47,4 +47,30 @@ impl ComponentPreviewDb {
|
||||
WHERE item_id = ? AND workspace_id = ?
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_unloaded_items(
|
||||
&self,
|
||||
workspace: WorkspaceId,
|
||||
alive_items: Vec<ItemId>,
|
||||
) -> Result<()> {
|
||||
let placeholders = alive_items
|
||||
.iter()
|
||||
.map(|_| "?")
|
||||
.collect::<Vec<&str>>()
|
||||
.join(", ");
|
||||
|
||||
let query = format!(
|
||||
"DELETE FROM component_previews WHERE workspace_id = ? AND item_id NOT IN ({placeholders})"
|
||||
);
|
||||
|
||||
self.write(move |conn| {
|
||||
let mut statement = Statement::prepare(conn, query)?;
|
||||
let mut next_index = statement.bind(&workspace, 1)?;
|
||||
for id in alive_items {
|
||||
next_index = statement.bind(&id, next_index)?;
|
||||
}
|
||||
statement.exec()
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,10 +121,6 @@ impl TcpArguments {
|
||||
/// an optional build step is completed, we turn it's result into a DebugTaskDefinition by running a locator (or using a user-provided task) and resolving task variables.
|
||||
/// Finally, a [DebugTaskDefinition] has to be turned into a concrete debugger invocation ([DebugAdapterBinary]).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(
|
||||
any(feature = "test-support", test),
|
||||
derive(serde::Deserialize, serde::Serialize)
|
||||
)]
|
||||
pub struct DebugTaskDefinition {
|
||||
pub label: SharedString,
|
||||
pub adapter: SharedString,
|
||||
@@ -528,7 +524,6 @@ impl FakeAdapter {
|
||||
} else {
|
||||
None
|
||||
},
|
||||
"raw_request": serde_json::to_value(config).unwrap()
|
||||
});
|
||||
let request = match config.request {
|
||||
DebugRequest::Launch(_) => dap_types::StartDebuggingRequestArgumentsRequest::Launch,
|
||||
|
||||
@@ -45,9 +45,9 @@ pub static ALL_FILE_DB_FAILED: LazyLock<AtomicBool> = LazyLock::new(|| AtomicBoo
|
||||
/// This will retry a couple times if there are failures. If opening fails once, the db directory
|
||||
/// is moved to a backup folder and a new one is created. If that fails, a shared in memory db is created.
|
||||
/// In either case, static variables are set so that the user can be notified.
|
||||
pub async fn open_db<M: Migrator + 'static>(db_dir: &Path, scope: &str) -> ThreadSafeConnection {
|
||||
pub async fn open_db<M: Migrator + 'static>(db_dir: &Path, scope: &str) -> ThreadSafeConnection<M> {
|
||||
if *ZED_STATELESS {
|
||||
return open_fallback_db::<M>().await;
|
||||
return open_fallback_db().await;
|
||||
}
|
||||
|
||||
let main_db_dir = db_dir.join(format!("0-{}", scope));
|
||||
@@ -58,7 +58,7 @@ pub async fn open_db<M: Migrator + 'static>(db_dir: &Path, scope: &str) -> Threa
|
||||
.context("Could not create db directory")
|
||||
.log_err()?;
|
||||
let db_path = main_db_dir.join(Path::new(DB_FILE_NAME));
|
||||
open_main_db::<M>(&db_path).await
|
||||
open_main_db(&db_path).await
|
||||
})
|
||||
.await;
|
||||
|
||||
@@ -70,12 +70,12 @@ pub async fn open_db<M: Migrator + 'static>(db_dir: &Path, scope: &str) -> Threa
|
||||
ALL_FILE_DB_FAILED.store(true, Ordering::Release);
|
||||
|
||||
// If still failed, create an in memory db with a known name
|
||||
open_fallback_db::<M>().await
|
||||
open_fallback_db().await
|
||||
}
|
||||
|
||||
async fn open_main_db<M: Migrator>(db_path: &Path) -> Option<ThreadSafeConnection> {
|
||||
async fn open_main_db<M: Migrator>(db_path: &Path) -> Option<ThreadSafeConnection<M>> {
|
||||
log::info!("Opening main db");
|
||||
ThreadSafeConnection::builder::<M>(db_path.to_string_lossy().as_ref(), true)
|
||||
ThreadSafeConnection::<M>::builder(db_path.to_string_lossy().as_ref(), true)
|
||||
.with_db_initialization_query(DB_INITIALIZE_QUERY)
|
||||
.with_connection_initialize_query(CONNECTION_INITIALIZE_QUERY)
|
||||
.build()
|
||||
@@ -83,9 +83,9 @@ async fn open_main_db<M: Migrator>(db_path: &Path) -> Option<ThreadSafeConnectio
|
||||
.log_err()
|
||||
}
|
||||
|
||||
async fn open_fallback_db<M: Migrator>() -> ThreadSafeConnection {
|
||||
async fn open_fallback_db<M: Migrator>() -> ThreadSafeConnection<M> {
|
||||
log::info!("Opening fallback db");
|
||||
ThreadSafeConnection::builder::<M>(FALLBACK_DB_NAME, false)
|
||||
ThreadSafeConnection::<M>::builder(FALLBACK_DB_NAME, false)
|
||||
.with_db_initialization_query(DB_INITIALIZE_QUERY)
|
||||
.with_connection_initialize_query(CONNECTION_INITIALIZE_QUERY)
|
||||
.build()
|
||||
@@ -96,10 +96,10 @@ async fn open_fallback_db<M: Migrator>() -> ThreadSafeConnection {
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub async fn open_test_db<M: Migrator>(db_name: &str) -> ThreadSafeConnection {
|
||||
pub async fn open_test_db<M: Migrator>(db_name: &str) -> ThreadSafeConnection<M> {
|
||||
use sqlez::thread_safe_connection::locking_queue;
|
||||
|
||||
ThreadSafeConnection::builder::<M>(db_name, false)
|
||||
ThreadSafeConnection::<M>::builder(db_name, false)
|
||||
.with_db_initialization_query(DB_INITIALIZE_QUERY)
|
||||
.with_connection_initialize_query(CONNECTION_INITIALIZE_QUERY)
|
||||
// Serialize queued writes via a mutex and run them synchronously
|
||||
@@ -113,10 +113,10 @@ pub async fn open_test_db<M: Migrator>(db_name: &str) -> ThreadSafeConnection {
|
||||
#[macro_export]
|
||||
macro_rules! define_connection {
|
||||
(pub static ref $id:ident: $t:ident<()> = $migrations:expr; $($global:ident)?) => {
|
||||
pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection);
|
||||
pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection<$t>);
|
||||
|
||||
impl ::std::ops::Deref for $t {
|
||||
type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection;
|
||||
type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection<$t>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
@@ -133,16 +133,9 @@ macro_rules! define_connection {
|
||||
}
|
||||
}
|
||||
|
||||
impl $t {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub async fn open_test_db(name: &'static str) -> Self {
|
||||
$t($crate::open_test_db::<$t>(name).await)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| {
|
||||
$t($crate::smol::block_on($crate::open_test_db::<$t>(stringify!($id))))
|
||||
$t($crate::smol::block_on($crate::open_test_db(stringify!($id))))
|
||||
});
|
||||
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
@@ -153,14 +146,14 @@ macro_rules! define_connection {
|
||||
} else {
|
||||
$crate::RELEASE_CHANNEL.dev_name()
|
||||
};
|
||||
$t($crate::smol::block_on($crate::open_db::<$t>(db_dir, scope)))
|
||||
$t($crate::smol::block_on($crate::open_db(db_dir, scope)))
|
||||
});
|
||||
};
|
||||
(pub static ref $id:ident: $t:ident<$($d:ty),+> = $migrations:expr; $($global:ident)?) => {
|
||||
pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection);
|
||||
pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection<( $($d),+, $t )>);
|
||||
|
||||
impl ::std::ops::Deref for $t {
|
||||
type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection;
|
||||
type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection<($($d),+, $t)>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
@@ -179,7 +172,7 @@ macro_rules! define_connection {
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| {
|
||||
$t($crate::smol::block_on($crate::open_test_db::<($($d),+, $t)>(stringify!($id))))
|
||||
$t($crate::smol::block_on($crate::open_test_db(stringify!($id))))
|
||||
});
|
||||
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
@@ -190,7 +183,7 @@ macro_rules! define_connection {
|
||||
} else {
|
||||
$crate::RELEASE_CHANNEL.dev_name()
|
||||
};
|
||||
$t($crate::smol::block_on($crate::open_db::<($($d),+, $t)>(db_dir, scope)))
|
||||
$t($crate::smol::block_on($crate::open_db(db_dir, scope)))
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_kvp() {
|
||||
let db = KeyValueStore::open_test_db("test_kvp").await;
|
||||
let db = KeyValueStore(crate::open_test_db("test_kvp").await);
|
||||
|
||||
assert_eq!(db.read_kvp("key-1").unwrap(), None);
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ use crate::{
|
||||
};
|
||||
use crate::{new_session_modal::NewSessionModal, session::DebugSession};
|
||||
use anyhow::{Result, anyhow};
|
||||
use collections::HashMap;
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use dap::DebugRequest;
|
||||
use dap::{
|
||||
@@ -14,6 +15,7 @@ use dap::{
|
||||
client::SessionId, debugger_settings::DebuggerSettings,
|
||||
};
|
||||
use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
|
||||
use futures::{SinkExt as _, channel::mpsc};
|
||||
use gpui::{
|
||||
Action, App, AsyncWindowContext, Context, DismissEvent, Entity, EntityId, EventEmitter,
|
||||
FocusHandle, Focusable, MouseButton, MouseDownEvent, Point, Subscription, Task, WeakEntity,
|
||||
@@ -22,11 +24,21 @@ use gpui::{
|
||||
|
||||
use language::Buffer;
|
||||
use project::debugger::session::{Session, SessionStateEvent};
|
||||
use project::{Project, debugger::session::ThreadStatus};
|
||||
use project::{
|
||||
Project,
|
||||
debugger::{
|
||||
dap_store::{self, DapStore},
|
||||
session::ThreadStatus,
|
||||
},
|
||||
terminals::TerminalKind,
|
||||
};
|
||||
use rpc::proto::{self};
|
||||
use settings::Settings;
|
||||
use std::any::TypeId;
|
||||
use task::{DebugScenario, TaskContext};
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use task::{DebugScenario, HideStrategy, RevealStrategy, RevealTarget, TaskContext, TaskId};
|
||||
use terminal_view::TerminalView;
|
||||
use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*};
|
||||
use workspace::SplitDirection;
|
||||
use workspace::{
|
||||
@@ -62,21 +74,27 @@ pub struct DebugPanel {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
focus_handle: FocusHandle,
|
||||
context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl DebugPanel {
|
||||
pub fn new(
|
||||
workspace: &Workspace,
|
||||
_window: &mut Window,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> Entity<Self> {
|
||||
cx.new(|cx| {
|
||||
let project = workspace.project().clone();
|
||||
let dap_store = project.read(cx).dap_store();
|
||||
|
||||
let _subscriptions =
|
||||
vec![cx.subscribe_in(&dap_store, window, Self::handle_dap_store_event)];
|
||||
|
||||
let debug_panel = Self {
|
||||
size: px(300.),
|
||||
sessions: vec![],
|
||||
active_session: None,
|
||||
_subscriptions,
|
||||
past_debug_definition: None,
|
||||
focus_handle: cx.focus_handle(),
|
||||
project,
|
||||
@@ -92,14 +110,20 @@ impl DebugPanel {
|
||||
let (has_active_session, supports_restart, support_step_back, status) = self
|
||||
.active_session()
|
||||
.map(|item| {
|
||||
let running = item.read(cx).running_state().clone();
|
||||
let caps = running.read(cx).capabilities(cx);
|
||||
(
|
||||
!running.read(cx).session().read(cx).is_terminated(),
|
||||
caps.supports_restart_request.unwrap_or_default(),
|
||||
caps.supports_step_back.unwrap_or_default(),
|
||||
running.read(cx).thread_status(cx),
|
||||
)
|
||||
let running = item.read(cx).mode().as_running().cloned();
|
||||
|
||||
match running {
|
||||
Some(running) => {
|
||||
let caps = running.read(cx).capabilities(cx);
|
||||
(
|
||||
!running.read(cx).session().read(cx).is_terminated(),
|
||||
caps.supports_restart_request.unwrap_or_default(),
|
||||
caps.supports_step_back.unwrap_or_default(),
|
||||
running.read(cx).thread_status(cx),
|
||||
)
|
||||
}
|
||||
None => (false, false, false, None),
|
||||
}
|
||||
})
|
||||
.unwrap_or((false, false, false, None));
|
||||
|
||||
@@ -264,7 +288,7 @@ impl DebugPanel {
|
||||
cx.subscribe_in(
|
||||
&session,
|
||||
window,
|
||||
move |this, session, event: &SessionStateEvent, window, cx| match event {
|
||||
move |_, session, event: &SessionStateEvent, window, cx| match event {
|
||||
SessionStateEvent::Restart => {
|
||||
let mut curr_session = session.clone();
|
||||
while let Some(parent_session) = curr_session
|
||||
@@ -286,9 +310,6 @@ impl DebugPanel {
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
SessionStateEvent::SpawnChildSession { request } => {
|
||||
this.handle_start_debugging_request(request, session.clone(), window, cx);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
)
|
||||
@@ -302,11 +323,11 @@ impl DebugPanel {
|
||||
this.sessions.retain(|session| {
|
||||
session
|
||||
.read(cx)
|
||||
.running_state()
|
||||
.read(cx)
|
||||
.session()
|
||||
.read(cx)
|
||||
.is_terminated()
|
||||
.mode()
|
||||
.as_running()
|
||||
.map_or(false, |running_state| {
|
||||
!running_state.read(cx).session().read(cx).is_terminated()
|
||||
})
|
||||
});
|
||||
|
||||
let session_item = DebugSession::running(
|
||||
@@ -319,13 +340,11 @@ impl DebugPanel {
|
||||
cx,
|
||||
);
|
||||
|
||||
// We might want to make this an event subscription and only notify when a new thread is selected
|
||||
// This is used to filter the command menu correctly
|
||||
cx.observe(
|
||||
&session_item.read(cx).running_state().clone(),
|
||||
|_, _, cx| cx.notify(),
|
||||
)
|
||||
.detach();
|
||||
if let Some(running) = session_item.read(cx).mode().as_running().cloned() {
|
||||
// We might want to make this an event subscription and only notify when a new thread is selected
|
||||
// This is used to filter the command menu correctly
|
||||
cx.observe(&running, |_, _, cx| cx.notify()).detach();
|
||||
}
|
||||
|
||||
this.sessions.push(session_item.clone());
|
||||
this.activate_session(session_item, window, cx);
|
||||
@@ -338,7 +357,7 @@ impl DebugPanel {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn handle_start_debugging_request(
|
||||
pub fn start_child_session(
|
||||
&mut self,
|
||||
request: &StartDebuggingRequestArguments,
|
||||
parent_session: Entity<Session>,
|
||||
@@ -400,6 +419,47 @@ impl DebugPanel {
|
||||
self.active_session.clone()
|
||||
}
|
||||
|
||||
fn handle_dap_store_event(
|
||||
&mut self,
|
||||
_dap_store: &Entity<DapStore>,
|
||||
event: &dap_store::DapStoreEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
dap_store::DapStoreEvent::RunInTerminal {
|
||||
session_id,
|
||||
title,
|
||||
cwd,
|
||||
command,
|
||||
args,
|
||||
envs,
|
||||
sender,
|
||||
..
|
||||
} => {
|
||||
self.handle_run_in_terminal_request(
|
||||
*session_id,
|
||||
title.clone(),
|
||||
cwd.clone(),
|
||||
command.clone(),
|
||||
args.clone(),
|
||||
envs.clone(),
|
||||
sender.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
dap_store::DapStoreEvent::SpawnChildSession {
|
||||
request,
|
||||
parent_session,
|
||||
} => {
|
||||
self.start_child_session(request, parent_session.clone(), window, cx);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_scenario(
|
||||
&self,
|
||||
scenario: DebugScenario,
|
||||
@@ -469,6 +529,101 @@ impl DebugPanel {
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_run_in_terminal_request(
|
||||
&self,
|
||||
session_id: SessionId,
|
||||
title: Option<String>,
|
||||
cwd: Option<Arc<Path>>,
|
||||
command: Option<String>,
|
||||
args: Vec<String>,
|
||||
envs: HashMap<String, String>,
|
||||
mut sender: mpsc::Sender<Result<u32>>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let Some(session) = self
|
||||
.sessions
|
||||
.iter()
|
||||
.find(|s| s.read(cx).session_id(cx) == session_id)
|
||||
else {
|
||||
return Task::ready(Err(anyhow!("no session {:?} found", session_id)));
|
||||
};
|
||||
let running = session.read(cx).running_state();
|
||||
let cwd = cwd.map(|p| p.to_path_buf());
|
||||
let shell = self
|
||||
.project
|
||||
.read(cx)
|
||||
.terminal_settings(&cwd, cx)
|
||||
.shell
|
||||
.clone();
|
||||
let kind = if let Some(command) = command {
|
||||
let title = title.clone().unwrap_or(command.clone());
|
||||
TerminalKind::Task(task::SpawnInTerminal {
|
||||
id: TaskId("debug".to_string()),
|
||||
full_label: title.clone(),
|
||||
label: title.clone(),
|
||||
command: command.clone(),
|
||||
args,
|
||||
command_label: title.clone(),
|
||||
cwd,
|
||||
env: envs,
|
||||
use_new_terminal: true,
|
||||
allow_concurrent_runs: true,
|
||||
reveal: RevealStrategy::NoFocus,
|
||||
reveal_target: RevealTarget::Dock,
|
||||
hide: HideStrategy::Never,
|
||||
shell,
|
||||
show_summary: false,
|
||||
show_command: false,
|
||||
show_rerun: false,
|
||||
})
|
||||
} else {
|
||||
TerminalKind::Shell(cwd.map(|c| c.to_path_buf()))
|
||||
};
|
||||
|
||||
let workspace = self.workspace.clone();
|
||||
let project = self.project.downgrade();
|
||||
|
||||
let terminal_task = self.project.update(cx, |project, cx| {
|
||||
project.create_terminal(kind, window.window_handle(), cx)
|
||||
});
|
||||
let terminal_task = cx.spawn_in(window, async move |_, cx| {
|
||||
let terminal = terminal_task.await?;
|
||||
|
||||
let terminal_view = cx.new_window_entity(|window, cx| {
|
||||
TerminalView::new(terminal.clone(), workspace, None, project, window, cx)
|
||||
})?;
|
||||
|
||||
running.update_in(cx, |running, window, cx| {
|
||||
running.ensure_pane_item(DebuggerPaneItem::Terminal, window, cx);
|
||||
running.debug_terminal.update(cx, |debug_terminal, cx| {
|
||||
debug_terminal.terminal = Some(terminal_view);
|
||||
cx.notify();
|
||||
});
|
||||
})?;
|
||||
|
||||
anyhow::Ok(terminal.read_with(cx, |terminal, _| terminal.pty_info.pid())?)
|
||||
});
|
||||
|
||||
cx.background_spawn(async move {
|
||||
match terminal_task.await {
|
||||
Ok(pid_task) => match pid_task {
|
||||
Some(pid) => sender.send(Ok(pid.as_u32())).await?,
|
||||
None => {
|
||||
sender
|
||||
.send(Err(anyhow!(
|
||||
"Terminal was spawned but PID was not available"
|
||||
)))
|
||||
.await?
|
||||
}
|
||||
},
|
||||
Err(error) => sender.send(Err(anyhow!(error))).await?,
|
||||
};
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn close_session(&mut self, entity_id: EntityId, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(session) = self
|
||||
.sessions
|
||||
@@ -479,9 +634,11 @@ impl DebugPanel {
|
||||
return;
|
||||
};
|
||||
session.update(cx, |this, cx| {
|
||||
this.running_state().update(cx, |this, cx| {
|
||||
this.serialize_layout(window, cx);
|
||||
});
|
||||
if let Some(running) = this.mode().as_running() {
|
||||
running.update(cx, |this, cx| {
|
||||
this.serialize_layout(window, cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
let session_id = session.update(cx, |this, cx| this.session_id(cx));
|
||||
let should_prompt = self
|
||||
@@ -618,7 +775,7 @@ impl DebugPanel {
|
||||
if let Some(running_state) = self
|
||||
.active_session
|
||||
.as_ref()
|
||||
.map(|session| session.read(cx).running_state().clone())
|
||||
.and_then(|session| session.read(cx).mode().as_running().cloned())
|
||||
{
|
||||
let pane_items_status = running_state.read(cx).pane_items_status(cx);
|
||||
let this = cx.weak_entity();
|
||||
@@ -629,10 +786,10 @@ impl DebugPanel {
|
||||
let this = this.clone();
|
||||
move |window, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
if let Some(running_state) = this
|
||||
.active_session
|
||||
.as_ref()
|
||||
.map(|session| session.read(cx).running_state().clone())
|
||||
if let Some(running_state) =
|
||||
this.active_session.as_ref().and_then(|session| {
|
||||
session.read(cx).mode().as_running().cloned()
|
||||
})
|
||||
{
|
||||
running_state.update(cx, |state, cx| {
|
||||
if is_visible {
|
||||
@@ -675,7 +832,7 @@ impl DebugPanel {
|
||||
h_flex().gap_2().w_full().when_some(
|
||||
active_session
|
||||
.as_ref()
|
||||
.map(|session| session.read(cx).running_state()),
|
||||
.and_then(|session| session.read(cx).mode().as_running()),
|
||||
|this, running_session| {
|
||||
let thread_status = running_session
|
||||
.read(cx)
|
||||
@@ -913,7 +1070,7 @@ impl DebugPanel {
|
||||
.when_some(
|
||||
active_session
|
||||
.as_ref()
|
||||
.map(|session| session.read(cx).running_state())
|
||||
.and_then(|session| session.read(cx).mode().as_running())
|
||||
.cloned(),
|
||||
|this, session| {
|
||||
this.child(
|
||||
@@ -976,10 +1133,12 @@ impl DebugPanel {
|
||||
) {
|
||||
if let Some(session) = self.active_session() {
|
||||
session.update(cx, |session, cx| {
|
||||
session.running_state().update(cx, |running, cx| {
|
||||
running.activate_pane_in_direction(direction, window, cx);
|
||||
})
|
||||
});
|
||||
if let Some(running) = session.mode().as_running() {
|
||||
running.update(cx, |running, cx| {
|
||||
running.activate_pane_in_direction(direction, window, cx);
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -991,10 +1150,12 @@ impl DebugPanel {
|
||||
) {
|
||||
if let Some(session) = self.active_session() {
|
||||
session.update(cx, |session, cx| {
|
||||
session.running_state().update(cx, |running, cx| {
|
||||
running.activate_item(item, window, cx);
|
||||
});
|
||||
});
|
||||
if let Some(running) = session.mode().as_running() {
|
||||
running.update(cx, |running, cx| {
|
||||
running.activate_item(item, window, cx);
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1007,9 +1168,11 @@ impl DebugPanel {
|
||||
debug_assert!(self.sessions.contains(&session_item));
|
||||
session_item.focus_handle(cx).focus(window);
|
||||
session_item.update(cx, |this, cx| {
|
||||
this.running_state().update(cx, |this, cx| {
|
||||
this.go_to_selected_stack_frame(window, cx);
|
||||
});
|
||||
if let Some(running) = this.mode().as_running() {
|
||||
running.update(cx, |this, cx| {
|
||||
this.go_to_selected_stack_frame(window, cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
self.active_session = Some(session_item);
|
||||
cx.notify();
|
||||
@@ -1094,7 +1257,7 @@ impl Render for DebugPanel {
|
||||
if self
|
||||
.active_session
|
||||
.as_ref()
|
||||
.map(|session| session.read(cx).running_state())
|
||||
.and_then(|session| session.read(cx).mode().as_running().cloned())
|
||||
.map(|state| state.read(cx).has_open_context_menu(cx))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
@@ -1212,9 +1375,10 @@ impl Render for DebugPanel {
|
||||
if this
|
||||
.active_session
|
||||
.as_ref()
|
||||
.map(|session| {
|
||||
let state = session.read(cx).running_state();
|
||||
state.read(cx).has_pane_at_position(event.position)
|
||||
.and_then(|session| {
|
||||
session.read(cx).mode().as_running().map(|state| {
|
||||
state.read(cx).has_pane_at_position(event.position)
|
||||
})
|
||||
})
|
||||
.unwrap_or(false)
|
||||
{
|
||||
|
||||
@@ -64,7 +64,7 @@ pub fn init(cx: &mut App) {
|
||||
if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| {
|
||||
panel
|
||||
.active_session()
|
||||
.map(|session| session.read(cx).running_state().clone())
|
||||
.and_then(|session| session.read(cx).mode().as_running().cloned())
|
||||
}) {
|
||||
active_item.update(cx, |item, cx| item.pause_thread(cx))
|
||||
}
|
||||
@@ -75,7 +75,7 @@ pub fn init(cx: &mut App) {
|
||||
if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| {
|
||||
panel
|
||||
.active_session()
|
||||
.map(|session| session.read(cx).running_state().clone())
|
||||
.and_then(|session| session.read(cx).mode().as_running().cloned())
|
||||
}) {
|
||||
active_item.update(cx, |item, cx| item.restart_session(cx))
|
||||
}
|
||||
@@ -86,7 +86,7 @@ pub fn init(cx: &mut App) {
|
||||
if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| {
|
||||
panel
|
||||
.active_session()
|
||||
.map(|session| session.read(cx).running_state().clone())
|
||||
.and_then(|session| session.read(cx).mode().as_running().cloned())
|
||||
}) {
|
||||
active_item.update(cx, |item, cx| item.step_in(cx))
|
||||
}
|
||||
@@ -97,7 +97,7 @@ pub fn init(cx: &mut App) {
|
||||
if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| {
|
||||
panel
|
||||
.active_session()
|
||||
.map(|session| session.read(cx).running_state().clone())
|
||||
.and_then(|session| session.read(cx).mode().as_running().cloned())
|
||||
}) {
|
||||
active_item.update(cx, |item, cx| item.step_over(cx))
|
||||
}
|
||||
@@ -108,7 +108,7 @@ pub fn init(cx: &mut App) {
|
||||
if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| {
|
||||
panel
|
||||
.active_session()
|
||||
.map(|session| session.read(cx).running_state().clone())
|
||||
.and_then(|session| session.read(cx).mode().as_running().cloned())
|
||||
}) {
|
||||
active_item.update(cx, |item, cx| item.step_back(cx))
|
||||
}
|
||||
@@ -119,7 +119,7 @@ pub fn init(cx: &mut App) {
|
||||
if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| {
|
||||
panel
|
||||
.active_session()
|
||||
.map(|session| session.read(cx).running_state().clone())
|
||||
.and_then(|session| session.read(cx).mode().as_running().cloned())
|
||||
}) {
|
||||
cx.defer(move |cx| {
|
||||
active_item.update(cx, |item, cx| item.stop_thread(cx))
|
||||
@@ -132,7 +132,7 @@ pub fn init(cx: &mut App) {
|
||||
if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| {
|
||||
panel
|
||||
.active_session()
|
||||
.map(|session| session.read(cx).running_state().clone())
|
||||
.and_then(|session| session.read(cx).mode().as_running().cloned())
|
||||
}) {
|
||||
active_item.update(cx, |item, cx| item.toggle_ignore_breakpoints(cx))
|
||||
}
|
||||
@@ -209,8 +209,11 @@ pub fn init(cx: &mut App) {
|
||||
state: debugger::breakpoint_store::BreakpointState::Enabled,
|
||||
};
|
||||
|
||||
active_session.update(cx, |session, cx| {
|
||||
session.running_state().update(cx, |state, cx| {
|
||||
active_session
|
||||
.update(cx, |session_item, _| {
|
||||
session_item.mode().as_running().cloned()
|
||||
})?
|
||||
.update(cx, |state, cx| {
|
||||
if let Some(thread_id) = state.selected_thread_id() {
|
||||
state.session().update(cx, |session, cx| {
|
||||
session.run_to_position(
|
||||
@@ -221,7 +224,6 @@ pub fn init(cx: &mut App) {
|
||||
})
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Some(())
|
||||
});
|
||||
@@ -244,16 +246,17 @@ pub fn init(cx: &mut App) {
|
||||
cx,
|
||||
)?;
|
||||
|
||||
active_session.update(cx, |session, cx| {
|
||||
session.running_state().update(cx, |state, cx| {
|
||||
active_session
|
||||
.update(cx, |session_item, _| {
|
||||
session_item.mode().as_running().cloned()
|
||||
})?
|
||||
.update(cx, |state, cx| {
|
||||
let stack_id = state.selected_stack_frame_id(cx);
|
||||
|
||||
state.session().update(cx, |session, cx| {
|
||||
session.evaluate(text, None, stack_id, None, cx).detach();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Some(())
|
||||
});
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::sync::OnceLock;
|
||||
use dap::client::SessionId;
|
||||
use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity};
|
||||
use project::Project;
|
||||
use project::debugger::session::Session;
|
||||
use project::debugger::{dap_store::DapStore, session::Session};
|
||||
use project::worktree_store::WorktreeStore;
|
||||
use rpc::proto::{self, PeerId};
|
||||
use running::RunningState;
|
||||
@@ -18,10 +18,23 @@ use workspace::{
|
||||
use crate::debugger_panel::DebugPanel;
|
||||
use crate::persistence::SerializedPaneLayout;
|
||||
|
||||
pub(crate) enum DebugSessionState {
|
||||
Running(Entity<running::RunningState>),
|
||||
}
|
||||
|
||||
impl DebugSessionState {
|
||||
pub(crate) fn as_running(&self) -> Option<&Entity<running::RunningState>> {
|
||||
match &self {
|
||||
DebugSessionState::Running(entity) => Some(entity),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DebugSession {
|
||||
remote_id: Option<workspace::ViewId>,
|
||||
running_state: Entity<RunningState>,
|
||||
mode: DebugSessionState,
|
||||
label: OnceLock<SharedString>,
|
||||
dap_store: WeakEntity<DapStore>,
|
||||
_debug_panel: WeakEntity<DebugPanel>,
|
||||
_worktree_store: WeakEntity<WorktreeStore>,
|
||||
_workspace: WeakEntity<Workspace>,
|
||||
@@ -44,7 +57,7 @@ impl DebugSession {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<Self> {
|
||||
let running_state = cx.new(|cx| {
|
||||
let mode = cx.new(|cx| {
|
||||
RunningState::new(
|
||||
session.clone(),
|
||||
project.clone(),
|
||||
@@ -56,12 +69,13 @@ impl DebugSession {
|
||||
});
|
||||
|
||||
cx.new(|cx| Self {
|
||||
_subscriptions: [cx.subscribe(&running_state, |_, _, _, cx| {
|
||||
_subscriptions: [cx.subscribe(&mode, |_, _, _, cx| {
|
||||
cx.notify();
|
||||
})],
|
||||
remote_id: None,
|
||||
running_state,
|
||||
mode: DebugSessionState::Running(mode),
|
||||
label: OnceLock::new(),
|
||||
dap_store: project.read(cx).dap_store().downgrade(),
|
||||
_debug_panel,
|
||||
_worktree_store: project.read(cx).worktree_store().downgrade(),
|
||||
_workspace: workspace,
|
||||
@@ -69,16 +83,31 @@ impl DebugSession {
|
||||
}
|
||||
|
||||
pub(crate) fn session_id(&self, cx: &App) -> SessionId {
|
||||
self.running_state.read(cx).session_id()
|
||||
match &self.mode {
|
||||
DebugSessionState::Running(entity) => entity.read(cx).session_id(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn session(&self, cx: &App) -> Entity<Session> {
|
||||
self.running_state.read(cx).session().clone()
|
||||
match &self.mode {
|
||||
DebugSessionState::Running(entity) => entity.read(cx).session().clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn shutdown(&mut self, cx: &mut Context<Self>) {
|
||||
self.running_state
|
||||
.update(cx, |state, cx| state.shutdown(cx));
|
||||
match &self.mode {
|
||||
DebugSessionState::Running(state) => state.update(cx, |state, cx| state.shutdown(cx)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn mode(&self) -> &DebugSessionState {
|
||||
&self.mode
|
||||
}
|
||||
|
||||
pub(crate) fn running_state(&self) -> Entity<RunningState> {
|
||||
match &self.mode {
|
||||
DebugSessionState::Running(running_state) => running_state.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn label(&self, cx: &App) -> SharedString {
|
||||
@@ -86,40 +115,36 @@ impl DebugSession {
|
||||
return label.clone();
|
||||
}
|
||||
|
||||
let session = self.running_state.read(cx).session();
|
||||
let session_id = match &self.mode {
|
||||
DebugSessionState::Running(running_state) => running_state.read(cx).session_id(),
|
||||
};
|
||||
|
||||
let Ok(Some(session)) = self
|
||||
.dap_store
|
||||
.read_with(cx, |store, _| store.session_by_id(session_id))
|
||||
else {
|
||||
return "".into();
|
||||
};
|
||||
|
||||
self.label
|
||||
.get_or_init(|| session.read(cx).label())
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
pub(crate) fn running_state(&self) -> &Entity<RunningState> {
|
||||
&self.running_state
|
||||
}
|
||||
|
||||
pub(crate) fn label_element(&self, cx: &App) -> AnyElement {
|
||||
let label = self.label(cx);
|
||||
|
||||
let icon = {
|
||||
if self
|
||||
.running_state
|
||||
.read(cx)
|
||||
.session()
|
||||
.read(cx)
|
||||
.is_terminated()
|
||||
{
|
||||
Some(Indicator::dot().color(Color::Error))
|
||||
} else {
|
||||
match self
|
||||
.running_state
|
||||
.read(cx)
|
||||
.thread_status(cx)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
project::debugger::session::ThreadStatus::Stopped => {
|
||||
Some(Indicator::dot().color(Color::Conflict))
|
||||
let icon = match &self.mode {
|
||||
DebugSessionState::Running(state) => {
|
||||
if state.read(cx).session().read(cx).is_terminated() {
|
||||
Some(Indicator::dot().color(Color::Error))
|
||||
} else {
|
||||
match state.read(cx).thread_status(cx).unwrap_or_default() {
|
||||
project::debugger::session::ThreadStatus::Stopped => {
|
||||
Some(Indicator::dot().color(Color::Conflict))
|
||||
}
|
||||
_ => Some(Indicator::dot().color(Color::Success)),
|
||||
}
|
||||
_ => Some(Indicator::dot().color(Color::Success)),
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -137,7 +162,9 @@ impl EventEmitter<DebugPanelItemEvent> for DebugSession {}
|
||||
|
||||
impl Focusable for DebugSession {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
self.running_state.focus_handle(cx)
|
||||
match &self.mode {
|
||||
DebugSessionState::Running(running_state) => running_state.focus_handle(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,7 +243,10 @@ impl FollowableItem for DebugSession {
|
||||
|
||||
impl Render for DebugSession {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
self.running_state
|
||||
.update(cx, |this, cx| this.render(window, cx).into_any_element())
|
||||
match &self.mode {
|
||||
DebugSessionState::Running(running_state) => {
|
||||
running_state.update(cx, |this, cx| this.render(window, cx).into_any_element())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,20 +5,15 @@ pub(crate) mod module_list;
|
||||
pub mod stack_frame_list;
|
||||
pub mod variable_list;
|
||||
|
||||
use std::{any::Any, ops::ControlFlow, path::PathBuf, sync::Arc, time::Duration};
|
||||
use std::{any::Any, ops::ControlFlow, sync::Arc, time::Duration};
|
||||
|
||||
use crate::persistence::{self, DebuggerPaneItem, SerializedPaneLayout};
|
||||
|
||||
use super::DebugPanelItemEvent;
|
||||
use anyhow::{Result, anyhow};
|
||||
use breakpoint_list::BreakpointList;
|
||||
use collections::{HashMap, IndexMap};
|
||||
use console::Console;
|
||||
use dap::{
|
||||
Capabilities, RunInTerminalRequestArguments, Thread, client::SessionId,
|
||||
debugger_settings::DebuggerSettings,
|
||||
};
|
||||
use futures::{SinkExt, channel::mpsc};
|
||||
use dap::{Capabilities, Thread, client::SessionId, debugger_settings::DebuggerSettings};
|
||||
use gpui::{
|
||||
Action as _, AnyView, AppContext, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
|
||||
NoAction, Pixels, Point, Subscription, Task, WeakEntity,
|
||||
@@ -28,10 +23,8 @@ use module_list::ModuleList;
|
||||
use project::{
|
||||
Project,
|
||||
debugger::session::{Session, SessionEvent, ThreadId, ThreadStatus},
|
||||
terminals::TerminalKind,
|
||||
};
|
||||
use rpc::proto::ViewId;
|
||||
use serde_json::Value;
|
||||
use settings::Settings;
|
||||
use stack_frame_list::StackFrameList;
|
||||
use terminal_view::TerminalView;
|
||||
@@ -39,7 +32,7 @@ use ui::{
|
||||
ActiveTheme, AnyElement, App, ButtonCommon as _, Clickable as _, Context, ContextMenu,
|
||||
DropdownMenu, FluentBuilder, IconButton, IconName, IconSize, InteractiveElement, IntoElement,
|
||||
Label, LabelCommon as _, ParentElement, Render, SharedString, StatefulInteractiveElement,
|
||||
Styled, Tab, Tooltip, VisibleOnHover, VisualContext, Window, div, h_flex, v_flex,
|
||||
Styled, Tab, Tooltip, VisibleOnHover, Window, div, h_flex, v_flex,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use variable_list::VariableList;
|
||||
@@ -566,9 +559,6 @@ impl RunningState {
|
||||
this.remove_pane_item(DebuggerPaneItem::LoadedSources, window, cx);
|
||||
}
|
||||
}
|
||||
SessionEvent::RunInTerminal { request, sender } => this
|
||||
.handle_run_in_terminal(request, sender.clone(), window, cx)
|
||||
.detach_and_log_err(cx),
|
||||
|
||||
_ => {}
|
||||
}
|
||||
@@ -667,111 +657,6 @@ impl RunningState {
|
||||
self.panes.pane_at_pixel_position(position).is_some()
|
||||
}
|
||||
|
||||
fn handle_run_in_terminal(
|
||||
&self,
|
||||
request: &RunInTerminalRequestArguments,
|
||||
mut sender: mpsc::Sender<Result<u32>>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let running = cx.entity();
|
||||
let Ok(project) = self
|
||||
.workspace
|
||||
.update(cx, |workspace, _| workspace.project().clone())
|
||||
else {
|
||||
return Task::ready(Err(anyhow!("no workspace")));
|
||||
};
|
||||
let session = self.session.read(cx);
|
||||
|
||||
let cwd = Some(&request.cwd)
|
||||
.filter(|cwd| cwd.len() > 0)
|
||||
.map(PathBuf::from)
|
||||
.or_else(|| session.binary().cwd.clone());
|
||||
|
||||
let mut args = request.args.clone();
|
||||
|
||||
// Handle special case for NodeJS debug adapter
|
||||
// If only the Node binary path is provided, we set the command to None
|
||||
// This prevents the NodeJS REPL from appearing, which is not the desired behavior
|
||||
// The expected usage is for users to provide their own Node command, e.g., `node test.js`
|
||||
// This allows the NodeJS debug client to attach correctly
|
||||
let command = if args.len() > 1 {
|
||||
Some(args.remove(0))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut envs: HashMap<String, String> = Default::default();
|
||||
if let Some(Value::Object(env)) = &request.env {
|
||||
for (key, value) in env {
|
||||
let value_str = match (key.as_str(), value) {
|
||||
(_, Value::String(value)) => value,
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
envs.insert(key.clone(), value_str.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let shell = project.read(cx).terminal_settings(&cwd, cx).shell.clone();
|
||||
let kind = if let Some(command) = command {
|
||||
let title = request.title.clone().unwrap_or(command.clone());
|
||||
TerminalKind::Task(task::SpawnInTerminal {
|
||||
id: task::TaskId("debug".to_string()),
|
||||
full_label: title.clone(),
|
||||
label: title.clone(),
|
||||
command: command.clone(),
|
||||
args,
|
||||
command_label: title.clone(),
|
||||
cwd,
|
||||
env: envs,
|
||||
use_new_terminal: true,
|
||||
allow_concurrent_runs: true,
|
||||
reveal: task::RevealStrategy::NoFocus,
|
||||
reveal_target: task::RevealTarget::Dock,
|
||||
hide: task::HideStrategy::Never,
|
||||
shell,
|
||||
show_summary: false,
|
||||
show_command: false,
|
||||
show_rerun: false,
|
||||
})
|
||||
} else {
|
||||
TerminalKind::Shell(cwd.map(|c| c.to_path_buf()))
|
||||
};
|
||||
|
||||
let workspace = self.workspace.clone();
|
||||
let weak_project = project.downgrade();
|
||||
|
||||
let terminal_task = project.update(cx, |project, cx| {
|
||||
project.create_terminal(kind, window.window_handle(), cx)
|
||||
});
|
||||
let terminal_task = cx.spawn_in(window, async move |_, cx| {
|
||||
let terminal = terminal_task.await?;
|
||||
|
||||
let terminal_view = cx.new_window_entity(|window, cx| {
|
||||
TerminalView::new(terminal.clone(), workspace, None, weak_project, window, cx)
|
||||
})?;
|
||||
|
||||
running.update_in(cx, |running, window, cx| {
|
||||
running.ensure_pane_item(DebuggerPaneItem::Terminal, window, cx);
|
||||
running.debug_terminal.update(cx, |debug_terminal, cx| {
|
||||
debug_terminal.terminal = Some(terminal_view);
|
||||
cx.notify();
|
||||
});
|
||||
})?;
|
||||
|
||||
terminal.read_with(cx, |terminal, _| {
|
||||
terminal
|
||||
.pty_info
|
||||
.pid()
|
||||
.map(|pid| pid.as_u32())
|
||||
.ok_or_else(|| anyhow!("Terminal was spawned but PID was not available"))
|
||||
})?
|
||||
});
|
||||
|
||||
cx.background_spawn(async move { anyhow::Ok(sender.send(terminal_task.await).await?) })
|
||||
}
|
||||
|
||||
fn create_sub_view(
|
||||
&self,
|
||||
item_kind: DebuggerPaneItem,
|
||||
|
||||
@@ -45,7 +45,7 @@ impl Console {
|
||||
let mut editor = Editor::multi_line(window, cx);
|
||||
editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
|
||||
editor.set_read_only(true);
|
||||
editor.set_show_gutter(false, cx);
|
||||
editor.set_show_gutter(true, cx);
|
||||
editor.set_show_runnables(false, cx);
|
||||
editor.set_show_breakpoints(false, cx);
|
||||
editor.set_show_code_actions(false, cx);
|
||||
@@ -57,8 +57,6 @@ impl Console {
|
||||
editor.set_show_wrap_guides(false, cx);
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
editor.set_show_edit_predictions(Some(false), window, cx);
|
||||
editor.set_use_modal_editing(false);
|
||||
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
|
||||
editor
|
||||
});
|
||||
let focus_handle = cx.focus_handle();
|
||||
@@ -148,23 +146,6 @@ impl Console {
|
||||
expression
|
||||
});
|
||||
|
||||
self.add_messages(
|
||||
[OutputEvent {
|
||||
category: None,
|
||||
output: format!("> {expression}"),
|
||||
group: None,
|
||||
variables_reference: None,
|
||||
source: None,
|
||||
line: None,
|
||||
column: None,
|
||||
data: None,
|
||||
location_reference: None,
|
||||
}]
|
||||
.iter(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
self.session.update(cx, |session, cx| {
|
||||
session
|
||||
.evaluate(
|
||||
@@ -179,10 +160,6 @@ impl Console {
|
||||
}
|
||||
|
||||
fn render_console(&self, cx: &Context<Self>) -> impl IntoElement {
|
||||
EditorElement::new(&self.console, self.editor_style(cx))
|
||||
}
|
||||
|
||||
fn editor_style(&self, cx: &Context<Self>) -> EditorStyle {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = TextStyle {
|
||||
color: if self.console.read(cx).read_only(cx) {
|
||||
@@ -197,16 +174,44 @@ impl Console {
|
||||
line_height: relative(settings.buffer_line_height.value()),
|
||||
..Default::default()
|
||||
};
|
||||
EditorStyle {
|
||||
background: cx.theme().colors().editor_background,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
..Default::default()
|
||||
}
|
||||
|
||||
EditorElement::new(
|
||||
&self.console,
|
||||
EditorStyle {
|
||||
background: cx.theme().colors().editor_background,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn render_query_bar(&self, cx: &Context<Self>) -> impl IntoElement {
|
||||
EditorElement::new(&self.query_bar, self.editor_style(cx))
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = TextStyle {
|
||||
color: if self.console.read(cx).read_only(cx) {
|
||||
cx.theme().colors().text_disabled
|
||||
} else {
|
||||
cx.theme().colors().text
|
||||
},
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_fallbacks: settings.ui_font.fallbacks.clone(),
|
||||
font_size: TextSize::Editor.rems(cx).into(),
|
||||
font_weight: settings.ui_font.weight,
|
||||
line_height: relative(1.3),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
EditorElement::new(
|
||||
&self.query_bar,
|
||||
EditorStyle {
|
||||
background: cx.theme().colors().editor_background,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -118,8 +118,8 @@ pub fn start_debug_session_with<T: Fn(&Arc<DebugAdapterClient>) + 'static>(
|
||||
workspace
|
||||
.panel::<DebugPanel>(cx)
|
||||
.and_then(|panel| panel.read(cx).active_session())
|
||||
.map(|session| session.read(cx).running_state().read(cx).session())
|
||||
.cloned()
|
||||
.and_then(|session| session.read(cx).mode().as_running().cloned())
|
||||
.map(|running| running.read(cx).session().clone())
|
||||
.ok_or_else(|| anyhow!("Failed to get active session"))
|
||||
})??;
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ use project::{FakeFs, Project};
|
||||
use serde_json::json;
|
||||
use task::{AttachRequest, TcpArgumentsTemplate};
|
||||
use tests::{init_test, init_test_workspace};
|
||||
use util::path;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut TestAppContext) {
|
||||
@@ -16,14 +15,14 @@ async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut Te
|
||||
let fs = FakeFs::new(executor.clone());
|
||||
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
"/project",
|
||||
json!({
|
||||
"main.rs": "First line\nSecond line\nThird line\nFourth line",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
let project = Project::test(fs, ["/project".as_ref()], cx).await;
|
||||
let workspace = init_test_workspace(&project, cx).await;
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
|
||||
@@ -42,9 +41,7 @@ async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut Te
|
||||
},
|
||||
|client| {
|
||||
client.on_request::<dap::requests::Attach, _>(move |_, args| {
|
||||
let raw = &args.raw;
|
||||
assert_eq!(raw["request"], "attach");
|
||||
assert_eq!(raw["process_id"], 10);
|
||||
assert_eq!(json!({"request": "attach", "process_id": 10}), args.raw);
|
||||
|
||||
Ok(())
|
||||
});
|
||||
@@ -80,23 +77,21 @@ async fn test_show_attach_modal_and_select_process(
|
||||
let fs = FakeFs::new(executor.clone());
|
||||
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
"/project",
|
||||
json!({
|
||||
"main.rs": "First line\nSecond line\nThird line\nFourth line",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
let project = Project::test(fs, ["/project".as_ref()], cx).await;
|
||||
let workspace = init_test_workspace(&project, cx).await;
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
// Set up handlers for sessions spawned via modal.
|
||||
let _initialize_subscription =
|
||||
project::debugger::test::intercept_debug_sessions(cx, |client| {
|
||||
client.on_request::<dap::requests::Attach, _>(move |_, args| {
|
||||
let raw = &args.raw;
|
||||
assert_eq!(raw["request"], "attach");
|
||||
assert_eq!(raw["process_id"], 1);
|
||||
assert_eq!(json!({"request": "attach", "process_id": 1}), args.raw);
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
@@ -7,7 +7,6 @@ use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
|
||||
use project::{FakeFs, Project};
|
||||
use serde_json::json;
|
||||
use tests::{init_test, init_test_workspace};
|
||||
use util::path;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestAppContext) {
|
||||
@@ -16,14 +15,14 @@ async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestApp
|
||||
let fs = FakeFs::new(executor.clone());
|
||||
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
"/project",
|
||||
json!({
|
||||
"main.rs": "First line\nSecond line\nThird line\nFourth line",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
let project = Project::test(fs, ["/project".as_ref()], cx).await;
|
||||
let workspace = init_test_workspace(&project, cx).await;
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
workspace
|
||||
@@ -87,7 +86,10 @@ async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestApp
|
||||
let running_state =
|
||||
active_debug_session_panel(workspace, cx).update_in(cx, |item, window, cx| {
|
||||
cx.focus_self(window);
|
||||
item.running_state().clone()
|
||||
item.mode()
|
||||
.as_running()
|
||||
.expect("Session should be running by this point")
|
||||
.clone()
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
@@ -102,7 +104,7 @@ async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestApp
|
||||
|
||||
assert_eq!(
|
||||
"First console output line before thread stopped!\nFirst output line before thread stopped!\n",
|
||||
active_debug_session_panel.read(cx).running_state().read(cx).console().read(cx).editor().read(cx).text(cx).as_str()
|
||||
active_debug_session_panel.read(cx).mode().as_running().unwrap().read(cx).console().read(cx).editor().read(cx).text(cx).as_str()
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
@@ -151,7 +153,7 @@ async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestApp
|
||||
|
||||
assert_eq!(
|
||||
"First console output line before thread stopped!\nFirst output line before thread stopped!\nSecond output line after thread stopped!\nSecond console output line after thread stopped!\n",
|
||||
active_session_panel.read(cx).running_state().read(cx).console().read(cx).editor().read(cx).text(cx).as_str()
|
||||
active_session_panel.read(cx).mode().as_running().unwrap().read(cx).console().read(cx).editor().read(cx).text(cx).as_str()
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
@@ -5,7 +5,6 @@ use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
|
||||
use project::Project;
|
||||
use serde_json::json;
|
||||
use std::cell::OnceCell;
|
||||
use util::path;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_dap_logger_captures_all_session_rpc_messages(
|
||||
@@ -29,7 +28,7 @@ async fn test_dap_logger_captures_all_session_rpc_messages(
|
||||
// Create a filesystem with a simple project
|
||||
let fs = project::FakeFs::new(executor.clone());
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
"/project",
|
||||
json!({
|
||||
"main.rs": "fn main() {\n println!(\"Hello, world!\");\n}"
|
||||
}),
|
||||
@@ -43,7 +42,7 @@ async fn test_dap_logger_captures_all_session_rpc_messages(
|
||||
"log_store shouldn't contain any session IDs before any sessions were created"
|
||||
);
|
||||
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
let project = Project::test(fs, ["/project".as_ref()], cx).await;
|
||||
|
||||
let workspace = init_test_workspace(&project, cx).await;
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
use crate::{
|
||||
persistence::DebuggerPaneItem,
|
||||
tests::{start_debug_session, start_debug_session_with},
|
||||
*,
|
||||
};
|
||||
use crate::{persistence::DebuggerPaneItem, tests::start_debug_session, *};
|
||||
use dap::{
|
||||
ErrorResponse, Message, RunInTerminalRequestArguments, SourceBreakpoint,
|
||||
StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
|
||||
adapters::DebugTaskDefinition,
|
||||
client::SessionId,
|
||||
requests::{
|
||||
Continue, Disconnect, Launch, Next, RunInTerminal, SetBreakpoints, StackTrace,
|
||||
@@ -14,7 +9,7 @@ use dap::{
|
||||
},
|
||||
};
|
||||
use editor::{
|
||||
ActiveDebugLine, Editor, EditorMode, MultiBuffer,
|
||||
Editor, EditorMode, MultiBuffer,
|
||||
actions::{self},
|
||||
};
|
||||
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
|
||||
@@ -24,14 +19,12 @@ use project::{
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::Path,
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
};
|
||||
use task::LaunchRequest;
|
||||
use terminal_view::terminal_panel::TerminalPanel;
|
||||
use tests::{active_debug_session_panel, init_test, init_test_workspace};
|
||||
use util::path;
|
||||
@@ -44,14 +37,14 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test
|
||||
let fs = FakeFs::new(executor.clone());
|
||||
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
"/project",
|
||||
json!({
|
||||
"main.rs": "First line\nSecond line\nThird line\nFourth line",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
let project = Project::test(fs, ["/project".as_ref()], cx).await;
|
||||
let workspace = init_test_workspace(&project, cx).await;
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
|
||||
@@ -84,7 +77,11 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test
|
||||
debug_panel.update(cx, |debug_panel, _| debug_panel.active_session().unwrap());
|
||||
|
||||
let running_state = active_session.update(cx, |active_session, _| {
|
||||
active_session.running_state().clone()
|
||||
active_session
|
||||
.mode()
|
||||
.as_running()
|
||||
.expect("Session should be running by this point")
|
||||
.clone()
|
||||
});
|
||||
|
||||
debug_panel.update(cx, |this, cx| {
|
||||
@@ -116,7 +113,11 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test
|
||||
.unwrap();
|
||||
|
||||
let running_state = active_session.update(cx, |active_session, _| {
|
||||
active_session.running_state().clone()
|
||||
active_session
|
||||
.mode()
|
||||
.as_running()
|
||||
.expect("Session should be running by this point")
|
||||
.clone()
|
||||
});
|
||||
|
||||
assert_eq!(client.id(), running_state.read(cx).session_id());
|
||||
@@ -145,7 +146,11 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test
|
||||
.unwrap();
|
||||
|
||||
let running_state = active_session.update(cx, |active_session, _| {
|
||||
active_session.running_state().clone()
|
||||
active_session
|
||||
.mode()
|
||||
.as_running()
|
||||
.expect("Session should be running by this point")
|
||||
.clone()
|
||||
});
|
||||
|
||||
debug_panel.update(cx, |this, cx| {
|
||||
@@ -169,14 +174,14 @@ async fn test_we_can_only_have_one_panel_per_debug_session(
|
||||
let fs = FakeFs::new(executor.clone());
|
||||
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
"/project",
|
||||
json!({
|
||||
"main.rs": "First line\nSecond line\nThird line\nFourth line",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
let project = Project::test(fs, ["/project".as_ref()], cx).await;
|
||||
let workspace = init_test_workspace(&project, cx).await;
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
|
||||
@@ -235,7 +240,11 @@ async fn test_we_can_only_have_one_panel_per_debug_session(
|
||||
.unwrap();
|
||||
|
||||
let running_state = active_session.update(cx, |active_session, _| {
|
||||
active_session.running_state().clone()
|
||||
active_session
|
||||
.mode()
|
||||
.as_running()
|
||||
.expect("Session should be running by this point")
|
||||
.clone()
|
||||
});
|
||||
|
||||
assert_eq!(client.id(), active_session.read(cx).session_id(cx));
|
||||
@@ -268,7 +277,11 @@ async fn test_we_can_only_have_one_panel_per_debug_session(
|
||||
.unwrap();
|
||||
|
||||
let running_state = active_session.update(cx, |active_session, _| {
|
||||
active_session.running_state().clone()
|
||||
active_session
|
||||
.mode()
|
||||
.as_running()
|
||||
.expect("Session should be running by this point")
|
||||
.clone()
|
||||
});
|
||||
|
||||
assert_eq!(client.id(), active_session.read(cx).session_id(cx));
|
||||
@@ -296,7 +309,11 @@ async fn test_we_can_only_have_one_panel_per_debug_session(
|
||||
.unwrap();
|
||||
|
||||
let running_state = active_session.update(cx, |active_session, _| {
|
||||
active_session.running_state().clone()
|
||||
active_session
|
||||
.mode()
|
||||
.as_running()
|
||||
.expect("Session should be running by this point")
|
||||
.clone()
|
||||
});
|
||||
|
||||
debug_panel.update(cx, |this, cx| {
|
||||
@@ -322,14 +339,14 @@ async fn test_handle_successful_run_in_terminal_reverse_request(
|
||||
let fs = FakeFs::new(executor.clone());
|
||||
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
"/project",
|
||||
json!({
|
||||
"main.rs": "First line\nSecond line\nThird line\nFourth line",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
let project = Project::test(fs, ["/project".as_ref()], cx).await;
|
||||
let workspace = init_test_workspace(&project, cx).await;
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
|
||||
@@ -393,14 +410,14 @@ async fn test_handle_start_debugging_request(
|
||||
let fs = FakeFs::new(executor.clone());
|
||||
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
"/project",
|
||||
json!({
|
||||
"main.rs": "First line\nSecond line\nThird line\nFourth line",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
let project = Project::test(fs, ["/project".as_ref()], cx).await;
|
||||
let workspace = init_test_workspace(&project, cx).await;
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
|
||||
@@ -469,14 +486,14 @@ async fn test_handle_error_run_in_terminal_reverse_request(
|
||||
let fs = FakeFs::new(executor.clone());
|
||||
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
"/project",
|
||||
json!({
|
||||
"main.rs": "First line\nSecond line\nThird line\nFourth line",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
let project = Project::test(fs, ["/project".as_ref()], cx).await;
|
||||
let workspace = init_test_workspace(&project, cx).await;
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
|
||||
@@ -499,8 +516,8 @@ async fn test_handle_error_run_in_terminal_reverse_request(
|
||||
.fake_reverse_request::<RunInTerminal>(RunInTerminalRequestArguments {
|
||||
kind: None,
|
||||
title: None,
|
||||
cwd: "".into(),
|
||||
args: vec!["oops".into(), "oops".into()],
|
||||
cwd: "/non-existing/path".into(), // invalid/non-existing path will cause the terminal spawn to fail
|
||||
args: vec![],
|
||||
env: None,
|
||||
args_can_be_interpreted_by_shell: None,
|
||||
})
|
||||
@@ -537,14 +554,14 @@ async fn test_handle_start_debugging_reverse_request(
|
||||
let fs = FakeFs::new(executor.clone());
|
||||
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
"/project",
|
||||
json!({
|
||||
"main.rs": "First line\nSecond line\nThird line\nFourth line",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
let project = Project::test(fs, ["/project".as_ref()], cx).await;
|
||||
let workspace = init_test_workspace(&project, cx).await;
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
|
||||
@@ -633,14 +650,14 @@ async fn test_shutdown_children_when_parent_session_shutdown(
|
||||
let fs = FakeFs::new(executor.clone());
|
||||
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
"/project",
|
||||
json!({
|
||||
"main.rs": "First line\nSecond line\nThird line\nFourth line",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
let project = Project::test(fs, ["/project".as_ref()], cx).await;
|
||||
let dap_store = project.update(cx, |project, _| project.dap_store());
|
||||
let workspace = init_test_workspace(&project, cx).await;
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
@@ -739,14 +756,14 @@ async fn test_shutdown_parent_session_if_all_children_are_shutdown(
|
||||
let fs = FakeFs::new(executor.clone());
|
||||
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
"/project",
|
||||
json!({
|
||||
"main.rs": "First line\nSecond line\nThird line\nFourth line",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
let project = Project::test(fs, ["/project".as_ref()], cx).await;
|
||||
let dap_store = project.update(cx, |project, _| project.dap_store());
|
||||
let workspace = init_test_workspace(&project, cx).await;
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
@@ -859,14 +876,14 @@ async fn test_debug_panel_item_thread_status_reset_on_failure(
|
||||
let fs = FakeFs::new(executor.clone());
|
||||
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
"/project",
|
||||
json!({
|
||||
"main.rs": "First line\nSecond line\nThird line\nFourth line",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
let project = Project::test(fs, ["/project".as_ref()], cx).await;
|
||||
let workspace = init_test_workspace(&project, cx).await;
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
|
||||
@@ -985,8 +1002,12 @@ async fn test_debug_panel_item_thread_status_reset_on_failure(
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
let running_state = active_debug_session_panel(workspace, cx)
|
||||
.update(cx, |item, _| item.running_state().clone());
|
||||
let running_state = active_debug_session_panel(workspace, cx).update_in(cx, |item, _, _| {
|
||||
item.mode()
|
||||
.as_running()
|
||||
.expect("Session should be running by this point")
|
||||
.clone()
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
let thread_id = ThreadId(1);
|
||||
@@ -1298,7 +1319,7 @@ async fn test_unsetting_breakpoints_on_clear_breakpoint_action(
|
||||
.expect("We should always send a breakpoint's path")
|
||||
.as_str()
|
||||
{
|
||||
path!("/project/main.rs") | path!("/project/second.rs") => {}
|
||||
"/project/main.rs" | "/project/second.rs" => {}
|
||||
_ => {
|
||||
panic!("Unset breakpoints for path that doesn't have any")
|
||||
}
|
||||
@@ -1326,14 +1347,14 @@ async fn test_debug_session_is_shutdown_when_attach_and_launch_request_fails(
|
||||
let fs = FakeFs::new(executor.clone());
|
||||
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
"/project",
|
||||
json!({
|
||||
"main.rs": "First line\nSecond line\nThird line\nFourth line",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
let project = Project::test(fs, ["/project".as_ref()], cx).await;
|
||||
let workspace = init_test_workspace(&project, cx).await;
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
|
||||
@@ -1363,330 +1384,3 @@ async fn test_debug_session_is_shutdown_when_attach_and_launch_request_fails(
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_we_send_arguments_from_user_config(
|
||||
executor: BackgroundExecutor,
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(executor.clone());
|
||||
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
json!({
|
||||
"main.rs": "First line\nSecond line\nThird line\nFourth line",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
let workspace = init_test_workspace(&project, cx).await;
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
let debug_definition = DebugTaskDefinition {
|
||||
adapter: "fake-adapter".into(),
|
||||
request: dap::DebugRequest::Launch(LaunchRequest {
|
||||
program: "main.rs".to_owned(),
|
||||
args: vec!["arg1".to_owned(), "arg2".to_owned()],
|
||||
cwd: Some(path!("/Random_path").into()),
|
||||
env: HashMap::from_iter(vec![("KEY".to_owned(), "VALUE".to_owned())]),
|
||||
}),
|
||||
label: "test".into(),
|
||||
initialize_args: None,
|
||||
tcp_connection: None,
|
||||
stop_on_entry: None,
|
||||
};
|
||||
|
||||
let launch_handler_called = Arc::new(AtomicBool::new(false));
|
||||
|
||||
start_debug_session_with(&workspace, cx, debug_definition.clone(), {
|
||||
let debug_definition = debug_definition.clone();
|
||||
let launch_handler_called = launch_handler_called.clone();
|
||||
|
||||
move |client| {
|
||||
let debug_definition = debug_definition.clone();
|
||||
let launch_handler_called = launch_handler_called.clone();
|
||||
|
||||
client.on_request::<dap::requests::Launch, _>(move |_, args| {
|
||||
launch_handler_called.store(true, Ordering::SeqCst);
|
||||
|
||||
let obj = args.raw.as_object().unwrap();
|
||||
let sent_definition = serde_json::from_value::<DebugTaskDefinition>(
|
||||
obj.get(&"raw_request".to_owned()).unwrap().clone(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(sent_definition, debug_definition);
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
assert!(
|
||||
launch_handler_called.load(Ordering::SeqCst),
|
||||
"Launch request handler was not called"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_active_debug_line_setting(executor: BackgroundExecutor, cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(executor.clone());
|
||||
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
json!({
|
||||
"main.rs": "First line\nSecond line\nThird line\nFourth line",
|
||||
"second.rs": "First line\nSecond line\nThird line\nFourth line",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
let workspace = init_test_workspace(&project, cx).await;
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
let project_path = Path::new(path!("/project"));
|
||||
let worktree = project
|
||||
.update(cx, |project, cx| project.find_worktree(project_path, cx))
|
||||
.expect("This worktree should exist in project")
|
||||
.0;
|
||||
|
||||
let worktree_id = workspace
|
||||
.update(cx, |_, _, cx| worktree.read(cx).id())
|
||||
.unwrap();
|
||||
|
||||
let main_buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_buffer((worktree_id, "main.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let second_buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_buffer((worktree_id, "second.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (main_editor, cx) = cx.add_window_view(|window, cx| {
|
||||
Editor::new(
|
||||
EditorMode::full(),
|
||||
MultiBuffer::build_from_buffer(main_buffer, cx),
|
||||
Some(project.clone()),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let (second_editor, cx) = cx.add_window_view(|window, cx| {
|
||||
Editor::new(
|
||||
EditorMode::full(),
|
||||
MultiBuffer::build_from_buffer(second_buffer, cx),
|
||||
Some(project.clone()),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
|
||||
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
|
||||
|
||||
client.on_request::<dap::requests::Threads, _>(move |_, _| {
|
||||
Ok(dap::ThreadsResponse {
|
||||
threads: vec![dap::Thread {
|
||||
id: 1,
|
||||
name: "Thread 1".into(),
|
||||
}],
|
||||
})
|
||||
});
|
||||
|
||||
client.on_request::<dap::requests::Scopes, _>(move |_, _| {
|
||||
Ok(dap::ScopesResponse {
|
||||
scopes: Vec::default(),
|
||||
})
|
||||
});
|
||||
|
||||
client.on_request::<StackTrace, _>(move |_, args| {
|
||||
assert_eq!(args.thread_id, 1);
|
||||
|
||||
Ok(dap::StackTraceResponse {
|
||||
stack_frames: vec![dap::StackFrame {
|
||||
id: 1,
|
||||
name: "frame 1".into(),
|
||||
source: Some(dap::Source {
|
||||
name: Some("main.rs".into()),
|
||||
path: Some(path!("/project/main.rs").into()),
|
||||
source_reference: None,
|
||||
presentation_hint: None,
|
||||
origin: None,
|
||||
sources: None,
|
||||
adapter_data: None,
|
||||
checksums: None,
|
||||
}),
|
||||
line: 2,
|
||||
column: 0,
|
||||
end_line: None,
|
||||
end_column: None,
|
||||
can_restart: None,
|
||||
instruction_pointer_reference: None,
|
||||
module_id: None,
|
||||
presentation_hint: None,
|
||||
}],
|
||||
total_frames: None,
|
||||
})
|
||||
});
|
||||
|
||||
client
|
||||
.fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
|
||||
reason: dap::StoppedEventReason::Breakpoint,
|
||||
description: None,
|
||||
thread_id: Some(1),
|
||||
preserve_focus_hint: None,
|
||||
text: None,
|
||||
all_threads_stopped: None,
|
||||
hit_breakpoint_ids: None,
|
||||
}))
|
||||
.await;
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
main_editor.update_in(cx, |editor, window, cx| {
|
||||
let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
|
||||
|
||||
assert_eq!(
|
||||
active_debug_lines.len(),
|
||||
1,
|
||||
"There should be only one active debug line"
|
||||
);
|
||||
|
||||
let point = editor
|
||||
.snapshot(window, cx)
|
||||
.buffer_snapshot
|
||||
.summary_for_anchor::<language::Point>(&active_debug_lines.first().unwrap().0.start);
|
||||
|
||||
assert_eq!(point.row, 1);
|
||||
});
|
||||
|
||||
second_editor.update(cx, |editor, _| {
|
||||
let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
|
||||
|
||||
assert!(
|
||||
active_debug_lines.is_empty(),
|
||||
"There shouldn't be any active debug lines"
|
||||
);
|
||||
});
|
||||
|
||||
let handled_second_stacktrace = Arc::new(AtomicBool::new(false));
|
||||
client.on_request::<StackTrace, _>({
|
||||
let handled_second_stacktrace = handled_second_stacktrace.clone();
|
||||
move |_, args| {
|
||||
handled_second_stacktrace.store(true, Ordering::SeqCst);
|
||||
assert_eq!(args.thread_id, 1);
|
||||
|
||||
Ok(dap::StackTraceResponse {
|
||||
stack_frames: vec![dap::StackFrame {
|
||||
id: 2,
|
||||
name: "frame 2".into(),
|
||||
source: Some(dap::Source {
|
||||
name: Some("second.rs".into()),
|
||||
path: Some(path!("/project/second.rs").into()),
|
||||
source_reference: None,
|
||||
presentation_hint: None,
|
||||
origin: None,
|
||||
sources: None,
|
||||
adapter_data: None,
|
||||
checksums: None,
|
||||
}),
|
||||
line: 3,
|
||||
column: 0,
|
||||
end_line: None,
|
||||
end_column: None,
|
||||
can_restart: None,
|
||||
instruction_pointer_reference: None,
|
||||
module_id: None,
|
||||
presentation_hint: None,
|
||||
}],
|
||||
total_frames: None,
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
client
|
||||
.fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
|
||||
reason: dap::StoppedEventReason::Breakpoint,
|
||||
description: None,
|
||||
thread_id: Some(1),
|
||||
preserve_focus_hint: None,
|
||||
text: None,
|
||||
all_threads_stopped: None,
|
||||
hit_breakpoint_ids: None,
|
||||
}))
|
||||
.await;
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
second_editor.update_in(cx, |editor, window, cx| {
|
||||
let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
|
||||
|
||||
assert_eq!(
|
||||
active_debug_lines.len(),
|
||||
1,
|
||||
"There should be only one active debug line"
|
||||
);
|
||||
|
||||
let point = editor
|
||||
.snapshot(window, cx)
|
||||
.buffer_snapshot
|
||||
.summary_for_anchor::<language::Point>(&active_debug_lines.first().unwrap().0.start);
|
||||
|
||||
assert_eq!(point.row, 2);
|
||||
});
|
||||
|
||||
main_editor.update(cx, |editor, _| {
|
||||
let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
|
||||
|
||||
assert!(
|
||||
active_debug_lines.is_empty(),
|
||||
"There shouldn't be any active debug lines"
|
||||
);
|
||||
});
|
||||
|
||||
assert!(
|
||||
handled_second_stacktrace.load(Ordering::SeqCst),
|
||||
"Second stacktrace request handler was not called"
|
||||
);
|
||||
|
||||
// Clean up
|
||||
let shutdown_session = project.update(cx, |project, cx| {
|
||||
project.dap_store().update(cx, |dap_store, cx| {
|
||||
dap_store.shutdown_session(session.read(cx).session_id(), cx)
|
||||
})
|
||||
});
|
||||
|
||||
shutdown_session.await.unwrap();
|
||||
|
||||
main_editor.update(cx, |editor, _| {
|
||||
let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
|
||||
|
||||
assert!(
|
||||
active_debug_lines.is_empty(),
|
||||
"There shouldn't be any active debug lines after session shutdown"
|
||||
);
|
||||
});
|
||||
|
||||
second_editor.update(cx, |editor, _| {
|
||||
let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
|
||||
|
||||
assert!(
|
||||
active_debug_lines.is_empty(),
|
||||
"There shouldn't be any active debug lines after session shutdown"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, AtomicI32, Ordering},
|
||||
};
|
||||
use util::path;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext) {
|
||||
@@ -20,7 +19,7 @@ async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext)
|
||||
|
||||
let fs = FakeFs::new(executor.clone());
|
||||
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
let project = Project::test(fs, ["/project".as_ref()], cx).await;
|
||||
let workspace = init_test_workspace(&project, cx).await;
|
||||
workspace
|
||||
.update(cx, |workspace, window, cx| {
|
||||
@@ -106,7 +105,10 @@ async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext)
|
||||
let running_state =
|
||||
active_debug_session_panel(workspace, cx).update_in(cx, |item, window, cx| {
|
||||
cx.focus_self(window);
|
||||
item.running_state().clone()
|
||||
item.mode()
|
||||
.as_running()
|
||||
.expect("Session should be running by this point")
|
||||
.clone()
|
||||
});
|
||||
|
||||
running_state.update_in(cx, |this, window, cx| {
|
||||
|
||||
@@ -138,33 +138,43 @@ async fn test_fetch_initial_stack_frames_and_go_to_stack_frame(
|
||||
|
||||
// trigger to load threads
|
||||
active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
|
||||
session.running_state().update(cx, |running_state, cx| {
|
||||
running_state
|
||||
.session()
|
||||
.update(cx, |session, cx| session.threads(cx));
|
||||
});
|
||||
session
|
||||
.mode()
|
||||
.as_running()
|
||||
.unwrap()
|
||||
.update(cx, |running_state, cx| {
|
||||
running_state
|
||||
.session()
|
||||
.update(cx, |session, cx| session.threads(cx));
|
||||
});
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
// select first thread
|
||||
active_debug_session_panel(workspace, cx).update_in(cx, |session, window, cx| {
|
||||
session.running_state().update(cx, |running_state, cx| {
|
||||
running_state.select_current_thread(
|
||||
&running_state
|
||||
.session()
|
||||
.update(cx, |session, cx| session.threads(cx)),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
session
|
||||
.mode()
|
||||
.as_running()
|
||||
.unwrap()
|
||||
.update(cx, |running_state, cx| {
|
||||
running_state.select_current_thread(
|
||||
&running_state
|
||||
.session()
|
||||
.update(cx, |session, cx| session.threads(cx)),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
|
||||
let stack_frame_list = session
|
||||
.running_state()
|
||||
.mode()
|
||||
.as_running()
|
||||
.unwrap()
|
||||
.update(cx, |state, _| state.stack_frame_list().clone());
|
||||
|
||||
stack_frame_list.update(cx, |stack_frame_list, cx| {
|
||||
@@ -299,26 +309,34 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC
|
||||
|
||||
// trigger threads to load
|
||||
active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
|
||||
session.running_state().update(cx, |running_state, cx| {
|
||||
running_state
|
||||
.session()
|
||||
.update(cx, |session, cx| session.threads(cx));
|
||||
});
|
||||
session
|
||||
.mode()
|
||||
.as_running()
|
||||
.unwrap()
|
||||
.update(cx, |running_state, cx| {
|
||||
running_state
|
||||
.session()
|
||||
.update(cx, |session, cx| session.threads(cx));
|
||||
});
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
// select first thread
|
||||
active_debug_session_panel(workspace, cx).update_in(cx, |session, window, cx| {
|
||||
session.running_state().update(cx, |running_state, cx| {
|
||||
running_state.select_current_thread(
|
||||
&running_state
|
||||
.session()
|
||||
.update(cx, |session, cx| session.threads(cx)),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
session
|
||||
.mode()
|
||||
.as_running()
|
||||
.unwrap()
|
||||
.update(cx, |running_state, cx| {
|
||||
running_state.select_current_thread(
|
||||
&running_state
|
||||
.session()
|
||||
.update(cx, |session, cx| session.threads(cx)),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
@@ -344,7 +362,7 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
|
||||
editor
|
||||
.highlighted_rows::<editor::ActiveDebugLine>()
|
||||
.highlighted_rows::<editor::DebugCurrentRowHighlight>()
|
||||
.map(|(range, _)| {
|
||||
let start = range.start.to_point(&snapshot.buffer_snapshot);
|
||||
let end = range.end.to_point(&snapshot.buffer_snapshot);
|
||||
@@ -365,7 +383,9 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC
|
||||
|
||||
active_debug_panel_item
|
||||
.read(cx)
|
||||
.running_state()
|
||||
.mode()
|
||||
.as_running()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.stack_frame_list()
|
||||
.clone()
|
||||
@@ -412,7 +432,7 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
|
||||
editor
|
||||
.highlighted_rows::<editor::ActiveDebugLine>()
|
||||
.highlighted_rows::<editor::DebugCurrentRowHighlight>()
|
||||
.map(|(range, _)| {
|
||||
let start = range.start.to_point(&snapshot.buffer_snapshot);
|
||||
let end = range.end.to_point(&snapshot.buffer_snapshot);
|
||||
@@ -656,26 +676,34 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo
|
||||
|
||||
// trigger threads to load
|
||||
active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
|
||||
session.running_state().update(cx, |running_state, cx| {
|
||||
running_state
|
||||
.session()
|
||||
.update(cx, |session, cx| session.threads(cx));
|
||||
});
|
||||
session
|
||||
.mode()
|
||||
.as_running()
|
||||
.unwrap()
|
||||
.update(cx, |running_state, cx| {
|
||||
running_state
|
||||
.session()
|
||||
.update(cx, |session, cx| session.threads(cx));
|
||||
});
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
// select first thread
|
||||
active_debug_session_panel(workspace, cx).update_in(cx, |session, window, cx| {
|
||||
session.running_state().update(cx, |running_state, cx| {
|
||||
running_state.select_current_thread(
|
||||
&running_state
|
||||
.session()
|
||||
.update(cx, |session, cx| session.threads(cx)),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
session
|
||||
.mode()
|
||||
.as_running()
|
||||
.unwrap()
|
||||
.update(cx, |running_state, cx| {
|
||||
running_state.select_current_thread(
|
||||
&running_state
|
||||
.session()
|
||||
.update(cx, |session, cx| session.threads(cx)),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
@@ -683,7 +711,9 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo
|
||||
// trigger stack frames to loaded
|
||||
active_debug_session_panel(workspace, cx).update(cx, |debug_panel_item, cx| {
|
||||
let stack_frame_list = debug_panel_item
|
||||
.running_state()
|
||||
.mode()
|
||||
.as_running()
|
||||
.unwrap()
|
||||
.update(cx, |state, _| state.stack_frame_list().clone());
|
||||
|
||||
stack_frame_list.update(cx, |stack_frame_list, cx| {
|
||||
@@ -695,7 +725,9 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo
|
||||
|
||||
active_debug_session_panel(workspace, cx).update_in(cx, |debug_panel_item, window, cx| {
|
||||
let stack_frame_list = debug_panel_item
|
||||
.running_state()
|
||||
.mode()
|
||||
.as_running()
|
||||
.unwrap()
|
||||
.update(cx, |state, _| state.stack_frame_list().clone());
|
||||
|
||||
stack_frame_list.update(cx, |stack_frame_list, cx| {
|
||||
|
||||
@@ -183,7 +183,10 @@ async fn test_basic_fetch_initial_scope_and_variables(
|
||||
let running_state =
|
||||
active_debug_session_panel(workspace, cx).update_in(cx, |item, window, cx| {
|
||||
cx.focus_self(window);
|
||||
item.running_state().clone()
|
||||
item.mode()
|
||||
.as_running()
|
||||
.expect("Session should be running by this point")
|
||||
.clone()
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
@@ -424,7 +427,10 @@ async fn test_fetch_variables_for_multiple_scopes(
|
||||
let running_state =
|
||||
active_debug_session_panel(workspace, cx).update_in(cx, |item, window, cx| {
|
||||
cx.focus_self(window);
|
||||
item.running_state().clone()
|
||||
item.mode()
|
||||
.as_running()
|
||||
.expect("Session should be running by this point")
|
||||
.clone()
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
@@ -704,7 +710,11 @@ async fn test_keyboard_navigation(executor: BackgroundExecutor, cx: &mut TestApp
|
||||
let running_state =
|
||||
active_debug_session_panel(workspace, cx).update_in(cx, |item, window, cx| {
|
||||
cx.focus_self(window);
|
||||
let running = item.running_state().clone();
|
||||
let running = item
|
||||
.mode()
|
||||
.as_running()
|
||||
.expect("Session should be running by this point")
|
||||
.clone();
|
||||
|
||||
let variable_list = running.read_with(cx, |state, _| state.variable_list().clone());
|
||||
variable_list.update(cx, |_, cx| cx.focus_self(window));
|
||||
@@ -1430,7 +1440,11 @@ async fn test_variable_list_only_sends_requests_when_rendering(
|
||||
cx.run_until_parked();
|
||||
|
||||
let running_state = active_debug_session_panel(workspace, cx).update_in(cx, |item, _, _| {
|
||||
let state = item.running_state().clone();
|
||||
let state = item
|
||||
.mode()
|
||||
.as_running()
|
||||
.expect("Session should be running by this point")
|
||||
.clone();
|
||||
|
||||
state
|
||||
});
|
||||
@@ -1728,7 +1742,10 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame(
|
||||
let running_state =
|
||||
active_debug_session_panel(workspace, cx).update_in(cx, |item, window, cx| {
|
||||
cx.focus_self(window);
|
||||
item.running_state().clone()
|
||||
item.mode()
|
||||
.as_running()
|
||||
.expect("Session should be running by this point")
|
||||
.clone()
|
||||
});
|
||||
|
||||
running_state.update(cx, |running_state, cx| {
|
||||
|
||||
@@ -3,11 +3,11 @@ use std::{ops::Range, sync::Arc};
|
||||
use editor::{
|
||||
Anchor, Editor, EditorSnapshot, ToOffset,
|
||||
display_map::{BlockContext, BlockPlacement, BlockProperties, BlockStyle},
|
||||
hover_popover::diagnostics_markdown_style,
|
||||
hover_markdown_style,
|
||||
scroll::Autoscroll,
|
||||
};
|
||||
use gpui::{AppContext, Entity, Focusable, WeakEntity};
|
||||
use language::{BufferId, Diagnostic, DiagnosticEntry};
|
||||
use language::{BufferId, DiagnosticEntry};
|
||||
use lsp::DiagnosticSeverity;
|
||||
use markdown::{Markdown, MarkdownElement};
|
||||
use settings::Settings;
|
||||
@@ -28,6 +28,7 @@ impl DiagnosticRenderer {
|
||||
diagnostic_group: Vec<DiagnosticEntry<Point>>,
|
||||
buffer_id: BufferId,
|
||||
diagnostics_editor: Option<WeakEntity<ProjectDiagnosticsEditor>>,
|
||||
merge_same_row: bool,
|
||||
cx: &mut App,
|
||||
) -> Vec<DiagnosticBlock> {
|
||||
let Some(primary_ix) = diagnostic_group
|
||||
@@ -37,87 +38,105 @@ impl DiagnosticRenderer {
|
||||
return Vec::new();
|
||||
};
|
||||
let primary = diagnostic_group[primary_ix].clone();
|
||||
let mut same_row = Vec::new();
|
||||
let mut close = Vec::new();
|
||||
let mut distant = Vec::new();
|
||||
let group_id = primary.diagnostic.group_id;
|
||||
let mut results = vec![];
|
||||
for entry in diagnostic_group.iter() {
|
||||
for (ix, entry) in diagnostic_group.into_iter().enumerate() {
|
||||
if entry.diagnostic.is_primary {
|
||||
let mut markdown = Self::markdown(&entry.diagnostic);
|
||||
let diagnostic = &primary.diagnostic;
|
||||
if diagnostic.source.is_some() || diagnostic.code.is_some() {
|
||||
markdown.push_str(" (");
|
||||
}
|
||||
if let Some(source) = diagnostic.source.as_ref() {
|
||||
markdown.push_str(&Markdown::escape(&source));
|
||||
}
|
||||
if diagnostic.source.is_some() && diagnostic.code.is_some() {
|
||||
markdown.push(' ');
|
||||
}
|
||||
if let Some(code) = diagnostic.code.as_ref() {
|
||||
if let Some(description) = diagnostic.code_description.as_ref() {
|
||||
markdown.push('[');
|
||||
markdown.push_str(&Markdown::escape(&code.to_string()));
|
||||
markdown.push_str("](");
|
||||
markdown.push_str(&Markdown::escape(description.as_ref()));
|
||||
markdown.push(')');
|
||||
} else {
|
||||
markdown.push_str(&Markdown::escape(&code.to_string()));
|
||||
}
|
||||
}
|
||||
if diagnostic.source.is_some() || diagnostic.code.is_some() {
|
||||
markdown.push(')');
|
||||
}
|
||||
|
||||
for (ix, entry) in diagnostic_group.iter().enumerate() {
|
||||
if entry.range.start.row.abs_diff(primary.range.start.row) >= 5 {
|
||||
markdown.push_str("\n- hint: [");
|
||||
markdown.push_str(&Markdown::escape(&entry.diagnostic.message));
|
||||
markdown.push_str(&format!(
|
||||
"](file://#diagnostic-{buffer_id}-{group_id}-{ix})\n",
|
||||
))
|
||||
}
|
||||
}
|
||||
results.push(DiagnosticBlock {
|
||||
initial_range: primary.range.clone(),
|
||||
severity: primary.diagnostic.severity,
|
||||
diagnostics_editor: diagnostics_editor.clone(),
|
||||
markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if entry.range.start.row == primary.range.start.row && merge_same_row {
|
||||
same_row.push(entry)
|
||||
} else if entry.range.start.row.abs_diff(primary.range.start.row) < 5 {
|
||||
let markdown = Self::markdown(&entry.diagnostic);
|
||||
|
||||
results.push(DiagnosticBlock {
|
||||
initial_range: entry.range.clone(),
|
||||
severity: entry.diagnostic.severity,
|
||||
diagnostics_editor: diagnostics_editor.clone(),
|
||||
markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)),
|
||||
});
|
||||
close.push(entry)
|
||||
} else {
|
||||
let mut markdown = Self::markdown(&entry.diagnostic);
|
||||
markdown.push_str(&format!(
|
||||
" ([back](file://#diagnostic-{buffer_id}-{group_id}-{primary_ix}))"
|
||||
));
|
||||
|
||||
results.push(DiagnosticBlock {
|
||||
initial_range: entry.range.clone(),
|
||||
severity: entry.diagnostic.severity,
|
||||
diagnostics_editor: diagnostics_editor.clone(),
|
||||
markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)),
|
||||
});
|
||||
distant.push((ix, entry))
|
||||
}
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
fn markdown(diagnostic: &Diagnostic) -> String {
|
||||
let mut markdown = String::new();
|
||||
let diagnostic = &primary.diagnostic;
|
||||
markdown.push_str(&Markdown::escape(&diagnostic.message));
|
||||
for entry in same_row {
|
||||
markdown.push_str("\n- hint: ");
|
||||
markdown.push_str(&Markdown::escape(&entry.diagnostic.message))
|
||||
}
|
||||
if diagnostic.source.is_some() || diagnostic.code.is_some() {
|
||||
markdown.push_str(" (");
|
||||
}
|
||||
if let Some(source) = diagnostic.source.as_ref() {
|
||||
markdown.push_str(&Markdown::escape(&source));
|
||||
}
|
||||
if diagnostic.source.is_some() && diagnostic.code.is_some() {
|
||||
markdown.push(' ');
|
||||
}
|
||||
if let Some(code) = diagnostic.code.as_ref() {
|
||||
if let Some(description) = diagnostic.code_description.as_ref() {
|
||||
markdown.push('[');
|
||||
markdown.push_str(&Markdown::escape(&code.to_string()));
|
||||
markdown.push_str("](");
|
||||
markdown.push_str(&Markdown::escape(description.as_ref()));
|
||||
markdown.push(')');
|
||||
} else {
|
||||
markdown.push_str(&Markdown::escape(&code.to_string()));
|
||||
}
|
||||
}
|
||||
if diagnostic.source.is_some() || diagnostic.code.is_some() {
|
||||
markdown.push(')');
|
||||
}
|
||||
|
||||
if let Some(md) = &diagnostic.markdown {
|
||||
markdown.push_str(md);
|
||||
} else {
|
||||
markdown.push_str(&Markdown::escape(&diagnostic.message));
|
||||
};
|
||||
markdown
|
||||
for (ix, entry) in &distant {
|
||||
markdown.push_str("\n- hint: [");
|
||||
markdown.push_str(&Markdown::escape(&entry.diagnostic.message));
|
||||
markdown.push_str(&format!(
|
||||
"](file://#diagnostic-{buffer_id}-{group_id}-{ix})\n",
|
||||
))
|
||||
}
|
||||
|
||||
let mut results = vec![DiagnosticBlock {
|
||||
initial_range: primary.range,
|
||||
severity: primary.diagnostic.severity,
|
||||
diagnostics_editor: diagnostics_editor.clone(),
|
||||
markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)),
|
||||
}];
|
||||
|
||||
for entry in close {
|
||||
let markdown = if let Some(source) = entry.diagnostic.source.as_ref() {
|
||||
format!("{}: {}", source, entry.diagnostic.message)
|
||||
} else {
|
||||
entry.diagnostic.message
|
||||
};
|
||||
let markdown = Markdown::escape(&markdown).to_string();
|
||||
|
||||
results.push(DiagnosticBlock {
|
||||
initial_range: entry.range,
|
||||
severity: entry.diagnostic.severity,
|
||||
diagnostics_editor: diagnostics_editor.clone(),
|
||||
markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)),
|
||||
});
|
||||
}
|
||||
|
||||
for (_, entry) in distant {
|
||||
let markdown = if let Some(source) = entry.diagnostic.source.as_ref() {
|
||||
format!("{}: {}", source, entry.diagnostic.message)
|
||||
} else {
|
||||
entry.diagnostic.message
|
||||
};
|
||||
let mut markdown = Markdown::escape(&markdown).to_string();
|
||||
markdown.push_str(&format!(
|
||||
" ([back](file://#diagnostic-{buffer_id}-{group_id}-{primary_ix}))"
|
||||
));
|
||||
|
||||
results.push(DiagnosticBlock {
|
||||
initial_range: entry.range,
|
||||
severity: entry.diagnostic.severity,
|
||||
diagnostics_editor: diagnostics_editor.clone(),
|
||||
markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)),
|
||||
});
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +149,7 @@ impl editor::DiagnosticRenderer for DiagnosticRenderer {
|
||||
editor: WeakEntity<Editor>,
|
||||
cx: &mut App,
|
||||
) -> Vec<BlockProperties<Anchor>> {
|
||||
let blocks = Self::diagnostic_blocks_for_group(diagnostic_group, buffer_id, None, cx);
|
||||
let blocks = Self::diagnostic_blocks_for_group(diagnostic_group, buffer_id, None, true, cx);
|
||||
blocks
|
||||
.into_iter()
|
||||
.map(|block| {
|
||||
@@ -157,7 +176,8 @@ impl editor::DiagnosticRenderer for DiagnosticRenderer {
|
||||
buffer_id: BufferId,
|
||||
cx: &mut App,
|
||||
) -> Option<Entity<Markdown>> {
|
||||
let blocks = Self::diagnostic_blocks_for_group(diagnostic_group, buffer_id, None, cx);
|
||||
let blocks =
|
||||
Self::diagnostic_blocks_for_group(diagnostic_group, buffer_id, None, false, cx);
|
||||
blocks.into_iter().find_map(|block| {
|
||||
if block.initial_range == range {
|
||||
Some(block.markdown)
|
||||
@@ -191,7 +211,7 @@ impl DiagnosticBlock {
|
||||
let cx = &bcx.app;
|
||||
let status_colors = bcx.app.theme().status();
|
||||
|
||||
let max_width = bcx.em_width * 120.;
|
||||
let max_width = bcx.em_width * 100.;
|
||||
|
||||
let (background_color, border_color) = match self.severity {
|
||||
DiagnosticSeverity::ERROR => (status_colors.error_background, status_colors.error),
|
||||
@@ -215,19 +235,16 @@ impl DiagnosticBlock {
|
||||
.border_color(border_color)
|
||||
.max_w(max_width)
|
||||
.child(
|
||||
MarkdownElement::new(
|
||||
self.markdown.clone(),
|
||||
diagnostics_markdown_style(bcx.window, cx),
|
||||
)
|
||||
.on_url_click({
|
||||
move |link, window, cx| {
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
Self::open_link(editor, &diagnostics_editor, link, window, cx)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}),
|
||||
MarkdownElement::new(self.markdown.clone(), hover_markdown_style(bcx.window, cx))
|
||||
.on_url_click({
|
||||
move |link, window, cx| {
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
Self::open_link(editor, &diagnostics_editor, link, window, cx)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ use language::{
|
||||
Bias, Buffer, BufferRow, BufferSnapshot, DiagnosticEntry, Point, ToTreeSitterPoint,
|
||||
};
|
||||
use lsp::DiagnosticSeverity;
|
||||
|
||||
use project::{DiagnosticSummary, Project, ProjectPath, project_settings::ProjectSettings};
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
@@ -417,6 +416,7 @@ impl ProjectDiagnosticsEditor {
|
||||
group,
|
||||
buffer_snapshot.remote_id(),
|
||||
Some(this.clone()),
|
||||
true,
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
@@ -522,7 +522,7 @@ impl ProjectDiagnosticsEditor {
|
||||
markdown::MarkdownElement::rendered_text(
|
||||
markdown.clone(),
|
||||
cx,
|
||||
editor::hover_popover::diagnostics_markdown_style,
|
||||
editor::hover_markdown_style,
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
@@ -286,7 +286,7 @@ impl InlayId {
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ActiveDebugLine {}
|
||||
pub enum DebugCurrentRowHighlight {}
|
||||
enum DocumentHighlightRead {}
|
||||
enum DocumentHighlightWrite {}
|
||||
enum InputComposition {}
|
||||
@@ -871,6 +871,7 @@ pub struct Editor {
|
||||
show_breadcrumbs: bool,
|
||||
show_gutter: bool,
|
||||
show_scrollbars: bool,
|
||||
disable_scrolling: bool,
|
||||
disable_expand_excerpt_buttons: bool,
|
||||
show_line_numbers: Option<bool>,
|
||||
use_relative_line_numbers: Option<bool>,
|
||||
@@ -1580,11 +1581,7 @@ impl Editor {
|
||||
&project.read(cx).breakpoint_store(),
|
||||
window,
|
||||
|editor, _, event, window, cx| match event {
|
||||
BreakpointStoreEvent::ClearDebugLines => {
|
||||
editor.clear_row_highlights::<ActiveDebugLine>();
|
||||
editor.refresh_inline_values(cx);
|
||||
}
|
||||
BreakpointStoreEvent::SetDebugLine => {
|
||||
BreakpointStoreEvent::ActiveDebugLineChanged => {
|
||||
if editor.go_to_active_debug_line(window, cx) {
|
||||
cx.stop_propagation();
|
||||
}
|
||||
@@ -1667,6 +1664,7 @@ impl Editor {
|
||||
blink_manager: blink_manager.clone(),
|
||||
show_local_selections: true,
|
||||
show_scrollbars: true,
|
||||
disable_scrolling: false,
|
||||
mode,
|
||||
show_breadcrumbs: EditorSettings::get_global(cx).toolbar.breadcrumbs,
|
||||
show_gutter: mode.is_full(),
|
||||
@@ -2625,7 +2623,7 @@ impl Editor {
|
||||
}
|
||||
self.refresh_code_actions(window, cx);
|
||||
self.refresh_document_highlights(cx);
|
||||
self.refresh_selected_text_highlights(false, window, cx);
|
||||
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_indent_conflict = true;
|
||||
@@ -5819,12 +5817,7 @@ impl Editor {
|
||||
})
|
||||
}
|
||||
|
||||
fn refresh_selected_text_highlights(
|
||||
&mut self,
|
||||
on_buffer_edit: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
fn refresh_selected_text_highlights(&mut self, window: &mut Window, cx: &mut Context<Editor>) {
|
||||
let Some((query_text, query_range)) = self.prepare_highlight_query_from_selection(cx)
|
||||
else {
|
||||
self.clear_background_highlights::<SelectedTextHighlight>(cx);
|
||||
@@ -5833,13 +5826,12 @@ impl Editor {
|
||||
return;
|
||||
};
|
||||
let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
|
||||
if on_buffer_edit
|
||||
|| self
|
||||
.quick_selection_highlight_task
|
||||
.as_ref()
|
||||
.map_or(true, |(prev_anchor_range, _)| {
|
||||
prev_anchor_range != &query_range
|
||||
})
|
||||
if self
|
||||
.quick_selection_highlight_task
|
||||
.as_ref()
|
||||
.map_or(true, |(prev_anchor_range, _)| {
|
||||
prev_anchor_range != &query_range
|
||||
})
|
||||
{
|
||||
let multi_buffer_visible_start = self
|
||||
.scroll_manager
|
||||
@@ -5864,13 +5856,12 @@ impl Editor {
|
||||
),
|
||||
));
|
||||
}
|
||||
if on_buffer_edit
|
||||
|| self
|
||||
.debounced_selection_highlight_task
|
||||
.as_ref()
|
||||
.map_or(true, |(prev_anchor_range, _)| {
|
||||
prev_anchor_range != &query_range
|
||||
})
|
||||
if self
|
||||
.debounced_selection_highlight_task
|
||||
.as_ref()
|
||||
.map_or(true, |(prev_anchor_range, _)| {
|
||||
prev_anchor_range != &query_range
|
||||
})
|
||||
{
|
||||
let multi_buffer_start = multi_buffer_snapshot
|
||||
.anchor_before(0)
|
||||
@@ -16485,6 +16476,11 @@ impl Editor {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn disable_scrolling(&mut self, cx: &mut Context<Self>) {
|
||||
self.disable_scrolling = true;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_show_line_numbers(&mut self, show_line_numbers: bool, cx: &mut Context<Self>) {
|
||||
self.show_line_numbers = Some(show_line_numbers);
|
||||
cx.notify();
|
||||
@@ -16632,7 +16628,7 @@ impl Editor {
|
||||
|
||||
let Some(active_stack_frame) = breakpoint_store.read(cx).active_position().cloned()
|
||||
else {
|
||||
self.clear_row_highlights::<ActiveDebugLine>();
|
||||
self.clear_row_highlights::<DebugCurrentRowHighlight>();
|
||||
return None;
|
||||
};
|
||||
|
||||
@@ -16659,8 +16655,8 @@ impl Editor {
|
||||
let multibuffer_anchor = snapshot.anchor_in_excerpt(id, position)?;
|
||||
|
||||
handled = true;
|
||||
self.clear_row_highlights::<ActiveDebugLine>();
|
||||
self.go_to_line::<ActiveDebugLine>(
|
||||
self.clear_row_highlights::<DebugCurrentRowHighlight>();
|
||||
self.go_to_line::<DebugCurrentRowHighlight>(
|
||||
multibuffer_anchor,
|
||||
Some(cx.theme().colors().editor_debugger_active_line_background),
|
||||
window,
|
||||
@@ -17685,7 +17681,7 @@ impl Editor {
|
||||
|
||||
let current_execution_position = self
|
||||
.highlighted_rows
|
||||
.get(&TypeId::of::<ActiveDebugLine>())
|
||||
.get(&TypeId::of::<DebugCurrentRowHighlight>())
|
||||
.and_then(|lines| lines.last().map(|line| line.range.start));
|
||||
|
||||
self.inline_value_cache.refresh_task = cx.spawn(async move |editor, cx| {
|
||||
@@ -17753,8 +17749,6 @@ impl Editor {
|
||||
self.active_indent_guides_state.dirty = true;
|
||||
self.refresh_active_diagnostics(cx);
|
||||
self.refresh_code_actions(window, cx);
|
||||
self.refresh_selected_text_highlights(true, window, cx);
|
||||
refresh_matching_bracket_highlights(self, window, cx);
|
||||
if self.has_active_inline_completion() {
|
||||
self.update_visible_inline_completion(window, cx);
|
||||
}
|
||||
|
||||
@@ -5678,7 +5678,9 @@ impl EditorElement {
|
||||
}
|
||||
|
||||
fn paint_mouse_listeners(&mut self, layout: &EditorLayout, window: &mut Window, cx: &mut App) {
|
||||
self.paint_scroll_wheel_listener(layout, window, cx);
|
||||
if !self.editor.read(cx).disable_scrolling {
|
||||
self.paint_scroll_wheel_listener(layout, window, cx);
|
||||
}
|
||||
|
||||
window.on_mouse_event({
|
||||
let position_map = layout.position_map.clone();
|
||||
|
||||
@@ -506,7 +506,7 @@ impl GitBlame {
|
||||
} else {
|
||||
// If we weren't triggered by a user, we just log errors in the background, instead of sending
|
||||
// notifications.
|
||||
log::debug!("failed to get git blame data: {error:?}");
|
||||
log::error!("failed to get git blame data: {error:?}");
|
||||
}
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -655,59 +655,11 @@ pub fn hover_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
|
||||
},
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
selection_background_color: { cx.theme().players().local().selection },
|
||||
heading: StyleRefinement::default()
|
||||
.font_weight(FontWeight::BOLD)
|
||||
.text_base()
|
||||
.mt(rems(1.))
|
||||
.mb_0(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn diagnostics_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let ui_font_family = settings.ui_font.family.clone();
|
||||
let ui_font_fallbacks = settings.ui_font.fallbacks.clone();
|
||||
let buffer_font_family = settings.buffer_font.family.clone();
|
||||
let buffer_font_fallbacks = settings.buffer_font.fallbacks.clone();
|
||||
|
||||
let mut base_text_style = window.text_style();
|
||||
base_text_style.refine(&TextStyleRefinement {
|
||||
font_family: Some(ui_font_family.clone()),
|
||||
font_fallbacks: ui_font_fallbacks,
|
||||
color: Some(cx.theme().colors().editor_foreground),
|
||||
..Default::default()
|
||||
});
|
||||
MarkdownStyle {
|
||||
base_text_style,
|
||||
code_block: StyleRefinement::default().my(rems(1.)).font_buffer(cx),
|
||||
inline_code: TextStyleRefinement {
|
||||
background_color: Some(cx.theme().colors().editor_background.opacity(0.5)),
|
||||
font_family: Some(buffer_font_family),
|
||||
font_fallbacks: buffer_font_fallbacks,
|
||||
..Default::default()
|
||||
},
|
||||
rule_color: cx.theme().colors().border,
|
||||
block_quote_border_color: Color::Muted.color(cx),
|
||||
block_quote: TextStyleRefinement {
|
||||
color: Some(Color::Muted.color(cx)),
|
||||
..Default::default()
|
||||
},
|
||||
link: TextStyleRefinement {
|
||||
color: Some(cx.theme().colors().editor_foreground),
|
||||
underline: Some(gpui::UnderlineStyle {
|
||||
thickness: px(1.),
|
||||
color: Some(cx.theme().colors().editor_foreground),
|
||||
wavy: false,
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
selection_background_color: { cx.theme().players().local().selection },
|
||||
height_is_multiple_of_line_height: true,
|
||||
heading: StyleRefinement::default()
|
||||
.font_weight(FontWeight::BOLD)
|
||||
.text_base()
|
||||
.mt(rems(1.))
|
||||
.mb_0(),
|
||||
..Default::default()
|
||||
}
|
||||
@@ -999,7 +951,7 @@ impl DiagnosticPopover {
|
||||
.child(
|
||||
MarkdownElement::new(
|
||||
self.markdown.clone(),
|
||||
diagnostics_markdown_style(window, cx),
|
||||
hover_markdown_style(window, cx),
|
||||
)
|
||||
.on_url_click(move |link, window, cx| {
|
||||
if let Some(renderer) = GlobalDiagnosticRenderer::global(cx) {
|
||||
|
||||
@@ -1014,10 +1014,12 @@ impl SerializableItem for Editor {
|
||||
fn cleanup(
|
||||
workspace_id: WorkspaceId,
|
||||
alive_items: Vec<ItemId>,
|
||||
_window: &mut Window,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<()>> {
|
||||
workspace::delete_unloaded_items(alive_items, workspace_id, "editors", &DB, cx)
|
||||
window.spawn(cx, async move |_| {
|
||||
DB.delete_unloaded_items(workspace_id, alive_items).await
|
||||
})
|
||||
}
|
||||
|
||||
fn deserialize(
|
||||
|
||||
@@ -207,6 +207,14 @@ pub fn deploy_context_menu(
|
||||
ui::ContextMenu::build(window, cx, |menu, _window, _cx| {
|
||||
let builder = menu
|
||||
.on_blur_subscription(Subscription::new(|| {}))
|
||||
.action(
|
||||
"Show Code Actions",
|
||||
Box::new(ToggleCodeActions {
|
||||
deployed_from_indicator: None,
|
||||
quick_launch: false,
|
||||
}),
|
||||
)
|
||||
.separator()
|
||||
.when(evaluate_selection && has_selections, |builder| {
|
||||
builder
|
||||
.action("Evaluate Selection", Box::new(DebuggerEvaluateSelectedText))
|
||||
@@ -223,13 +231,6 @@ pub fn deploy_context_menu(
|
||||
.when(has_selections, |cx| {
|
||||
cx.action("Format Selections", Box::new(FormatSelections))
|
||||
})
|
||||
.action(
|
||||
"Show Code Actions",
|
||||
Box::new(ToggleCodeActions {
|
||||
deployed_from_indicator: None,
|
||||
quick_launch: false,
|
||||
}),
|
||||
)
|
||||
.separator()
|
||||
.action("Cut", Box::new(Cut))
|
||||
.action("Copy", Box::new(Copy))
|
||||
|
||||
@@ -373,6 +373,32 @@ VALUES {placeholders};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_unloaded_items(
|
||||
&self,
|
||||
workspace: WorkspaceId,
|
||||
alive_items: Vec<ItemId>,
|
||||
) -> Result<()> {
|
||||
let placeholders = alive_items
|
||||
.iter()
|
||||
.map(|_| "?")
|
||||
.collect::<Vec<&str>>()
|
||||
.join(", ");
|
||||
|
||||
let query = format!(
|
||||
"DELETE FROM editors WHERE workspace_id = ? AND item_id NOT IN ({placeholders})"
|
||||
);
|
||||
|
||||
self.write(move |conn| {
|
||||
let mut statement = Statement::prepare(conn, query)?;
|
||||
let mut next_index = statement.bind(&workspace, 1)?;
|
||||
for id in alive_items {
|
||||
next_index = statement.bind(&id, next_index)?;
|
||||
}
|
||||
statement.exec()
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -184,6 +184,9 @@ impl ScrollManager {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
if self.forbid_vertical_scroll {
|
||||
return;
|
||||
}
|
||||
let (new_anchor, top_row) = if scroll_position.y <= 0. {
|
||||
(
|
||||
ScrollAnchor {
|
||||
@@ -255,16 +258,10 @@ impl ScrollManager {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
let adjusted_anchor = if self.forbid_vertical_scroll {
|
||||
ScrollAnchor {
|
||||
offset: gpui::Point::new(anchor.offset.x, self.anchor.offset.y),
|
||||
anchor: self.anchor.anchor,
|
||||
}
|
||||
} else {
|
||||
anchor
|
||||
};
|
||||
|
||||
self.anchor = adjusted_anchor;
|
||||
if self.forbid_vertical_scroll {
|
||||
return;
|
||||
}
|
||||
self.anchor = anchor;
|
||||
cx.emit(EditorEvent::ScrollPositionChanged { local, autoscroll });
|
||||
self.show_scrollbars(window, cx);
|
||||
self.autoscroll_request.take();
|
||||
@@ -407,12 +404,11 @@ impl Editor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let mut delta = scroll_delta;
|
||||
if self.scroll_manager.forbid_vertical_scroll {
|
||||
delta.y = 0.0;
|
||||
return;
|
||||
}
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let position = self.scroll_manager.anchor.scroll_position(&display_map) + delta;
|
||||
let position = self.scroll_manager.anchor.scroll_position(&display_map) + scroll_delta;
|
||||
self.set_scroll_position_taking_display_map(position, true, false, display_map, window, cx);
|
||||
}
|
||||
|
||||
@@ -422,12 +418,10 @@ impl Editor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let mut position = scroll_position;
|
||||
if self.scroll_manager.forbid_vertical_scroll {
|
||||
let current_position = self.scroll_position(cx);
|
||||
position.y = current_position.y;
|
||||
return;
|
||||
}
|
||||
self.set_scroll_position_internal(position, true, false, window, cx);
|
||||
self.set_scroll_position_internal(scroll_position, true, false, window, cx);
|
||||
}
|
||||
|
||||
/// Scrolls so that `row` is at the top of the editor view.
|
||||
@@ -486,15 +480,8 @@ impl Editor {
|
||||
self.edit_prediction_preview
|
||||
.set_previous_scroll_position(None);
|
||||
|
||||
let adjusted_position = if self.scroll_manager.forbid_vertical_scroll {
|
||||
let current_position = self.scroll_manager.anchor.scroll_position(&display_map);
|
||||
gpui::Point::new(scroll_position.x, current_position.y)
|
||||
} else {
|
||||
scroll_position
|
||||
};
|
||||
|
||||
self.scroll_manager.set_scroll_position(
|
||||
adjusted_position,
|
||||
scroll_position,
|
||||
&display_map,
|
||||
local,
|
||||
autoscroll,
|
||||
|
||||
@@ -44,7 +44,6 @@ language_extension.workspace = true
|
||||
language_model.workspace = true
|
||||
language_models.workspace = true
|
||||
languages = { workspace = true, features = ["load-grammars"] }
|
||||
markdown.workspace = true
|
||||
node_runtime.workspace = true
|
||||
pathdiff.workspace = true
|
||||
paths.workspace = true
|
||||
|
||||
@@ -10,13 +10,13 @@ use crate::{
|
||||
ToolMetrics,
|
||||
assertions::{AssertionsReport, RanAssertion, RanAssertionResult},
|
||||
};
|
||||
use agent::{ContextLoadResult, Thread, ThreadEvent};
|
||||
use agent::{ContextLoadResult, ThreadEvent};
|
||||
use anyhow::{Result, anyhow};
|
||||
use async_trait::async_trait;
|
||||
use buffer_diff::DiffHunkStatus;
|
||||
use collections::HashMap;
|
||||
use futures::{FutureExt as _, StreamExt, channel::mpsc, select_biased};
|
||||
use gpui::{App, AppContext, AsyncApp, Entity};
|
||||
use gpui::{AppContext, AsyncApp, Entity};
|
||||
use language_model::{LanguageModel, Role, StopReason};
|
||||
|
||||
pub const THREAD_EVENT_TIMEOUT: Duration = Duration::from_secs(60 * 2);
|
||||
@@ -276,8 +276,7 @@ impl ExampleContext {
|
||||
| ThreadEvent::ReceivedTextChunk
|
||||
| ThreadEvent::StreamedToolUse { .. }
|
||||
| ThreadEvent::CheckpointChanged
|
||||
| ThreadEvent::UsageUpdated(_)
|
||||
| ThreadEvent::CancelEditing => {
|
||||
| ThreadEvent::UsageUpdated(_) => {
|
||||
tx.try_send(Ok(())).ok();
|
||||
if std::env::var("ZED_EVAL_DEBUG").is_ok() {
|
||||
println!("{}Event: {:#?}", log_prefix, event);
|
||||
@@ -314,7 +313,7 @@ impl ExampleContext {
|
||||
for message in thread.messages().skip(message_count_before) {
|
||||
messages.push(Message {
|
||||
_role: message.role,
|
||||
text: message.to_string(),
|
||||
_text: message.to_string(),
|
||||
tool_use: thread
|
||||
.tool_uses_for_message(message.id, cx)
|
||||
.into_iter()
|
||||
@@ -362,90 +361,6 @@ impl ExampleContext {
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn agent_thread(&self) -> Entity<Thread> {
|
||||
self.agent_thread.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl AppContext for ExampleContext {
|
||||
type Result<T> = anyhow::Result<T>;
|
||||
|
||||
fn new<T: 'static>(
|
||||
&mut self,
|
||||
build_entity: impl FnOnce(&mut gpui::Context<T>) -> T,
|
||||
) -> Self::Result<Entity<T>> {
|
||||
self.app.new(build_entity)
|
||||
}
|
||||
|
||||
fn reserve_entity<T: 'static>(&mut self) -> Self::Result<gpui::Reservation<T>> {
|
||||
self.app.reserve_entity()
|
||||
}
|
||||
|
||||
fn insert_entity<T: 'static>(
|
||||
&mut self,
|
||||
reservation: gpui::Reservation<T>,
|
||||
build_entity: impl FnOnce(&mut gpui::Context<T>) -> T,
|
||||
) -> Self::Result<Entity<T>> {
|
||||
self.app.insert_entity(reservation, build_entity)
|
||||
}
|
||||
|
||||
fn update_entity<T, R>(
|
||||
&mut self,
|
||||
handle: &Entity<T>,
|
||||
update: impl FnOnce(&mut T, &mut gpui::Context<T>) -> R,
|
||||
) -> Self::Result<R>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
self.app.update_entity(handle, update)
|
||||
}
|
||||
|
||||
fn read_entity<T, R>(
|
||||
&self,
|
||||
handle: &Entity<T>,
|
||||
read: impl FnOnce(&T, &App) -> R,
|
||||
) -> Self::Result<R>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
self.app.read_entity(handle, read)
|
||||
}
|
||||
|
||||
fn update_window<T, F>(&mut self, window: gpui::AnyWindowHandle, f: F) -> Result<T>
|
||||
where
|
||||
F: FnOnce(gpui::AnyView, &mut gpui::Window, &mut App) -> T,
|
||||
{
|
||||
self.app.update_window(window, f)
|
||||
}
|
||||
|
||||
fn read_window<T, R>(
|
||||
&self,
|
||||
window: &gpui::WindowHandle<T>,
|
||||
read: impl FnOnce(Entity<T>, &App) -> R,
|
||||
) -> Result<R>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
self.app.read_window(window, read)
|
||||
}
|
||||
|
||||
fn background_spawn<R>(
|
||||
&self,
|
||||
future: impl std::future::Future<Output = R> + Send + 'static,
|
||||
) -> gpui::Task<R>
|
||||
where
|
||||
R: Send + 'static,
|
||||
{
|
||||
self.app.background_spawn(future)
|
||||
}
|
||||
|
||||
fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> Self::Result<R>
|
||||
where
|
||||
G: gpui::Global,
|
||||
{
|
||||
self.app.read_global(callback)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -471,20 +386,15 @@ impl Response {
|
||||
cx.assert_some(result, format!("called `{}`", tool_name))
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn tool_uses(&self) -> impl Iterator<Item = &ToolUse> {
|
||||
self.messages.iter().flat_map(|msg| &msg.tool_use)
|
||||
}
|
||||
|
||||
pub fn texts(&self) -> impl Iterator<Item = String> {
|
||||
self.messages.iter().map(|message| message.text.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Message {
|
||||
_role: Role,
|
||||
text: String,
|
||||
_text: String,
|
||||
tool_use: Vec<ToolUse>,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::path::Path;
|
||||
use std::{collections::HashSet, path::Path};
|
||||
|
||||
use anyhow::Result;
|
||||
use assistant_tools::{CreateFileToolInput, EditFileToolInput, ReadFileToolInput};
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::example::{Example, ExampleContext, ExampleMetadata, JudgeAssertion, LanguageServer};
|
||||
@@ -31,7 +32,39 @@ impl Example for AddArgToTraitMethod {
|
||||
"#
|
||||
));
|
||||
|
||||
let _ = cx.run_to_end().await?;
|
||||
let response = cx.run_to_end().await?;
|
||||
|
||||
// Reads files before it edits them
|
||||
|
||||
let mut read_files = HashSet::new();
|
||||
|
||||
for tool_use in response.tool_uses() {
|
||||
match tool_use.name.as_str() {
|
||||
"read_file" => {
|
||||
if let Ok(input) = tool_use.parse_input::<ReadFileToolInput>() {
|
||||
read_files.insert(input.path);
|
||||
}
|
||||
}
|
||||
"create_file" => {
|
||||
if let Ok(input) = tool_use.parse_input::<CreateFileToolInput>() {
|
||||
read_files.insert(input.path);
|
||||
}
|
||||
}
|
||||
"edit_file" => {
|
||||
if let Ok(input) = tool_use.parse_input::<EditFileToolInput>() {
|
||||
cx.assert(
|
||||
read_files.contains(input.path.to_str().unwrap()),
|
||||
format!(
|
||||
"Read before edit: {}",
|
||||
&input.path.file_stem().unwrap().to_str().unwrap()
|
||||
),
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Adds ignored argument to all but `batch_tool`
|
||||
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use markdown::PathWithRange;
|
||||
|
||||
use crate::example::{Example, ExampleContext, ExampleMetadata, JudgeAssertion, LanguageServer};
|
||||
|
||||
pub struct CodeBlockCitations;
|
||||
|
||||
const FENCE: &str = "```";
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl Example for CodeBlockCitations {
|
||||
fn meta(&self) -> ExampleMetadata {
|
||||
ExampleMetadata {
|
||||
name: "code_block_citations".to_string(),
|
||||
url: "https://github.com/zed-industries/zed.git".to_string(),
|
||||
revision: "f69aeb6311dde3c0b8979c293d019d66498d54f2".to_string(),
|
||||
language_server: Some(LanguageServer {
|
||||
file_extension: "rs".to_string(),
|
||||
allow_preexisting_diagnostics: false,
|
||||
}),
|
||||
max_assertions: None,
|
||||
}
|
||||
}
|
||||
|
||||
async fn conversation(&self, cx: &mut ExampleContext) -> Result<()> {
|
||||
const FILENAME: &str = "assistant_tool.rs";
|
||||
cx.push_user_message(format!(
|
||||
r#"
|
||||
Show me the method bodies of all the methods of the `Tool` trait in {FILENAME}.
|
||||
|
||||
Please show each method in a separate code snippet.
|
||||
"#
|
||||
));
|
||||
|
||||
// Verify that the messages all have the correct formatting.
|
||||
let texts: Vec<String> = cx.run_to_end().await?.texts().collect();
|
||||
let closing_fence = format!("\n{FENCE}");
|
||||
|
||||
for text in texts.iter() {
|
||||
let mut text = text.as_str();
|
||||
|
||||
while let Some(index) = text.find(FENCE) {
|
||||
// Advance text past the opening backticks.
|
||||
text = &text[index + FENCE.len()..];
|
||||
|
||||
// Find the closing backticks.
|
||||
let content_len = text.find(&closing_fence);
|
||||
|
||||
// Verify the citation format - e.g. ```path/to/foo.txt#L123-456
|
||||
if let Some(citation_len) = text.find('\n') {
|
||||
let citation = &text[..citation_len];
|
||||
|
||||
if let Ok(()) =
|
||||
cx.assert(citation.contains("/"), format!("Slash in {citation:?}",))
|
||||
{
|
||||
let path_range = PathWithRange::new(citation);
|
||||
let path = cx
|
||||
.agent_thread()
|
||||
.update(cx, |thread, cx| {
|
||||
thread
|
||||
.project()
|
||||
.read(cx)
|
||||
.find_project_path(path_range.path, cx)
|
||||
})
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
if let Ok(path) = cx.assert_some(path, format!("Valid path: {citation:?}"))
|
||||
{
|
||||
let buffer_text = {
|
||||
let buffer = match cx.agent_thread().update(cx, |thread, cx| {
|
||||
thread
|
||||
.project()
|
||||
.update(cx, |project, cx| project.open_buffer(path, cx))
|
||||
}) {
|
||||
Ok(buffer_task) => buffer_task.await.ok(),
|
||||
Err(err) => {
|
||||
cx.assert(
|
||||
false,
|
||||
format!("Expected Ok(buffer), not {err:?}"),
|
||||
)
|
||||
.ok();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
let Ok(buffer_text) = cx.assert_some(
|
||||
buffer.and_then(|buffer| {
|
||||
buffer.read_with(cx, |buffer, _| buffer.text()).ok()
|
||||
}),
|
||||
"Reading buffer text succeeded",
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
buffer_text
|
||||
};
|
||||
|
||||
if let Some(content_len) = content_len {
|
||||
// + 1 because there's a newline character after the citation.
|
||||
let content =
|
||||
&text[(citation.len() + 1)..content_len - (citation.len() + 1)];
|
||||
|
||||
// deindent (trim the start of each line) because sometimes the model
|
||||
// chooses to deindent its code snippets for the sake of readability,
|
||||
// which in markdown is not only reasonable but usually desirable.
|
||||
cx.assert(
|
||||
deindent(&buffer_text)
|
||||
.trim()
|
||||
.contains(deindent(&content).trim()),
|
||||
"Code block content was found in file",
|
||||
)
|
||||
.ok();
|
||||
|
||||
if let Some(range) = path_range.range {
|
||||
let start_line_index = range.start.line.saturating_sub(1);
|
||||
let line_count =
|
||||
range.end.line.saturating_sub(start_line_index);
|
||||
let mut snippet = buffer_text
|
||||
.lines()
|
||||
.skip(start_line_index as usize)
|
||||
.take(line_count as usize)
|
||||
.collect::<Vec<&str>>()
|
||||
.join("\n");
|
||||
|
||||
if let Some(start_col) = range.start.col {
|
||||
snippet = snippet[start_col as usize..].to_string();
|
||||
}
|
||||
|
||||
if let Some(end_col) = range.end.col {
|
||||
let last_line = snippet.lines().last().unwrap();
|
||||
snippet = snippet
|
||||
[..snippet.len() - last_line.len() + end_col as usize]
|
||||
.to_string();
|
||||
}
|
||||
|
||||
// deindent (trim the start of each line) because sometimes the model
|
||||
// chooses to deindent its code snippets for the sake of readability,
|
||||
// which in markdown is not only reasonable but usually desirable.
|
||||
cx.assert_eq(
|
||||
deindent(snippet.as_str()).trim(),
|
||||
deindent(content).trim(),
|
||||
format!(
|
||||
"Code block was at {:?}-{:?}",
|
||||
range.start, range.end
|
||||
),
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cx.assert(
|
||||
false,
|
||||
format!("Opening {FENCE} did not have a newline anywhere after it."),
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
|
||||
if let Some(content_len) = content_len {
|
||||
// Advance past the closing backticks
|
||||
text = &text[content_len + FENCE.len()..];
|
||||
} else {
|
||||
// There were no closing backticks associated with these opening backticks.
|
||||
cx.assert(
|
||||
false,
|
||||
"Code block opening had matching closing backticks.".to_string(),
|
||||
)
|
||||
.ok();
|
||||
|
||||
// There are no more code blocks to parse, so we're done.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn thread_assertions(&self) -> Vec<JudgeAssertion> {
|
||||
vec![
|
||||
JudgeAssertion {
|
||||
id: "trait method bodies are shown".to_string(),
|
||||
description:
|
||||
"All method bodies of the Tool trait are shown."
|
||||
.to_string(),
|
||||
},
|
||||
JudgeAssertion {
|
||||
id: "code blocks used".to_string(),
|
||||
description:
|
||||
"All code snippets are rendered inside markdown code blocks (as opposed to any other formatting besides code blocks)."
|
||||
.to_string(),
|
||||
},
|
||||
JudgeAssertion {
|
||||
id: "code blocks use backticks".to_string(),
|
||||
description:
|
||||
format!("All markdown code blocks use backtick fences ({FENCE}) rather than indentation.")
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
fn deindent(as_str: impl AsRef<str>) -> String {
|
||||
as_str
|
||||
.as_ref()
|
||||
.lines()
|
||||
.map(|line| line.trim_start())
|
||||
.collect::<Vec<&str>>()
|
||||
.join("\n")
|
||||
}
|
||||
@@ -12,14 +12,12 @@ use util::serde::default_true;
|
||||
use crate::example::{Example, ExampleContext, ExampleMetadata, JudgeAssertion};
|
||||
|
||||
mod add_arg_to_trait_method;
|
||||
mod code_block_citations;
|
||||
mod file_search;
|
||||
|
||||
pub fn all(examples_dir: &Path) -> Vec<Rc<dyn Example>> {
|
||||
let mut threads: Vec<Rc<dyn Example>> = vec![
|
||||
Rc::new(file_search::FileSearchExample),
|
||||
Rc::new(add_arg_to_trait_method::AddArgToTraitMethod),
|
||||
Rc::new(code_block_citations::CodeBlockCitations),
|
||||
];
|
||||
|
||||
for example_path in list_declarative_examples(examples_dir).unwrap() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use agent::{Message, MessageSegment, ThreadStore};
|
||||
use agent::ThreadStore;
|
||||
use anyhow::{Context, Result, anyhow, bail};
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use client::proto::LspWorkProgress;
|
||||
@@ -60,7 +60,7 @@ pub struct RunOutput {
|
||||
pub response_count: usize,
|
||||
pub token_usage: TokenUsage,
|
||||
pub tool_metrics: ToolMetrics,
|
||||
pub all_messages: String,
|
||||
pub last_request: LanguageModelRequest,
|
||||
pub programmatic_assertions: AssertionsReport,
|
||||
}
|
||||
|
||||
@@ -309,15 +309,19 @@ impl ExampleInstance {
|
||||
let thread_store = thread_store.await?;
|
||||
let thread =
|
||||
thread_store.update(cx, |thread_store, cx| thread_store.create_thread(cx))?;
|
||||
let last_request = Rc::new(RefCell::new(None));
|
||||
|
||||
thread.update(cx, |thread, _cx| {
|
||||
let mut request_count = 0;
|
||||
let last_request = Rc::clone(&last_request);
|
||||
let previous_diff = Rc::new(RefCell::new("".to_string()));
|
||||
let example_output_dir = this.run_directory.clone();
|
||||
let last_diff_file_path = last_diff_file_path.clone();
|
||||
let messages_json_file_path = example_output_dir.join("last.messages.json");
|
||||
let this = this.clone();
|
||||
thread.set_request_callback(move |request, response_events| {
|
||||
*last_request.borrow_mut() = Some(request.clone());
|
||||
|
||||
request_count += 1;
|
||||
let messages_file_path = example_output_dir.join(format!("{request_count}.messages.md"));
|
||||
let diff_file_path = example_output_dir.join(format!("{request_count}.diff"));
|
||||
@@ -393,6 +397,10 @@ impl ExampleInstance {
|
||||
|
||||
}
|
||||
|
||||
let Some(last_request) = last_request.borrow_mut().take() else {
|
||||
return Err(anyhow!("No requests ran."));
|
||||
};
|
||||
|
||||
if let Some(diagnostics_before) = &diagnostics_before {
|
||||
fs::write(this.run_directory.join("diagnostics_before.txt"), diagnostics_before)?;
|
||||
}
|
||||
@@ -415,7 +423,7 @@ impl ExampleInstance {
|
||||
response_count,
|
||||
token_usage: thread.cumulative_token_usage(),
|
||||
tool_metrics: example_cx.tool_metrics.lock().unwrap().clone(),
|
||||
all_messages: messages_to_markdown(thread.messages()),
|
||||
last_request,
|
||||
programmatic_assertions: example_cx.assertions,
|
||||
}
|
||||
})
|
||||
@@ -518,23 +526,23 @@ impl ExampleInstance {
|
||||
|
||||
if thread_assertions.is_empty() {
|
||||
return (
|
||||
"No thread assertions".to_string(),
|
||||
"No diff assertions".to_string(),
|
||||
AssertionsReport::default(),
|
||||
);
|
||||
}
|
||||
|
||||
let judge_thread_prompt = include_str!("judge_thread_prompt.hbs");
|
||||
let judge_thread_prompt_name = "judge_thread_prompt";
|
||||
let judge_diff_prompt_name = "judge_thread_prompt";
|
||||
let mut hbs = Handlebars::new();
|
||||
hbs.register_template_string(judge_thread_prompt_name, judge_thread_prompt)
|
||||
hbs.register_template_string(judge_diff_prompt_name, judge_thread_prompt)
|
||||
.unwrap();
|
||||
|
||||
let complete_messages = &run_output.all_messages;
|
||||
let request_markdown = RequestMarkdown::new(&run_output.last_request);
|
||||
let to_prompt = |assertion: String| {
|
||||
hbs.render(
|
||||
judge_thread_prompt_name,
|
||||
judge_diff_prompt_name,
|
||||
&JudgeThreadInput {
|
||||
messages: complete_messages.clone(),
|
||||
messages: request_markdown.messages.clone(),
|
||||
assertion,
|
||||
},
|
||||
)
|
||||
@@ -809,51 +817,6 @@ pub async fn run_git(repo_path: &Path, args: &[&str]) -> Result<String> {
|
||||
}
|
||||
}
|
||||
|
||||
fn messages_to_markdown<'a>(message_iter: impl IntoIterator<Item = &'a Message>) -> String {
|
||||
let mut messages = String::new();
|
||||
let mut assistant_message_number: u32 = 1;
|
||||
|
||||
for message in message_iter {
|
||||
push_role(&message.role, &mut messages, &mut assistant_message_number);
|
||||
|
||||
for segment in &message.segments {
|
||||
match segment {
|
||||
MessageSegment::Text(text) => {
|
||||
messages.push_str(&text);
|
||||
messages.push_str("\n\n");
|
||||
}
|
||||
MessageSegment::Thinking { text, signature } => {
|
||||
messages.push_str("**Thinking**:\n\n");
|
||||
if let Some(sig) = signature {
|
||||
messages.push_str(&format!("Signature: {}\n\n", sig));
|
||||
}
|
||||
messages.push_str(&text);
|
||||
messages.push_str("\n");
|
||||
}
|
||||
MessageSegment::RedactedThinking(items) => {
|
||||
messages.push_str(&format!(
|
||||
"**Redacted Thinking**: {} item(s)\n\n",
|
||||
items.len()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
messages
|
||||
}
|
||||
|
||||
fn push_role(role: &Role, buf: &mut String, assistant_message_number: &mut u32) {
|
||||
match role {
|
||||
Role::System => buf.push_str("# ⚙️ SYSTEM\n\n"),
|
||||
Role::User => buf.push_str("# 👤 USER\n\n"),
|
||||
Role::Assistant => {
|
||||
buf.push_str(&format!("# 🤖 ASSISTANT {assistant_message_number}\n\n"));
|
||||
*assistant_message_number = *assistant_message_number + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_language_model_request(
|
||||
model: Arc<dyn LanguageModel>,
|
||||
request: LanguageModelRequest,
|
||||
@@ -912,7 +875,14 @@ impl RequestMarkdown {
|
||||
|
||||
// Print the messages
|
||||
for message in &request.messages {
|
||||
push_role(&message.role, &mut messages, &mut assistant_message_number);
|
||||
match message.role {
|
||||
Role::System => messages.push_str("# ⚙️ SYSTEM\n\n"),
|
||||
Role::User => messages.push_str("# 👤 USER\n\n"),
|
||||
Role::Assistant => {
|
||||
messages.push_str(&format!("# 🤖 ASSISTANT {assistant_message_number}\n\n"));
|
||||
assistant_message_number += 1;
|
||||
}
|
||||
};
|
||||
|
||||
for content in &message.content {
|
||||
match content {
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
[package]
|
||||
name = "zed_extension_api"
|
||||
version = "0.5.0"
|
||||
version = "0.4.0"
|
||||
description = "APIs for creating Zed extensions in Rust"
|
||||
repository = "https://github.com/zed-industries/zed"
|
||||
documentation = "https://docs.rs/zed_extension_api"
|
||||
keywords = ["zed", "extension"]
|
||||
edition.workspace = true
|
||||
# Change back to `true` when we're ready to publish v0.5.0.
|
||||
publish = false
|
||||
publish = true
|
||||
license = "Apache-2.0"
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -218,7 +218,7 @@ mod wit {
|
||||
|
||||
wit_bindgen::generate!({
|
||||
skip: ["init-extension"],
|
||||
path: "./wit/since_v0.5.0",
|
||||
path: "./wit/since_v0.4.0",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
interface common {
|
||||
/// A (half-open) range (`[start, end)`).
|
||||
record range {
|
||||
/// The start of the range (inclusive).
|
||||
start: u32,
|
||||
/// The end of the range (exclusive).
|
||||
end: u32,
|
||||
}
|
||||
|
||||
/// A list of environment variables.
|
||||
type env-vars = list<tuple<string, string>>;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user