mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
Compare commits
57 commits
b16e1d8eb7
...
5ce2b267fc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ce2b267fc | ||
|
|
09165c15dc | ||
|
|
e2e7a6769e | ||
|
|
5d3b9e467e | ||
|
|
06826ef10f | ||
|
|
7f4a99aa95 | ||
|
|
122619624d | ||
|
|
2ea99a81f1 | ||
|
|
c30d18b10d | ||
|
|
18051ab399 | ||
|
|
654a864b3a | ||
|
|
5fba9b0cba | ||
|
|
906bff792c | ||
|
|
c029cc4354 | ||
|
|
0bafd1938c | ||
|
|
b7b1d1a2c7 | ||
|
|
a1d2ef6514 | ||
|
|
81f818aa86 | ||
|
|
f49be143f3 | ||
|
|
3eab273b85 | ||
|
|
85f8bf7393 | ||
|
|
da87558cc2 | ||
|
|
6bca2136a1 | ||
|
|
ef5da3ccc2 | ||
|
|
a3669a29e6 | ||
|
|
3d9852ae04 | ||
|
|
46845bf2f5 | ||
|
|
e4b81180c1 | ||
|
|
aeb5d6d7ff | ||
|
|
486e5f7cdd | ||
|
|
d3070bbc0d | ||
|
|
e5f5767d2c | ||
|
|
12aacf3cea | ||
|
|
b32570d931 | ||
|
|
59d8766f35 | ||
|
|
27c566c212 | ||
|
|
2bba4e2220 | ||
|
|
ec64ba3e69 | ||
|
|
b3d93d4474 | ||
|
|
a6b0ee9f36 | ||
|
|
3bb2f2a61d | ||
|
|
24fd1015f0 | ||
|
|
ef5606bb61 | ||
|
|
5abe4bcbc6 | ||
|
|
d139a871db | ||
|
|
830414004a | ||
|
|
bc64e1f955 | ||
|
|
d74e47ea51 | ||
|
|
f0ed342c19 | ||
|
|
92b0efeee0 | ||
|
|
4d6a3c7e11 | ||
|
|
b8c853a63d | ||
|
|
8042408df4 | ||
|
|
518502e5ef | ||
|
|
6049ceaecf | ||
|
|
6726b15fce | ||
|
|
5eaab414fc |
219 changed files with 12277 additions and 9996 deletions
2
.github/CODEOWNERS.hold
vendored
2
.github/CODEOWNERS.hold
vendored
|
|
@ -55,7 +55,6 @@
|
||||||
/crates/open_ai/ @zed-industries/ai-team
|
/crates/open_ai/ @zed-industries/ai-team
|
||||||
/crates/open_router/ @zed-industries/ai-team
|
/crates/open_router/ @zed-industries/ai-team
|
||||||
/crates/prompt_store/ @zed-industries/ai-team
|
/crates/prompt_store/ @zed-industries/ai-team
|
||||||
/crates/rules_library/ @zed-industries/ai-team
|
|
||||||
# SUGGESTED: Review needed - based on Richard Feldman (2 commits)
|
# SUGGESTED: Review needed - based on Richard Feldman (2 commits)
|
||||||
/crates/shell_command_parser/ @zed-industries/ai-team
|
/crates/shell_command_parser/ @zed-industries/ai-team
|
||||||
/crates/vercel/ @zed-industries/ai-team
|
/crates/vercel/ @zed-industries/ai-team
|
||||||
|
|
@ -181,7 +180,6 @@
|
||||||
/crates/fs_benchmarks/ @zed-industries/infrastructure-team
|
/crates/fs_benchmarks/ @zed-industries/infrastructure-team
|
||||||
/crates/http_client/ @zed-industries/infrastructure-team
|
/crates/http_client/ @zed-industries/infrastructure-team
|
||||||
/crates/http_client_tls/ @zed-industries/infrastructure-team
|
/crates/http_client_tls/ @zed-industries/infrastructure-team
|
||||||
/crates/nc/ @zed-industries/infrastructure-team
|
|
||||||
/crates/net/ @zed-industries/infrastructure-team
|
/crates/net/ @zed-industries/infrastructure-team
|
||||||
/crates/paths/ @zed-industries/infrastructure-team
|
/crates/paths/ @zed-industries/infrastructure-team
|
||||||
/crates/release_channel/ @zed-industries/infrastructure-team
|
/crates/release_channel/ @zed-industries/infrastructure-team
|
||||||
|
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
name: Community Champion Auto Labeler
|
|
||||||
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: [opened]
|
|
||||||
pull_request_target:
|
|
||||||
types: [opened]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
label_community_champion:
|
|
||||||
if: github.repository_owner == 'zed-industries'
|
|
||||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
|
||||||
steps:
|
|
||||||
- name: Check if author is a community champion and apply label
|
|
||||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
|
|
||||||
env:
|
|
||||||
COMMUNITY_CHAMPIONS: |
|
|
||||||
0x2CA
|
|
||||||
5brian
|
|
||||||
5herlocked
|
|
||||||
abdelq
|
|
||||||
afgomez
|
|
||||||
AidanV
|
|
||||||
akbxr
|
|
||||||
AlvaroParker
|
|
||||||
amtoaer
|
|
||||||
artemevsevev
|
|
||||||
bajrangCoder
|
|
||||||
bcomnes
|
|
||||||
Be-ing
|
|
||||||
blopker
|
|
||||||
bnjjj
|
|
||||||
bobbymannino
|
|
||||||
CharlesChen0823
|
|
||||||
chbk
|
|
||||||
davewa
|
|
||||||
davidbarsky
|
|
||||||
ddoemonn
|
|
||||||
djsauble
|
|
||||||
errmayank
|
|
||||||
fantacell
|
|
||||||
fdncred
|
|
||||||
findrakecil
|
|
||||||
FloppyDisco
|
|
||||||
gko
|
|
||||||
huacnlee
|
|
||||||
imumesh18
|
|
||||||
injust
|
|
||||||
jacobtread
|
|
||||||
jansol
|
|
||||||
jeffreyguenther
|
|
||||||
jenslys
|
|
||||||
jongretar
|
|
||||||
lemorage
|
|
||||||
lingyaochu
|
|
||||||
lnay
|
|
||||||
marcocondrache
|
|
||||||
marius851000
|
|
||||||
mikebronner
|
|
||||||
ognevny
|
|
||||||
PKief
|
|
||||||
playdohface
|
|
||||||
RemcoSmitsDev
|
|
||||||
rgbkrk
|
|
||||||
romaninsh
|
|
||||||
rxptr
|
|
||||||
Simek
|
|
||||||
someone13574
|
|
||||||
sourcefrog
|
|
||||||
suxiaoshao
|
|
||||||
Takk8IS
|
|
||||||
tartarughina
|
|
||||||
thedadams
|
|
||||||
tidely
|
|
||||||
timvermeulen
|
|
||||||
valentinegb
|
|
||||||
versecafe
|
|
||||||
vitallium
|
|
||||||
WhySoBad
|
|
||||||
ya7010
|
|
||||||
Zertsov
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const communityChampions = process.env.COMMUNITY_CHAMPIONS
|
|
||||||
.split('\n')
|
|
||||||
.map(handle => handle.trim().toLowerCase())
|
|
||||||
.filter(handle => handle.length > 0);
|
|
||||||
|
|
||||||
let author;
|
|
||||||
if (context.eventName === 'issues') {
|
|
||||||
author = context.payload.issue.user.login;
|
|
||||||
} else if (context.eventName === 'pull_request_target') {
|
|
||||||
author = context.payload.pull_request.user.login;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!author || !communityChampions.includes(author.toLowerCase())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const issueNumber = context.payload.issue?.number || context.payload.pull_request?.number;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await github.rest.issues.addLabels({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: issueNumber,
|
|
||||||
labels: ['community champion']
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`Applied 'community champion' label to #${issueNumber} by ${author}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to apply label: ${error.message}`);
|
|
||||||
}
|
|
||||||
97
.github/workflows/nix_build.yml
vendored
Normal file
97
.github/workflows/nix_build.yml
vendored
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
# Generated from xtask::workflows::nix_build
|
||||||
|
# Rebuild with `cargo xtask workflows`.
|
||||||
|
name: nix_build
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
RUST_BACKTRACE: '1'
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types:
|
||||||
|
- labeled
|
||||||
|
- synchronize
|
||||||
|
jobs:
|
||||||
|
build_nix_linux_x86_64:
|
||||||
|
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') && ((github.event.action == 'labeled' && (github.event.label.name == 'run-nix' || github.event.label.name == 'run-bundling')) || (github.event.action == 'synchronize' && (contains(github.event.pull_request.labels.*.name, 'run-nix') || contains(github.event.pull_request.labels.*.name, 'run-bundling'))))
|
||||||
|
runs-on: namespace-profile-32x64-ubuntu-2004
|
||||||
|
env:
|
||||||
|
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||||
|
ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }}
|
||||||
|
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
||||||
|
GIT_LFS_SKIP_SMUDGE: '1'
|
||||||
|
steps:
|
||||||
|
- name: steps::checkout_repo
|
||||||
|
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
|
||||||
|
with:
|
||||||
|
clean: false
|
||||||
|
- name: steps::cache_nix_dependencies_namespace
|
||||||
|
uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9
|
||||||
|
with:
|
||||||
|
cache: nix
|
||||||
|
- name: nix_build::build_nix::install_nix
|
||||||
|
uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f
|
||||||
|
with:
|
||||||
|
github_access_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: nix_build::build_nix::cachix_action
|
||||||
|
uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad
|
||||||
|
with:
|
||||||
|
name: zed
|
||||||
|
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||||
|
cachixArgs: -v
|
||||||
|
pushFilter: -zed-editor-[0-9.]*
|
||||||
|
- name: nix_build::build_nix::build
|
||||||
|
run: nix build .#default -L --accept-flake-config
|
||||||
|
timeout-minutes: 60
|
||||||
|
continue-on-error: true
|
||||||
|
build_nix_mac_aarch64:
|
||||||
|
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') && ((github.event.action == 'labeled' && (github.event.label.name == 'run-nix' || github.event.label.name == 'run-bundling')) || (github.event.action == 'synchronize' && (contains(github.event.pull_request.labels.*.name, 'run-nix') || contains(github.event.pull_request.labels.*.name, 'run-bundling'))))
|
||||||
|
runs-on: namespace-profile-mac-large
|
||||||
|
env:
|
||||||
|
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||||
|
ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }}
|
||||||
|
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
||||||
|
GIT_LFS_SKIP_SMUDGE: '1'
|
||||||
|
steps:
|
||||||
|
- name: steps::checkout_repo
|
||||||
|
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
|
||||||
|
with:
|
||||||
|
clean: false
|
||||||
|
- name: steps::cache_nix_store_macos
|
||||||
|
uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9
|
||||||
|
with:
|
||||||
|
path: ~/nix-cache
|
||||||
|
- name: nix_build::build_nix::install_nix
|
||||||
|
uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f
|
||||||
|
with:
|
||||||
|
github_access_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: nix_build::build_nix::configure_local_nix_cache
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/nix-cache
|
||||||
|
echo "extra-substituters = file://$HOME/nix-cache?priority=10" | sudo tee -a /etc/nix/nix.conf
|
||||||
|
echo "require-sigs = false" | sudo tee -a /etc/nix/nix.conf
|
||||||
|
sudo launchctl kickstart -k system/org.nixos.nix-daemon
|
||||||
|
- name: nix_build::build_nix::cachix_action
|
||||||
|
uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad
|
||||||
|
with:
|
||||||
|
name: zed
|
||||||
|
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||||
|
cachixArgs: -v
|
||||||
|
pushFilter: -zed-editor-[0-9.]*
|
||||||
|
- name: nix_build::build_nix::build
|
||||||
|
run: nix build .#default -L --accept-flake-config
|
||||||
|
- name: nix_build::build_nix::export_to_local_nix_cache
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
if [ -L result ]; then
|
||||||
|
echo "Copying build closure to local binary cache..."
|
||||||
|
nix copy --to "file://$HOME/nix-cache" ./result || echo "Warning: nix copy to local cache failed"
|
||||||
|
else
|
||||||
|
echo "No build result found, skipping cache export."
|
||||||
|
fi
|
||||||
|
timeout-minutes: 60
|
||||||
|
continue-on-error: true
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash -euxo pipefail {0}
|
||||||
247
.github/workflows/pr_issue_labeler.yml
vendored
Normal file
247
.github/workflows/pr_issue_labeler.yml
vendored
Normal file
|
|
@ -0,0 +1,247 @@
|
||||||
|
# Labels pull requests by author:
|
||||||
|
# - 'community champion' for community champions
|
||||||
|
# - 'bot' for bot accounts
|
||||||
|
# - 'staff' for staff team members
|
||||||
|
# - 'guild' for guild members
|
||||||
|
# - 'first contribution' for first-time external contributors
|
||||||
|
# Labels issues by author:
|
||||||
|
# - 'community champion' for community champions
|
||||||
|
|
||||||
|
name: PR Issue Labeler
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened]
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-authorship-and-label:
|
||||||
|
if: github.repository == 'zed-industries/zed'
|
||||||
|
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||||
|
timeout-minutes: 5
|
||||||
|
steps:
|
||||||
|
- id: get-app-token
|
||||||
|
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.ZED_COMMUNITY_BOT_APP_ID }}
|
||||||
|
private-key: ${{ secrets.ZED_COMMUNITY_BOT_PRIVATE_KEY }}
|
||||||
|
owner: zed-industries
|
||||||
|
|
||||||
|
- id: apply-authorship-label
|
||||||
|
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||||
|
with:
|
||||||
|
github-token: ${{ steps.get-app-token.outputs.token }}
|
||||||
|
script: |
|
||||||
|
const BOT_LABEL = 'bot';
|
||||||
|
const STAFF_LABEL = 'staff';
|
||||||
|
const STAFF_TEAM_SLUG = 'staff';
|
||||||
|
const FIRST_CONTRIBUTION_LABEL = 'first contribution';
|
||||||
|
const GUILD_LABEL = 'guild';
|
||||||
|
const GUILD_MEMBERS = [
|
||||||
|
'11happy',
|
||||||
|
'AidanV',
|
||||||
|
'alanpjohn',
|
||||||
|
'AmaanBilwar',
|
||||||
|
'arjunkomath',
|
||||||
|
'austincummings',
|
||||||
|
'ayushk-1801',
|
||||||
|
'criticic',
|
||||||
|
'dongdong867',
|
||||||
|
'emamulandalib',
|
||||||
|
'eureka928',
|
||||||
|
'feitreim',
|
||||||
|
'iam-liam',
|
||||||
|
'iksuddle',
|
||||||
|
'ishaksebsib',
|
||||||
|
'lingyaochu',
|
||||||
|
'loadingalias',
|
||||||
|
'marcocondrache',
|
||||||
|
'mchisolm0',
|
||||||
|
'MostlyKIGuess',
|
||||||
|
'nairadithya',
|
||||||
|
'nihalxkumar',
|
||||||
|
'notJoon',
|
||||||
|
'OmChillure',
|
||||||
|
'Palanikannan1437',
|
||||||
|
'polyesterswing',
|
||||||
|
'prayanshchh',
|
||||||
|
'razeghi71',
|
||||||
|
'sarmadgulzar',
|
||||||
|
'seanstrom',
|
||||||
|
'Shivansh-25',
|
||||||
|
'SkandaBhat',
|
||||||
|
'th0jensen',
|
||||||
|
'tommyming',
|
||||||
|
'transitoryangel',
|
||||||
|
'TwistingTwists',
|
||||||
|
'virajbhartiya',
|
||||||
|
'YEDASAVG',
|
||||||
|
'Ziqi-Yang',
|
||||||
|
];
|
||||||
|
const COMMUNITY_CHAMPION_LABEL = 'community champion';
|
||||||
|
const COMMUNITY_CHAMPIONS = [
|
||||||
|
'0x2CA',
|
||||||
|
'5brian',
|
||||||
|
'5herlocked',
|
||||||
|
'abdelq',
|
||||||
|
'afgomez',
|
||||||
|
'AidanV',
|
||||||
|
'akbxr',
|
||||||
|
'AlvaroParker',
|
||||||
|
'amtoaer',
|
||||||
|
'artemevsevev',
|
||||||
|
'bajrangCoder',
|
||||||
|
'bcomnes',
|
||||||
|
'Be-ing',
|
||||||
|
'blopker',
|
||||||
|
'bnjjj',
|
||||||
|
'bobbymannino',
|
||||||
|
'CharlesChen0823',
|
||||||
|
'chbk',
|
||||||
|
'davewa',
|
||||||
|
'davidbarsky',
|
||||||
|
'ddoemonn',
|
||||||
|
'djsauble',
|
||||||
|
'errmayank',
|
||||||
|
'fantacell',
|
||||||
|
'fdncred',
|
||||||
|
'findrakecil',
|
||||||
|
'FloppyDisco',
|
||||||
|
'gko',
|
||||||
|
'huacnlee',
|
||||||
|
'imumesh18',
|
||||||
|
'injust',
|
||||||
|
'jacobtread',
|
||||||
|
'jansol',
|
||||||
|
'jeffreyguenther',
|
||||||
|
'jenslys',
|
||||||
|
'jongretar',
|
||||||
|
'lemorage',
|
||||||
|
'lingyaochu',
|
||||||
|
'lnay',
|
||||||
|
'marcocondrache',
|
||||||
|
'marius851000',
|
||||||
|
'mikebronner',
|
||||||
|
'ognevny',
|
||||||
|
'PKief',
|
||||||
|
'playdohface',
|
||||||
|
'RemcoSmitsDev',
|
||||||
|
'rgbkrk',
|
||||||
|
'romaninsh',
|
||||||
|
'rxptr',
|
||||||
|
'Simek',
|
||||||
|
'someone13574',
|
||||||
|
'sourcefrog',
|
||||||
|
'suxiaoshao',
|
||||||
|
'Takk8IS',
|
||||||
|
'tartarughina',
|
||||||
|
'thedadams',
|
||||||
|
'tidely',
|
||||||
|
'timvermeulen',
|
||||||
|
'valentinegb',
|
||||||
|
'versecafe',
|
||||||
|
'vitallium',
|
||||||
|
'WhySoBad',
|
||||||
|
'ya7010',
|
||||||
|
'Zertsov',
|
||||||
|
];
|
||||||
|
|
||||||
|
const pr = context.payload.pull_request;
|
||||||
|
const issue = context.payload.issue;
|
||||||
|
const target = pr || issue;
|
||||||
|
const author = target.user.login;
|
||||||
|
|
||||||
|
const listIncludesAuthor = (members, author) => {
|
||||||
|
const authorLower = author.toLowerCase();
|
||||||
|
return members.some((member) => member.toLowerCase() === authorLower);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isStaffMember = async (author) => {
|
||||||
|
try {
|
||||||
|
const response = await github.rest.teams.getMembershipForUserInOrg({
|
||||||
|
org: 'zed-industries',
|
||||||
|
team_slug: STAFF_TEAM_SLUG,
|
||||||
|
username: author
|
||||||
|
});
|
||||||
|
return response.data.state === 'active';
|
||||||
|
} catch (error) {
|
||||||
|
if (error.status !== 404) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getIssueLabels = () => {
|
||||||
|
if (listIncludesAuthor(COMMUNITY_CHAMPIONS, author)) {
|
||||||
|
return [COMMUNITY_CHAMPION_LABEL];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPullRequestLabels = async () => {
|
||||||
|
if (target.user.type === 'Bot') {
|
||||||
|
return [BOT_LABEL];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await isStaffMember(author)) {
|
||||||
|
return [STAFF_LABEL];
|
||||||
|
}
|
||||||
|
|
||||||
|
// External contributors
|
||||||
|
|
||||||
|
const labelsToAdd = [];
|
||||||
|
|
||||||
|
if (listIncludesAuthor(COMMUNITY_CHAMPIONS, author)) {
|
||||||
|
labelsToAdd.push(COMMUNITY_CHAMPION_LABEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listIncludesAuthor(GUILD_MEMBERS, author)) {
|
||||||
|
labelsToAdd.push(GUILD_LABEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We use inverted logic here due to a suspected GitHub bug where first-time contributors
|
||||||
|
// get 'NONE' instead of 'FIRST_TIME_CONTRIBUTOR' or 'FIRST_TIMER'.
|
||||||
|
// https://github.com/orgs/community/discussions/78038
|
||||||
|
// This will break if GitHub ever adds new associations.
|
||||||
|
const association = pr.author_association;
|
||||||
|
const knownAssociations = ['CONTRIBUTOR', 'COLLABORATOR', 'MEMBER', 'OWNER', 'MANNEQUIN'];
|
||||||
|
|
||||||
|
if (knownAssociations.includes(association)) {
|
||||||
|
console.log(`PR #${pr.number} by ${author}: not a first-time contributor (association: '${association}')`);
|
||||||
|
} else {
|
||||||
|
labelsToAdd.push(FIRST_CONTRIBUTION_LABEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
return labelsToAdd;
|
||||||
|
};
|
||||||
|
|
||||||
|
const labelsToAdd = pr ? await getPullRequestLabels() : getIssueLabels();
|
||||||
|
|
||||||
|
if (labelsToAdd.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await github.rest.issues.addLabels({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: target.number,
|
||||||
|
labels: labelsToAdd
|
||||||
|
});
|
||||||
|
|
||||||
|
const targetType = pr ? 'PR' : 'issue';
|
||||||
|
const labels = labelsToAdd.map((label) => `'${label}'`).join(', ');
|
||||||
|
console.log(`${targetType} #${target.number} by ${author}: labeled ${labels}`);
|
||||||
|
} catch (error) {
|
||||||
|
if (pr) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(`Failed to label issue #${target.number}: ${error.message}`);
|
||||||
|
}
|
||||||
150
.github/workflows/pr_labeler.yml
vendored
150
.github/workflows/pr_labeler.yml
vendored
|
|
@ -1,150 +0,0 @@
|
||||||
# Labels pull requests by author: 'bot' for bot accounts, 'staff' for
|
|
||||||
# staff team members, 'guild' for guild members, 'first contribution' for
|
|
||||||
# first-time external contributors.
|
|
||||||
name: PR Labeler
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request_target:
|
|
||||||
types: [opened]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
check-authorship-and-label:
|
|
||||||
if: github.repository == 'zed-industries/zed'
|
|
||||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
|
||||||
timeout-minutes: 5
|
|
||||||
steps:
|
|
||||||
- id: get-app-token
|
|
||||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
|
|
||||||
with:
|
|
||||||
app-id: ${{ secrets.ZED_COMMUNITY_BOT_APP_ID }}
|
|
||||||
private-key: ${{ secrets.ZED_COMMUNITY_BOT_PRIVATE_KEY }}
|
|
||||||
owner: zed-industries
|
|
||||||
|
|
||||||
- id: apply-authorship-label
|
|
||||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
|
||||||
with:
|
|
||||||
github-token: ${{ steps.get-app-token.outputs.token }}
|
|
||||||
script: |
|
|
||||||
const BOT_LABEL = 'bot';
|
|
||||||
const STAFF_LABEL = 'staff';
|
|
||||||
const GUILD_LABEL = 'guild';
|
|
||||||
const FIRST_CONTRIBUTION_LABEL = 'first contribution';
|
|
||||||
const STAFF_TEAM_SLUG = 'staff';
|
|
||||||
const GUILD_MEMBERS = [
|
|
||||||
'11happy',
|
|
||||||
'AidanV',
|
|
||||||
'AmaanBilwar',
|
|
||||||
'MostlyKIGuess',
|
|
||||||
'OmChillure',
|
|
||||||
'Palanikannan1437',
|
|
||||||
'Shivansh-25',
|
|
||||||
'SkandaBhat',
|
|
||||||
'TwistingTwists',
|
|
||||||
'YEDASAVG',
|
|
||||||
'Ziqi-Yang',
|
|
||||||
'alanpjohn',
|
|
||||||
'arjunkomath',
|
|
||||||
'austincummings',
|
|
||||||
'ayushk-1801',
|
|
||||||
'criticic',
|
|
||||||
'dongdong867',
|
|
||||||
'emamulandalib',
|
|
||||||
'eureka928',
|
|
||||||
'feitreim',
|
|
||||||
'iam-liam',
|
|
||||||
'iksuddle',
|
|
||||||
'ishaksebsib',
|
|
||||||
'lingyaochu',
|
|
||||||
'loadingalias',
|
|
||||||
'marcocondrache',
|
|
||||||
'mchisolm0',
|
|
||||||
'nairadithya',
|
|
||||||
'nihalxkumar',
|
|
||||||
'notJoon',
|
|
||||||
'polyesterswing',
|
|
||||||
'prayanshchh',
|
|
||||||
'razeghi71',
|
|
||||||
'sarmadgulzar',
|
|
||||||
'seanstrom',
|
|
||||||
'th0jensen',
|
|
||||||
'tommyming',
|
|
||||||
'transitoryangel',
|
|
||||||
'virajbhartiya',
|
|
||||||
];
|
|
||||||
|
|
||||||
const pr = context.payload.pull_request;
|
|
||||||
const author = pr.user.login;
|
|
||||||
|
|
||||||
if (pr.user.type === 'Bot') {
|
|
||||||
await github.rest.issues.addLabels({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: pr.number,
|
|
||||||
labels: [BOT_LABEL]
|
|
||||||
});
|
|
||||||
console.log(`PR #${pr.number} by ${author}: labeled '${BOT_LABEL}' (user type: '${pr.user.type}')`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let isStaff = false;
|
|
||||||
try {
|
|
||||||
const response = await github.rest.teams.getMembershipForUserInOrg({
|
|
||||||
org: 'zed-industries',
|
|
||||||
team_slug: STAFF_TEAM_SLUG,
|
|
||||||
username: author
|
|
||||||
});
|
|
||||||
isStaff = response.data.state === 'active';
|
|
||||||
} catch (error) {
|
|
||||||
if (error.status !== 404) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isStaff) {
|
|
||||||
await github.rest.issues.addLabels({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: pr.number,
|
|
||||||
labels: [STAFF_LABEL]
|
|
||||||
});
|
|
||||||
console.log(`PR #${pr.number} by ${author}: labeled '${STAFF_LABEL}' (staff team member)`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const authorLower = author.toLowerCase();
|
|
||||||
const isGuildMember = GUILD_MEMBERS.some(
|
|
||||||
(member) => member.toLowerCase() === authorLower
|
|
||||||
);
|
|
||||||
if (isGuildMember) {
|
|
||||||
await github.rest.issues.addLabels({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: pr.number,
|
|
||||||
labels: [GUILD_LABEL]
|
|
||||||
});
|
|
||||||
console.log(`PR #${pr.number} by ${author}: labeled '${GUILD_LABEL}' (guild member)`);
|
|
||||||
// No early return: guild members can also get 'first contribution'
|
|
||||||
}
|
|
||||||
|
|
||||||
// We use inverted logic here due to a suspected GitHub bug where first-time contributors
|
|
||||||
// get 'NONE' instead of 'FIRST_TIME_CONTRIBUTOR' or 'FIRST_TIMER'.
|
|
||||||
// https://github.com/orgs/community/discussions/78038
|
|
||||||
// This will break if GitHub ever adds new associations.
|
|
||||||
const association = pr.author_association;
|
|
||||||
const knownAssociations = ['CONTRIBUTOR', 'COLLABORATOR', 'MEMBER', 'OWNER', 'MANNEQUIN'];
|
|
||||||
|
|
||||||
if (knownAssociations.includes(association)) {
|
|
||||||
console.log(`PR #${pr.number} by ${author}: not a first-time contributor (association: '${association}')`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await github.rest.issues.addLabels({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: pr.number,
|
|
||||||
labels: [FIRST_CONTRIBUTION_LABEL]
|
|
||||||
});
|
|
||||||
console.log(`PR #${pr.number} by ${author}: labeled '${FIRST_CONTRIBUTION_LABEL}' (association: '${association}')`);
|
|
||||||
79
.github/workflows/run_bundling.yml
vendored
79
.github/workflows/run_bundling.yml
vendored
|
|
@ -264,85 +264,6 @@ jobs:
|
||||||
path: target/zed-remote-server-windows-x86_64.zip
|
path: target/zed-remote-server-windows-x86_64.zip
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
build_nix_linux_x86_64:
|
|
||||||
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') && ((github.event.action == 'labeled' && github.event.label.name == 'run-bundling') || (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling')))
|
|
||||||
runs-on: namespace-profile-32x64-ubuntu-2004
|
|
||||||
env:
|
|
||||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
|
||||||
ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }}
|
|
||||||
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
|
||||||
GIT_LFS_SKIP_SMUDGE: '1'
|
|
||||||
steps:
|
|
||||||
- name: steps::checkout_repo
|
|
||||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
|
|
||||||
with:
|
|
||||||
clean: false
|
|
||||||
- name: steps::cache_nix_dependencies_namespace
|
|
||||||
uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9
|
|
||||||
with:
|
|
||||||
cache: nix
|
|
||||||
- name: nix_build::build_nix::install_nix
|
|
||||||
uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f
|
|
||||||
with:
|
|
||||||
github_access_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
- name: nix_build::build_nix::cachix_action
|
|
||||||
uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad
|
|
||||||
with:
|
|
||||||
name: zed
|
|
||||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
|
||||||
cachixArgs: -v
|
|
||||||
pushFilter: -zed-editor-[0-9.]*
|
|
||||||
- name: nix_build::build_nix::build
|
|
||||||
run: nix build .#default -L --accept-flake-config
|
|
||||||
timeout-minutes: 60
|
|
||||||
continue-on-error: true
|
|
||||||
build_nix_mac_aarch64:
|
|
||||||
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') && ((github.event.action == 'labeled' && github.event.label.name == 'run-bundling') || (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling')))
|
|
||||||
runs-on: namespace-profile-mac-large
|
|
||||||
env:
|
|
||||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
|
||||||
ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }}
|
|
||||||
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
|
||||||
GIT_LFS_SKIP_SMUDGE: '1'
|
|
||||||
steps:
|
|
||||||
- name: steps::checkout_repo
|
|
||||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
|
|
||||||
with:
|
|
||||||
clean: false
|
|
||||||
- name: steps::cache_nix_store_macos
|
|
||||||
uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9
|
|
||||||
with:
|
|
||||||
path: ~/nix-cache
|
|
||||||
- name: nix_build::build_nix::install_nix
|
|
||||||
uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f
|
|
||||||
with:
|
|
||||||
github_access_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
- name: nix_build::build_nix::configure_local_nix_cache
|
|
||||||
run: |
|
|
||||||
mkdir -p ~/nix-cache
|
|
||||||
echo "extra-substituters = file://$HOME/nix-cache?priority=10" | sudo tee -a /etc/nix/nix.conf
|
|
||||||
echo "require-sigs = false" | sudo tee -a /etc/nix/nix.conf
|
|
||||||
sudo launchctl kickstart -k system/org.nixos.nix-daemon
|
|
||||||
- name: nix_build::build_nix::cachix_action
|
|
||||||
uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad
|
|
||||||
with:
|
|
||||||
name: zed
|
|
||||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
|
||||||
cachixArgs: -v
|
|
||||||
pushFilter: -zed-editor-[0-9.]*
|
|
||||||
- name: nix_build::build_nix::build
|
|
||||||
run: nix build .#default -L --accept-flake-config
|
|
||||||
- name: nix_build::build_nix::export_to_local_nix_cache
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
if [ -L result ]; then
|
|
||||||
echo "Copying build closure to local binary cache..."
|
|
||||||
nix copy --to "file://$HOME/nix-cache" ./result || echo "Warning: nix copy to local cache failed"
|
|
||||||
else
|
|
||||||
echo "No build result found, skipping cache export."
|
|
||||||
fi
|
|
||||||
timeout-minutes: 60
|
|
||||||
continue-on-error: true
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
|
||||||
119
Cargo.lock
generated
119
Cargo.lock
generated
|
|
@ -109,7 +109,6 @@ dependencies = [
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"portable-pty",
|
"portable-pty",
|
||||||
"project",
|
"project",
|
||||||
"prompt_store",
|
|
||||||
"rand 0.9.4",
|
"rand 0.9.4",
|
||||||
"sandbox",
|
"sandbox",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
@ -234,6 +233,7 @@ dependencies = [
|
||||||
"agent_settings",
|
"agent_settings",
|
||||||
"agent_skills",
|
"agent_skills",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"assets",
|
||||||
"async-channel 2.5.0",
|
"async-channel 2.5.0",
|
||||||
"async-io",
|
"async-io",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|
@ -290,6 +290,7 @@ dependencies = [
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"text",
|
"text",
|
||||||
"theme",
|
"theme",
|
||||||
|
"theme_settings",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
"ui",
|
"ui",
|
||||||
"unindent",
|
"unindent",
|
||||||
|
|
@ -404,7 +405,7 @@ dependencies = [
|
||||||
"agent-client-protocol",
|
"agent-client-protocol",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"collections",
|
"collections",
|
||||||
"convert_case 0.8.0",
|
"convert_case 0.11.0",
|
||||||
"fs",
|
"fs",
|
||||||
"futures 0.3.32",
|
"futures 0.3.32",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
|
@ -426,6 +427,7 @@ name = "agent_skills"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"base64 0.22.1",
|
||||||
"const_format",
|
"const_format",
|
||||||
"fs",
|
"fs",
|
||||||
"futures 0.3.32",
|
"futures 0.3.32",
|
||||||
|
|
@ -434,6 +436,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_yaml_ng",
|
"serde_yaml_ng",
|
||||||
|
"url",
|
||||||
"util",
|
"util",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -512,7 +515,6 @@ dependencies = [
|
||||||
"remote_server",
|
"remote_server",
|
||||||
"reqwest_client",
|
"reqwest_client",
|
||||||
"rope",
|
"rope",
|
||||||
"rules_library",
|
|
||||||
"schemars 1.0.4",
|
"schemars 1.0.4",
|
||||||
"search",
|
"search",
|
||||||
"semver",
|
"semver",
|
||||||
|
|
@ -597,15 +599,14 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "alacritty_terminal"
|
name = "alacritty_terminal"
|
||||||
version = "0.25.1"
|
version = "0.26.1-dev"
|
||||||
source = "git+https://github.com/zed-industries/alacritty?rev=9d9640d4#9d9640d4e56d67a09d049f9c0a300aae08d4f61e"
|
source = "git+https://github.com/zed-industries/alacritty?rev=fcf32feacb367b75ec84dd40f041e4fd411d3cc1#fcf32feacb367b75ec84dd40f041e4fd411d3cc1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bitflags 2.10.0",
|
"bitflags 2.10.0",
|
||||||
"home",
|
"home",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"mach2 0.5.0",
|
|
||||||
"miow",
|
"miow",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"piper",
|
"piper",
|
||||||
|
|
@ -2161,7 +2162,7 @@ dependencies = [
|
||||||
"bitflags 2.10.0",
|
"bitflags 2.10.0",
|
||||||
"cexpr",
|
"cexpr",
|
||||||
"clang-sys",
|
"clang-sys",
|
||||||
"itertools 0.11.0",
|
"itertools 0.10.5",
|
||||||
"log",
|
"log",
|
||||||
"prettyplease",
|
"prettyplease",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
|
|
@ -2181,7 +2182,7 @@ dependencies = [
|
||||||
"bitflags 2.10.0",
|
"bitflags 2.10.0",
|
||||||
"cexpr",
|
"cexpr",
|
||||||
"clang-sys",
|
"clang-sys",
|
||||||
"itertools 0.11.0",
|
"itertools 0.10.5",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"regex",
|
"regex",
|
||||||
|
|
@ -3880,6 +3881,8 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"settings",
|
"settings",
|
||||||
|
"sqlez",
|
||||||
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -5309,7 +5312,7 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"option-ext",
|
"option-ext",
|
||||||
"redox_users 0.5.2",
|
"redox_users 0.5.2",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -5779,7 +5782,7 @@ dependencies = [
|
||||||
"client",
|
"client",
|
||||||
"clock",
|
"clock",
|
||||||
"collections",
|
"collections",
|
||||||
"convert_case 0.8.0",
|
"convert_case 0.11.0",
|
||||||
"criterion",
|
"criterion",
|
||||||
"ctor",
|
"ctor",
|
||||||
"dap",
|
"dap",
|
||||||
|
|
@ -6144,7 +6147,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -7600,7 +7603,7 @@ dependencies = [
|
||||||
"gobject-sys",
|
"gobject-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"system-deps",
|
"system-deps",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -9061,7 +9064,7 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"socket2 0.6.3",
|
"socket2 0.5.10",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|
@ -9079,7 +9082,7 @@ dependencies = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"windows-core 0.62.2",
|
"windows-core 0.56.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -9333,7 +9336,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
|
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown 0.16.1",
|
"hashbrown 0.15.5",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
@ -9746,6 +9749,7 @@ dependencies = [
|
||||||
"settings",
|
"settings",
|
||||||
"snippet_provider",
|
"snippet_provider",
|
||||||
"task",
|
"task",
|
||||||
|
"tempfile",
|
||||||
"theme",
|
"theme",
|
||||||
"util",
|
"util",
|
||||||
]
|
]
|
||||||
|
|
@ -10143,7 +10147,7 @@ dependencies = [
|
||||||
"cloud_api_types",
|
"cloud_api_types",
|
||||||
"collections",
|
"collections",
|
||||||
"component",
|
"component",
|
||||||
"convert_case 0.8.0",
|
"convert_case 0.11.0",
|
||||||
"copilot",
|
"copilot",
|
||||||
"copilot_chat",
|
"copilot_chat",
|
||||||
"copilot_ui",
|
"copilot_ui",
|
||||||
|
|
@ -10476,7 +10480,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libwebrtc"
|
name = "libwebrtc"
|
||||||
version = "0.3.26"
|
version = "0.3.26"
|
||||||
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=147fbca3d4b592d96d33f5e6a84b59fc0b5d9bf1#147fbca3d4b592d96d33f5e6a84b59fc0b5d9bf1"
|
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=c3a55bbc207008f1ca3474b6037fdd3c443cad0f#c3a55bbc207008f1ca3474b6037fdd3c443cad0f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cxx",
|
"cxx",
|
||||||
"glib",
|
"glib",
|
||||||
|
|
@ -10586,7 +10590,7 @@ checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "livekit"
|
name = "livekit"
|
||||||
version = "0.7.32"
|
version = "0.7.32"
|
||||||
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=147fbca3d4b592d96d33f5e6a84b59fc0b5d9bf1#147fbca3d4b592d96d33f5e6a84b59fc0b5d9bf1"
|
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=c3a55bbc207008f1ca3474b6037fdd3c443cad0f#c3a55bbc207008f1ca3474b6037fdd3c443cad0f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bmrng",
|
"bmrng",
|
||||||
|
|
@ -10612,7 +10616,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "livekit-api"
|
name = "livekit-api"
|
||||||
version = "0.4.14"
|
version = "0.4.14"
|
||||||
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=147fbca3d4b592d96d33f5e6a84b59fc0b5d9bf1#147fbca3d4b592d96d33f5e6a84b59fc0b5d9bf1"
|
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=c3a55bbc207008f1ca3474b6037fdd3c443cad0f#c3a55bbc207008f1ca3474b6037fdd3c443cad0f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.21.7",
|
"base64 0.21.7",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
|
@ -10639,7 +10643,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "livekit-protocol"
|
name = "livekit-protocol"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=147fbca3d4b592d96d33f5e6a84b59fc0b5d9bf1#147fbca3d4b592d96d33f5e6a84b59fc0b5d9bf1"
|
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=c3a55bbc207008f1ca3474b6037fdd3c443cad0f#c3a55bbc207008f1ca3474b6037fdd3c443cad0f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"livekit-runtime",
|
"livekit-runtime",
|
||||||
|
|
@ -10655,7 +10659,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "livekit-runtime"
|
name = "livekit-runtime"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=147fbca3d4b592d96d33f5e6a84b59fc0b5d9bf1#147fbca3d4b592d96d33f5e6a84b59fc0b5d9bf1"
|
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=c3a55bbc207008f1ca3474b6037fdd3c443cad0f#c3a55bbc207008f1ca3474b6037fdd3c443cad0f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
|
|
@ -11361,7 +11365,7 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"collections",
|
"collections",
|
||||||
"convert_case 0.8.0",
|
"convert_case 0.11.0",
|
||||||
"log",
|
"log",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|
@ -11722,16 +11726,6 @@ dependencies = [
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "nc"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"futures 0.3.32",
|
|
||||||
"net",
|
|
||||||
"smol",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ndk"
|
name = "ndk"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
|
@ -11957,7 +11951,7 @@ version = "0.50.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -14489,7 +14483,6 @@ dependencies = [
|
||||||
"db",
|
"db",
|
||||||
"fs",
|
"fs",
|
||||||
"futures 0.3.32",
|
"futures 0.3.32",
|
||||||
"fuzzy",
|
|
||||||
"gpui",
|
"gpui",
|
||||||
"handlebars 4.5.0",
|
"handlebars 4.5.0",
|
||||||
"heed",
|
"heed",
|
||||||
|
|
@ -14497,7 +14490,6 @@ dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"paths",
|
"paths",
|
||||||
"rope",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"strum 0.27.2",
|
"strum 0.27.2",
|
||||||
|
|
@ -14596,7 +14588,7 @@ checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes 1.11.1",
|
"bytes 1.11.1",
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"itertools 0.11.0",
|
"itertools 0.10.5",
|
||||||
"log",
|
"log",
|
||||||
"multimap",
|
"multimap",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
|
@ -14629,7 +14621,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1"
|
checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"itertools 0.11.0",
|
"itertools 0.10.5",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
|
|
@ -14891,7 +14883,7 @@ dependencies = [
|
||||||
"quinn-udp",
|
"quinn-udp",
|
||||||
"rustc-hash 2.1.1",
|
"rustc-hash 2.1.1",
|
||||||
"rustls 0.23.40",
|
"rustls 0.23.40",
|
||||||
"socket2 0.6.3",
|
"socket2 0.5.10",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|
@ -14928,9 +14920,9 @@ dependencies = [
|
||||||
"cfg_aliases 0.2.1",
|
"cfg_aliases 0.2.1",
|
||||||
"libc",
|
"libc",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"socket2 0.6.3",
|
"socket2 0.5.10",
|
||||||
"tracing",
|
"tracing",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -16014,33 +16006,6 @@ version = "0.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ad8388ea1a9e0ea807e442e8263a699e7edcb320ecbcd21b4fa8ff859acce3ba"
|
checksum = "ad8388ea1a9e0ea807e442e8263a699e7edcb320ecbcd21b4fa8ff859acce3ba"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rules_library"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"collections",
|
|
||||||
"editor",
|
|
||||||
"gpui",
|
|
||||||
"language",
|
|
||||||
"language_model",
|
|
||||||
"log",
|
|
||||||
"menu",
|
|
||||||
"picker",
|
|
||||||
"platform_title_bar",
|
|
||||||
"prompt_store",
|
|
||||||
"release_channel",
|
|
||||||
"rope",
|
|
||||||
"serde",
|
|
||||||
"settings",
|
|
||||||
"theme_settings",
|
|
||||||
"ui",
|
|
||||||
"ui_input",
|
|
||||||
"util",
|
|
||||||
"workspace",
|
|
||||||
"zed_actions",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "runtimelib"
|
name = "runtimelib"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
|
|
@ -16182,7 +16147,7 @@ dependencies = [
|
||||||
"errno 0.3.14",
|
"errno 0.3.14",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys 0.11.0",
|
"linux-raw-sys 0.11.0",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -17339,9 +17304,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook"
|
name = "signal-hook"
|
||||||
version = "0.3.18"
|
version = "0.4.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
|
checksum = "b2a0c28ca5908dbdbcd52e6fdaa00358ab88637f8ab33e1f188dd510eb44b53d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
|
|
@ -18804,7 +18769,7 @@ dependencies = [
|
||||||
"getrandom 0.3.4",
|
"getrandom 0.3.4",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix 1.1.2",
|
"rustix 1.1.2",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -19521,7 +19486,7 @@ name = "toolchain_selector"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"convert_case 0.8.0",
|
"convert_case 0.11.0",
|
||||||
"editor",
|
"editor",
|
||||||
"futures 0.3.32",
|
"futures 0.3.32",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
|
|
@ -19729,7 +19694,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2fb391ac70462b3097a755618fbf9c8f95ecc1eb379a414f7b46f202ed10db1f"
|
checksum = "2fb391ac70462b3097a755618fbf9c8f95ecc1eb379a414f7b46f202ed10db1f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"windows-targets 0.52.6",
|
"windows-targets 0.48.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -21522,7 +21487,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webrtc-sys"
|
name = "webrtc-sys"
|
||||||
version = "0.3.23"
|
version = "0.3.23"
|
||||||
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=147fbca3d4b592d96d33f5e6a84b59fc0b5d9bf1#147fbca3d4b592d96d33f5e6a84b59fc0b5d9bf1"
|
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=c3a55bbc207008f1ca3474b6037fdd3c443cad0f#c3a55bbc207008f1ca3474b6037fdd3c443cad0f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"cxx",
|
"cxx",
|
||||||
|
|
@ -21536,7 +21501,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webrtc-sys-build"
|
name = "webrtc-sys-build"
|
||||||
version = "0.3.13"
|
version = "0.3.13"
|
||||||
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=147fbca3d4b592d96d33f5e6a84b59fc0b5d9bf1#147fbca3d4b592d96d33f5e6a84b59fc0b5d9bf1"
|
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=c3a55bbc207008f1ca3474b6037fdd3c443cad0f#c3a55bbc207008f1ca3474b6037fdd3c443cad0f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"fs2",
|
"fs2",
|
||||||
|
|
@ -21834,7 +21799,7 @@ version = "0.1.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -23512,6 +23477,7 @@ dependencies = [
|
||||||
"agent-client-protocol",
|
"agent-client-protocol",
|
||||||
"agent_servers",
|
"agent_servers",
|
||||||
"agent_settings",
|
"agent_settings",
|
||||||
|
"agent_skills",
|
||||||
"agent_ui",
|
"agent_ui",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"ashpd",
|
"ashpd",
|
||||||
|
|
@ -23594,7 +23560,6 @@ dependencies = [
|
||||||
"migrator",
|
"migrator",
|
||||||
"mimalloc",
|
"mimalloc",
|
||||||
"miniprofiler_ui",
|
"miniprofiler_ui",
|
||||||
"nc",
|
|
||||||
"node_runtime",
|
"node_runtime",
|
||||||
"notifications",
|
"notifications",
|
||||||
"onboarding",
|
"onboarding",
|
||||||
|
|
|
||||||
14
Cargo.toml
14
Cargo.toml
|
|
@ -137,7 +137,6 @@ members = [
|
||||||
"crates/miniprofiler_ui",
|
"crates/miniprofiler_ui",
|
||||||
"crates/mistral",
|
"crates/mistral",
|
||||||
"crates/multi_buffer",
|
"crates/multi_buffer",
|
||||||
"crates/nc",
|
|
||||||
"crates/net",
|
"crates/net",
|
||||||
"crates/node_runtime",
|
"crates/node_runtime",
|
||||||
"crates/notifications",
|
"crates/notifications",
|
||||||
|
|
@ -172,7 +171,6 @@ members = [
|
||||||
"crates/reqwest_client",
|
"crates/reqwest_client",
|
||||||
"crates/rope",
|
"crates/rope",
|
||||||
"crates/rpc",
|
"crates/rpc",
|
||||||
"crates/rules_library",
|
|
||||||
"crates/sandbox",
|
"crates/sandbox",
|
||||||
"crates/skill_creator",
|
"crates/skill_creator",
|
||||||
"crates/scheduler",
|
"crates/scheduler",
|
||||||
|
|
@ -399,7 +397,6 @@ migrator = { path = "crates/migrator" }
|
||||||
mistral = { path = "crates/mistral" }
|
mistral = { path = "crates/mistral" }
|
||||||
multi_buffer = { path = "crates/multi_buffer" }
|
multi_buffer = { path = "crates/multi_buffer" }
|
||||||
miniprofiler_ui = { path = "crates/miniprofiler_ui" }
|
miniprofiler_ui = { path = "crates/miniprofiler_ui" }
|
||||||
nc = { path = "crates/nc" }
|
|
||||||
net = { path = "crates/net" }
|
net = { path = "crates/net" }
|
||||||
node_runtime = { path = "crates/node_runtime" }
|
node_runtime = { path = "crates/node_runtime" }
|
||||||
notifications = { path = "crates/notifications" }
|
notifications = { path = "crates/notifications" }
|
||||||
|
|
@ -434,7 +431,6 @@ reqwest_client = { path = "crates/reqwest_client" }
|
||||||
rodio = { git = "https://github.com/RustAudio/rodio", rev = "e50e726ddd0292f6ef9de0dda6b90af4ed1fb66a", features = ["wav", "playback", "wav_output", "recording"] }
|
rodio = { git = "https://github.com/RustAudio/rodio", rev = "e50e726ddd0292f6ef9de0dda6b90af4ed1fb66a", features = ["wav", "playback", "wav_output", "recording"] }
|
||||||
rope = { path = "crates/rope" }
|
rope = { path = "crates/rope" }
|
||||||
rpc = { path = "crates/rpc" }
|
rpc = { path = "crates/rpc" }
|
||||||
rules_library = { path = "crates/rules_library" }
|
|
||||||
skill_creator = { path = "crates/skill_creator" }
|
skill_creator = { path = "crates/skill_creator" }
|
||||||
scheduler = { path = "crates/scheduler" }
|
scheduler = { path = "crates/scheduler" }
|
||||||
sandbox = { path = "crates/sandbox" }
|
sandbox = { path = "crates/sandbox" }
|
||||||
|
|
@ -511,7 +507,7 @@ accesskit_unix = "0.21.0"
|
||||||
accesskit_windows = "0.32.1"
|
accesskit_windows = "0.32.1"
|
||||||
agent-client-protocol = { version = "=0.12.1", features = ["unstable"] }
|
agent-client-protocol = { version = "=0.12.1", features = ["unstable"] }
|
||||||
aho-corasick = "1.1"
|
aho-corasick = "1.1"
|
||||||
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty", rev = "9d9640d4" }
|
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty", rev = "fcf32feacb367b75ec84dd40f041e4fd411d3cc1" }
|
||||||
any_vec = "0.14"
|
any_vec = "0.14"
|
||||||
anyhow = "1.0.86"
|
anyhow = "1.0.86"
|
||||||
ashpd = { version = "0.13", default-features = false, features = [
|
ashpd = { version = "0.13", default-features = false, features = [
|
||||||
|
|
@ -563,7 +559,7 @@ clap = { version = "4.4", features = ["derive", "wrap_help"] }
|
||||||
cocoa = "=0.26.0"
|
cocoa = "=0.26.0"
|
||||||
cocoa-foundation = "=0.2.0"
|
cocoa-foundation = "=0.2.0"
|
||||||
const_format = "0.2"
|
const_format = "0.2"
|
||||||
convert_case = "0.8.0"
|
convert_case = "0.11.0"
|
||||||
core-foundation = "=0.10.0"
|
core-foundation = "=0.10.0"
|
||||||
core-foundation-sys = "0.8.6"
|
core-foundation-sys = "0.8.6"
|
||||||
core-video = { version = "0.5.2", features = ["metal"] }
|
core-video = { version = "0.5.2", features = ["metal"] }
|
||||||
|
|
@ -895,9 +891,9 @@ notify = { git = "https://github.com/zed-industries/notify.git", rev = "ce58c24c
|
||||||
notify-types = { git = "https://github.com/zed-industries/notify.git", rev = "ce58c24cad542c28e04ced02e20325a4ec28a31d" }
|
notify-types = { git = "https://github.com/zed-industries/notify.git", rev = "ce58c24cad542c28e04ced02e20325a4ec28a31d" }
|
||||||
windows-capture = { git = "https://github.com/zed-industries/windows-capture.git", rev = "f0d6c1b6691db75461b732f6d5ff56eed002eeb9" }
|
windows-capture = { git = "https://github.com/zed-industries/windows-capture.git", rev = "f0d6c1b6691db75461b732f6d5ff56eed002eeb9" }
|
||||||
calloop = { git = "https://github.com/zed-industries/calloop" }
|
calloop = { git = "https://github.com/zed-industries/calloop" }
|
||||||
livekit = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "147fbca3d4b592d96d33f5e6a84b59fc0b5d9bf1" }
|
livekit = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "c3a55bbc207008f1ca3474b6037fdd3c443cad0f" }
|
||||||
libwebrtc = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "147fbca3d4b592d96d33f5e6a84b59fc0b5d9bf1" }
|
libwebrtc = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "c3a55bbc207008f1ca3474b6037fdd3c443cad0f" }
|
||||||
webrtc-sys = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "147fbca3d4b592d96d33f5e6a84b59fc0b5d9bf1" }
|
webrtc-sys = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "c3a55bbc207008f1ca3474b6037fdd3c443cad0f" }
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
split-debuginfo = "unpacked"
|
split-debuginfo = "unpacked"
|
||||||
|
|
|
||||||
788
LICENSE-AGPL
788
LICENSE-AGPL
|
|
@ -1,788 +0,0 @@
|
||||||
Copyright 2022 - 2025 Zed Industries, Inc.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
|
||||||
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
|
|
||||||
You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
|
|
||||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
|
||||||
Version 3, 19 November 2007
|
|
||||||
|
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
|
||||||
of this license document, but changing it is not allowed.
|
|
||||||
|
|
||||||
|
|
||||||
Preamble
|
|
||||||
|
|
||||||
|
|
||||||
The GNU Affero General Public License is a free, copyleft license for
|
|
||||||
software and other kinds of works, specifically designed to ensure
|
|
||||||
cooperation with the community in the case of network server software.
|
|
||||||
|
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed
|
|
||||||
to take away your freedom to share and change the works. By contrast,
|
|
||||||
our General Public Licenses are intended to guarantee your freedom to
|
|
||||||
share and change all versions of a program--to make sure it remains free
|
|
||||||
software for all its users.
|
|
||||||
|
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
|
||||||
have the freedom to distribute copies of free software (and charge for
|
|
||||||
them if you wish), that you receive source code or can get it if you
|
|
||||||
want it, that you can change the software or use pieces of it in new
|
|
||||||
free programs, and that you know you can do these things.
|
|
||||||
|
|
||||||
|
|
||||||
Developers that use our General Public Licenses protect your rights
|
|
||||||
with two steps: (1) assert copyright on the software, and (2) offer
|
|
||||||
you this License which gives you legal permission to copy, distribute
|
|
||||||
and/or modify the software.
|
|
||||||
|
|
||||||
|
|
||||||
A secondary benefit of defending all users' freedom is that
|
|
||||||
improvements made in alternate versions of the program, if they
|
|
||||||
receive widespread use, become available for other developers to
|
|
||||||
incorporate. Many developers of free software are heartened and
|
|
||||||
encouraged by the resulting cooperation. However, in the case of
|
|
||||||
software used on network servers, this result may fail to come about.
|
|
||||||
The GNU General Public License permits making a modified version and
|
|
||||||
letting the public access it on a server without ever releasing its
|
|
||||||
source code to the public.
|
|
||||||
|
|
||||||
|
|
||||||
The GNU Affero General Public License is designed specifically to
|
|
||||||
ensure that, in such cases, the modified source code becomes available
|
|
||||||
to the community. It requires the operator of a network server to
|
|
||||||
provide the source code of the modified version running there to the
|
|
||||||
users of that server. Therefore, public use of a modified version, on
|
|
||||||
a publicly accessible server, gives the public access to the source
|
|
||||||
code of the modified version.
|
|
||||||
|
|
||||||
|
|
||||||
An older license, called the Affero General Public License and
|
|
||||||
published by Affero, was designed to accomplish similar goals. This is
|
|
||||||
a different license, not a version of the Affero GPL, but Affero has
|
|
||||||
released a new version of the Affero GPL which permits relicensing under
|
|
||||||
this license.
|
|
||||||
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
|
||||||
modification follow.
|
|
||||||
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
|
|
||||||
0. Definitions.
|
|
||||||
|
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
|
||||||
|
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
|
||||||
works, such as semiconductor masks.
|
|
||||||
|
|
||||||
|
|
||||||
"The Program" refers to any copyrightable work licensed under this
|
|
||||||
License. Each licensee is addressed as "you". "Licensees" and
|
|
||||||
"recipients" may be individuals or organizations.
|
|
||||||
|
|
||||||
|
|
||||||
To "modify" a work means to copy from or adapt all or part of the work
|
|
||||||
in a fashion requiring copyright permission, other than the making of an
|
|
||||||
exact copy. The resulting work is called a "modified version" of the
|
|
||||||
earlier work or a work "based on" the earlier work.
|
|
||||||
|
|
||||||
|
|
||||||
A "covered work" means either the unmodified Program or a work based
|
|
||||||
on the Program.
|
|
||||||
|
|
||||||
|
|
||||||
To "propagate" a work means to do anything with it that, without
|
|
||||||
permission, would make you directly or secondarily liable for
|
|
||||||
infringement under applicable copyright law, except executing it on a
|
|
||||||
computer or modifying a private copy. Propagation includes copying,
|
|
||||||
distribution (with or without modification), making available to the
|
|
||||||
public, and in some countries other activities as well.
|
|
||||||
|
|
||||||
|
|
||||||
To "convey" a work means any kind of propagation that enables other
|
|
||||||
parties to make or receive copies. Mere interaction with a user through
|
|
||||||
a computer network, with no transfer of a copy, is not conveying.
|
|
||||||
|
|
||||||
|
|
||||||
An interactive user interface displays "Appropriate Legal Notices"
|
|
||||||
to the extent that it includes a convenient and prominently visible
|
|
||||||
feature that (1) displays an appropriate copyright notice, and (2)
|
|
||||||
tells the user that there is no warranty for the work (except to the
|
|
||||||
extent that warranties are provided), that licensees may convey the
|
|
||||||
work under this License, and how to view a copy of this License. If
|
|
||||||
the interface presents a list of user commands or options, such as a
|
|
||||||
menu, a prominent item in the list meets this criterion.
|
|
||||||
|
|
||||||
|
|
||||||
1. Source Code.
|
|
||||||
|
|
||||||
|
|
||||||
The "source code" for a work means the preferred form of the work
|
|
||||||
for making modifications to it. "Object code" means any non-source
|
|
||||||
form of a work.
|
|
||||||
|
|
||||||
|
|
||||||
A "Standard Interface" means an interface that either is an official
|
|
||||||
standard defined by a recognized standards body, or, in the case of
|
|
||||||
interfaces specified for a particular programming language, one that
|
|
||||||
is widely used among developers working in that language.
|
|
||||||
|
|
||||||
|
|
||||||
The "System Libraries" of an executable work include anything, other
|
|
||||||
than the work as a whole, that (a) is included in the normal form of
|
|
||||||
packaging a Major Component, but which is not part of that Major
|
|
||||||
Component, and (b) serves only to enable use of the work with that
|
|
||||||
Major Component, or to implement a Standard Interface for which an
|
|
||||||
implementation is available to the public in source code form. A
|
|
||||||
"Major Component", in this context, means a major essential component
|
|
||||||
(kernel, window system, and so on) of the specific operating system
|
|
||||||
(if any) on which the executable work runs, or a compiler used to
|
|
||||||
produce the work, or an object code interpreter used to run it.
|
|
||||||
|
|
||||||
|
|
||||||
The "Corresponding Source" for a work in object code form means all
|
|
||||||
the source code needed to generate, install, and (for an executable
|
|
||||||
work) run the object code and to modify the work, including scripts to
|
|
||||||
control those activities. However, it does not include the work's
|
|
||||||
System Libraries, or general-purpose tools or generally available free
|
|
||||||
programs which are used unmodified in performing those activities but
|
|
||||||
which are not part of the work. For example, Corresponding Source
|
|
||||||
includes interface definition files associated with source files for
|
|
||||||
the work, and the source code for shared libraries and dynamically
|
|
||||||
linked subprograms that the work is specifically designed to require,
|
|
||||||
such as by intimate data communication or control flow between those
|
|
||||||
subprograms and other parts of the work.
|
|
||||||
|
|
||||||
|
|
||||||
The Corresponding Source need not include anything that users
|
|
||||||
can regenerate automatically from other parts of the Corresponding
|
|
||||||
Source.
|
|
||||||
|
|
||||||
|
|
||||||
The Corresponding Source for a work in source code form is that
|
|
||||||
same work.
|
|
||||||
|
|
||||||
|
|
||||||
2. Basic Permissions.
|
|
||||||
|
|
||||||
|
|
||||||
All rights granted under this License are granted for the term of
|
|
||||||
copyright on the Program, and are irrevocable provided the stated
|
|
||||||
conditions are met. This License explicitly affirms your unlimited
|
|
||||||
permission to run the unmodified Program. The output from running a
|
|
||||||
covered work is covered by this License only if the output, given its
|
|
||||||
content, constitutes a covered work. This License acknowledges your
|
|
||||||
rights of fair use or other equivalent, as provided by copyright law.
|
|
||||||
|
|
||||||
|
|
||||||
You may make, run and propagate covered works that you do not
|
|
||||||
convey, without conditions so long as your license otherwise remains
|
|
||||||
in force. You may convey covered works to others for the sole purpose
|
|
||||||
of having them make modifications exclusively for you, or provide you
|
|
||||||
with facilities for running those works, provided that you comply with
|
|
||||||
the terms of this License in conveying all material for which you do
|
|
||||||
not control copyright. Those thus making or running the covered works
|
|
||||||
for you must do so exclusively on your behalf, under your direction
|
|
||||||
and control, on terms that prohibit them from making any copies of
|
|
||||||
your copyrighted material outside their relationship with you.
|
|
||||||
|
|
||||||
|
|
||||||
Conveying under any other circumstances is permitted solely under
|
|
||||||
the conditions stated below. Sublicensing is not allowed; section 10
|
|
||||||
makes it unnecessary.
|
|
||||||
|
|
||||||
|
|
||||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
|
||||||
|
|
||||||
|
|
||||||
No covered work shall be deemed part of an effective technological
|
|
||||||
measure under any applicable law fulfilling obligations under article
|
|
||||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
|
||||||
similar laws prohibiting or restricting circumvention of such
|
|
||||||
measures.
|
|
||||||
|
|
||||||
|
|
||||||
When you convey a covered work, you waive any legal power to forbid
|
|
||||||
circumvention of technological measures to the extent such circumvention
|
|
||||||
is effected by exercising rights under this License with respect to
|
|
||||||
the covered work, and you disclaim any intention to limit operation or
|
|
||||||
modification of the work as a means of enforcing, against the work's
|
|
||||||
users, your or third parties' legal rights to forbid circumvention of
|
|
||||||
technological measures.
|
|
||||||
|
|
||||||
|
|
||||||
4. Conveying Verbatim Copies.
|
|
||||||
|
|
||||||
|
|
||||||
You may convey verbatim copies of the Program's source code as you
|
|
||||||
receive it, in any medium, provided that you conspicuously and
|
|
||||||
appropriately publish on each copy an appropriate copyright notice;
|
|
||||||
keep intact all notices stating that this License and any
|
|
||||||
non-permissive terms added in accord with section 7 apply to the code;
|
|
||||||
keep intact all notices of the absence of any warranty; and give all
|
|
||||||
recipients a copy of this License along with the Program.
|
|
||||||
|
|
||||||
|
|
||||||
You may charge any price or no price for each copy that you convey,
|
|
||||||
and you may offer support or warranty protection for a fee.
|
|
||||||
|
|
||||||
|
|
||||||
5. Conveying Modified Source Versions.
|
|
||||||
|
|
||||||
|
|
||||||
You may convey a work based on the Program, or the modifications to
|
|
||||||
produce it from the Program, in the form of source code under the
|
|
||||||
terms of section 4, provided that you also meet all of these conditions:
|
|
||||||
|
|
||||||
|
|
||||||
a) The work must carry prominent notices stating that you modified
|
|
||||||
it, and giving a relevant date.
|
|
||||||
|
|
||||||
|
|
||||||
b) The work must carry prominent notices stating that it is
|
|
||||||
released under this License and any conditions added under section
|
|
||||||
7. This requirement modifies the requirement in section 4 to
|
|
||||||
"keep intact all notices".
|
|
||||||
|
|
||||||
|
|
||||||
c) You must license the entire work, as a whole, under this
|
|
||||||
License to anyone who comes into possession of a copy. This
|
|
||||||
License will therefore apply, along with any applicable section 7
|
|
||||||
additional terms, to the whole of the work, and all its parts,
|
|
||||||
regardless of how they are packaged. This License gives no
|
|
||||||
permission to license the work in any other way, but it does not
|
|
||||||
invalidate such permission if you have separately received it.
|
|
||||||
|
|
||||||
|
|
||||||
d) If the work has interactive user interfaces, each must display
|
|
||||||
Appropriate Legal Notices; however, if the Program has interactive
|
|
||||||
interfaces that do not display Appropriate Legal Notices, your
|
|
||||||
work need not make them do so.
|
|
||||||
|
|
||||||
|
|
||||||
A compilation of a covered work with other separate and independent
|
|
||||||
works, which are not by their nature extensions of the covered work,
|
|
||||||
and which are not combined with it such as to form a larger program,
|
|
||||||
in or on a volume of a storage or distribution medium, is called an
|
|
||||||
"aggregate" if the compilation and its resulting copyright are not
|
|
||||||
used to limit the access or legal rights of the compilation's users
|
|
||||||
beyond what the individual works permit. Inclusion of a covered work
|
|
||||||
in an aggregate does not cause this License to apply to the other
|
|
||||||
parts of the aggregate.
|
|
||||||
|
|
||||||
|
|
||||||
6. Conveying Non-Source Forms.
|
|
||||||
|
|
||||||
|
|
||||||
You may convey a covered work in object code form under the terms
|
|
||||||
of sections 4 and 5, provided that you also convey the
|
|
||||||
machine-readable Corresponding Source under the terms of this License,
|
|
||||||
in one of these ways:
|
|
||||||
|
|
||||||
|
|
||||||
a) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by the
|
|
||||||
Corresponding Source fixed on a durable physical medium
|
|
||||||
customarily used for software interchange.
|
|
||||||
|
|
||||||
|
|
||||||
b) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by a
|
|
||||||
written offer, valid for at least three years and valid for as
|
|
||||||
long as you offer spare parts or customer support for that product
|
|
||||||
model, to give anyone who possesses the object code either (1) a
|
|
||||||
copy of the Corresponding Source for all the software in the
|
|
||||||
product that is covered by this License, on a durable physical
|
|
||||||
medium customarily used for software interchange, for a price no
|
|
||||||
more than your reasonable cost of physically performing this
|
|
||||||
conveying of source, or (2) access to copy the
|
|
||||||
Corresponding Source from a network server at no charge.
|
|
||||||
|
|
||||||
|
|
||||||
c) Convey individual copies of the object code with a copy of the
|
|
||||||
written offer to provide the Corresponding Source. This
|
|
||||||
alternative is allowed only occasionally and noncommercially, and
|
|
||||||
only if you received the object code with such an offer, in accord
|
|
||||||
with subsection 6b.
|
|
||||||
|
|
||||||
|
|
||||||
d) Convey the object code by offering access from a designated
|
|
||||||
place (gratis or for a charge), and offer equivalent access to the
|
|
||||||
Corresponding Source in the same way through the same place at no
|
|
||||||
further charge. You need not require recipients to copy the
|
|
||||||
Corresponding Source along with the object code. If the place to
|
|
||||||
copy the object code is a network server, the Corresponding Source
|
|
||||||
may be on a different server (operated by you or a third party)
|
|
||||||
that supports equivalent copying facilities, provided you maintain
|
|
||||||
clear directions next to the object code saying where to find the
|
|
||||||
Corresponding Source. Regardless of what server hosts the
|
|
||||||
Corresponding Source, you remain obligated to ensure that it is
|
|
||||||
available for as long as needed to satisfy these requirements.
|
|
||||||
|
|
||||||
|
|
||||||
e) Convey the object code using peer-to-peer transmission, provided
|
|
||||||
you inform other peers where the object code and Corresponding
|
|
||||||
Source of the work are being offered to the general public at no
|
|
||||||
charge under subsection 6d.
|
|
||||||
|
|
||||||
|
|
||||||
A separable portion of the object code, whose source code is excluded
|
|
||||||
from the Corresponding Source as a System Library, need not be
|
|
||||||
included in conveying the object code work.
|
|
||||||
|
|
||||||
|
|
||||||
A "User Product" is either (1) a "consumer product", which means any
|
|
||||||
tangible personal property which is normally used for personal, family,
|
|
||||||
or household purposes, or (2) anything designed or sold for incorporation
|
|
||||||
into a dwelling. In determining whether a product is a consumer product,
|
|
||||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
|
||||||
product received by a particular user, "normally used" refers to a
|
|
||||||
typical or common use of that class of product, regardless of the status
|
|
||||||
of the particular user or of the way in which the particular user
|
|
||||||
actually uses, or expects or is expected to use, the product. A product
|
|
||||||
is a consumer product regardless of whether the product has substantial
|
|
||||||
commercial, industrial or non-consumer uses, unless such uses represent
|
|
||||||
the only significant mode of use of the product.
|
|
||||||
|
|
||||||
|
|
||||||
"Installation Information" for a User Product means any methods,
|
|
||||||
procedures, authorization keys, or other information required to install
|
|
||||||
and execute modified versions of a covered work in that User Product from
|
|
||||||
a modified version of its Corresponding Source. The information must
|
|
||||||
suffice to ensure that the continued functioning of the modified object
|
|
||||||
code is in no case prevented or interfered with solely because
|
|
||||||
modification has been made.
|
|
||||||
|
|
||||||
|
|
||||||
If you convey an object code work under this section in, or with, or
|
|
||||||
specifically for use in, a User Product, and the conveying occurs as
|
|
||||||
part of a transaction in which the right of possession and use of the
|
|
||||||
User Product is transferred to the recipient in perpetuity or for a
|
|
||||||
fixed term (regardless of how the transaction is characterized), the
|
|
||||||
Corresponding Source conveyed under this section must be accompanied
|
|
||||||
by the Installation Information. But this requirement does not apply
|
|
||||||
if neither you nor any third party retains the ability to install
|
|
||||||
modified object code on the User Product (for example, the work has
|
|
||||||
been installed in ROM).
|
|
||||||
|
|
||||||
|
|
||||||
The requirement to provide Installation Information does not include a
|
|
||||||
requirement to continue to provide support service, warranty, or updates
|
|
||||||
for a work that has been modified or installed by the recipient, or for
|
|
||||||
the User Product in which it has been modified or installed. Access to a
|
|
||||||
network may be denied when the modification itself materially and
|
|
||||||
adversely affects the operation of the network or violates the rules and
|
|
||||||
protocols for communication across the network.
|
|
||||||
|
|
||||||
|
|
||||||
Corresponding Source conveyed, and Installation Information provided,
|
|
||||||
in accord with this section must be in a format that is publicly
|
|
||||||
documented (and with an implementation available to the public in
|
|
||||||
source code form), and must require no special password or key for
|
|
||||||
unpacking, reading or copying.
|
|
||||||
|
|
||||||
|
|
||||||
7. Additional Terms.
|
|
||||||
|
|
||||||
|
|
||||||
"Additional permissions" are terms that supplement the terms of this
|
|
||||||
License by making exceptions from one or more of its conditions.
|
|
||||||
Additional permissions that are applicable to the entire Program shall
|
|
||||||
be treated as though they were included in this License, to the extent
|
|
||||||
that they are valid under applicable law. If additional permissions
|
|
||||||
apply only to part of the Program, that part may be used separately
|
|
||||||
under those permissions, but the entire Program remains governed by
|
|
||||||
this License without regard to the additional permissions.
|
|
||||||
|
|
||||||
|
|
||||||
When you convey a copy of a covered work, you may at your option
|
|
||||||
remove any additional permissions from that copy, or from any part of
|
|
||||||
it. (Additional permissions may be written to require their own
|
|
||||||
removal in certain cases when you modify the work.) You may place
|
|
||||||
additional permissions on material, added by you to a covered work,
|
|
||||||
for which you have or can give appropriate copyright permission.
|
|
||||||
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, for material you
|
|
||||||
add to a covered work, you may (if authorized by the copyright holders of
|
|
||||||
that material) supplement the terms of this License with terms:
|
|
||||||
|
|
||||||
|
|
||||||
a) Disclaiming warranty or limiting liability differently from the
|
|
||||||
terms of sections 15 and 16 of this License; or
|
|
||||||
|
|
||||||
|
|
||||||
b) Requiring preservation of specified reasonable legal notices or
|
|
||||||
author attributions in that material or in the Appropriate Legal
|
|
||||||
Notices displayed by works containing it; or
|
|
||||||
|
|
||||||
|
|
||||||
c) Prohibiting misrepresentation of the origin of that material, or
|
|
||||||
requiring that modified versions of such material be marked in
|
|
||||||
reasonable ways as different from the original version; or
|
|
||||||
|
|
||||||
|
|
||||||
d) Limiting the use for publicity purposes of names of licensors or
|
|
||||||
authors of the material; or
|
|
||||||
|
|
||||||
|
|
||||||
e) Declining to grant rights under trademark law for use of some
|
|
||||||
trade names, trademarks, or service marks; or
|
|
||||||
|
|
||||||
|
|
||||||
f) Requiring indemnification of licensors and authors of that
|
|
||||||
material by anyone who conveys the material (or modified versions of
|
|
||||||
it) with contractual assumptions of liability to the recipient, for
|
|
||||||
any liability that these contractual assumptions directly impose on
|
|
||||||
those licensors and authors.
|
|
||||||
|
|
||||||
|
|
||||||
All other non-permissive additional terms are considered "further
|
|
||||||
restrictions" within the meaning of section 10. If the Program as you
|
|
||||||
received it, or any part of it, contains a notice stating that it is
|
|
||||||
governed by this License along with a term that is a further
|
|
||||||
restriction, you may remove that term. If a license document contains
|
|
||||||
a further restriction but permits relicensing or conveying under this
|
|
||||||
License, you may add to a covered work material governed by the terms
|
|
||||||
of that license document, provided that the further restriction does
|
|
||||||
not survive such relicensing or conveying.
|
|
||||||
|
|
||||||
|
|
||||||
If you add terms to a covered work in accord with this section, you
|
|
||||||
must place, in the relevant source files, a statement of the
|
|
||||||
additional terms that apply to those files, or a notice indicating
|
|
||||||
where to find the applicable terms.
|
|
||||||
|
|
||||||
|
|
||||||
Additional terms, permissive or non-permissive, may be stated in the
|
|
||||||
form of a separately written license, or stated as exceptions;
|
|
||||||
the above requirements apply either way.
|
|
||||||
|
|
||||||
|
|
||||||
8. Termination.
|
|
||||||
|
|
||||||
|
|
||||||
You may not propagate or modify a covered work except as expressly
|
|
||||||
provided under this License. Any attempt otherwise to propagate or
|
|
||||||
modify it is void, and will automatically terminate your rights under
|
|
||||||
this License (including any patent licenses granted under the third
|
|
||||||
paragraph of section 11).
|
|
||||||
|
|
||||||
|
|
||||||
However, if you cease all violation of this License, then your
|
|
||||||
license from a particular copyright holder is reinstated (a)
|
|
||||||
provisionally, unless and until the copyright holder explicitly and
|
|
||||||
finally terminates your license, and (b) permanently, if the copyright
|
|
||||||
holder fails to notify you of the violation by some reasonable means
|
|
||||||
prior to 60 days after the cessation.
|
|
||||||
|
|
||||||
|
|
||||||
Moreover, your license from a particular copyright holder is
|
|
||||||
reinstated permanently if the copyright holder notifies you of the
|
|
||||||
violation by some reasonable means, this is the first time you have
|
|
||||||
received notice of violation of this License (for any work) from that
|
|
||||||
copyright holder, and you cure the violation prior to 30 days after
|
|
||||||
your receipt of the notice.
|
|
||||||
|
|
||||||
|
|
||||||
Termination of your rights under this section does not terminate the
|
|
||||||
licenses of parties who have received copies or rights from you under
|
|
||||||
this License. If your rights have been terminated and not permanently
|
|
||||||
reinstated, you do not qualify to receive new licenses for the same
|
|
||||||
material under section 10.
|
|
||||||
|
|
||||||
|
|
||||||
9. Acceptance Not Required for Having Copies.
|
|
||||||
|
|
||||||
|
|
||||||
You are not required to accept this License in order to receive or
|
|
||||||
run a copy of the Program. Ancillary propagation of a covered work
|
|
||||||
occurring solely as a consequence of using peer-to-peer transmission
|
|
||||||
to receive a copy likewise does not require acceptance. However,
|
|
||||||
nothing other than this License grants you permission to propagate or
|
|
||||||
modify any covered work. These actions infringe copyright if you do
|
|
||||||
not accept this License. Therefore, by modifying or propagating a
|
|
||||||
covered work, you indicate your acceptance of this License to do so.
|
|
||||||
|
|
||||||
|
|
||||||
10. Automatic Licensing of Downstream Recipients.
|
|
||||||
|
|
||||||
|
|
||||||
Each time you convey a covered work, the recipient automatically
|
|
||||||
receives a license from the original licensors, to run, modify and
|
|
||||||
propagate that work, subject to this License. You are not responsible
|
|
||||||
for enforcing compliance by third parties with this License.
|
|
||||||
|
|
||||||
|
|
||||||
An "entity transaction" is a transaction transferring control of an
|
|
||||||
organization, or substantially all assets of one, or subdividing an
|
|
||||||
organization, or merging organizations. If propagation of a covered
|
|
||||||
work results from an entity transaction, each party to that
|
|
||||||
transaction who receives a copy of the work also receives whatever
|
|
||||||
licenses to the work the party's predecessor in interest had or could
|
|
||||||
give under the previous paragraph, plus a right to possession of the
|
|
||||||
Corresponding Source of the work from the predecessor in interest, if
|
|
||||||
the predecessor has it or can get it with reasonable efforts.
|
|
||||||
|
|
||||||
|
|
||||||
You may not impose any further restrictions on the exercise of the
|
|
||||||
rights granted or affirmed under this License. For example, you may
|
|
||||||
not impose a license fee, royalty, or other charge for exercise of
|
|
||||||
rights granted under this License, and you may not initiate litigation
|
|
||||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
|
||||||
any patent claim is infringed by making, using, selling, offering for
|
|
||||||
sale, or importing the Program or any portion of it.
|
|
||||||
|
|
||||||
|
|
||||||
11. Patents.
|
|
||||||
|
|
||||||
|
|
||||||
A "contributor" is a copyright holder who authorizes use under this
|
|
||||||
License of the Program or a work on which the Program is based. The
|
|
||||||
work thus licensed is called the contributor's "contributor version".
|
|
||||||
|
|
||||||
|
|
||||||
A contributor's "essential patent claims" are all patent claims
|
|
||||||
owned or controlled by the contributor, whether already acquired or
|
|
||||||
hereafter acquired, that would be infringed by some manner, permitted
|
|
||||||
by this License, of making, using, or selling its contributor version,
|
|
||||||
but do not include claims that would be infringed only as a
|
|
||||||
consequence of further modification of the contributor version. For
|
|
||||||
purposes of this definition, "control" includes the right to grant
|
|
||||||
patent sublicenses in a manner consistent with the requirements of
|
|
||||||
this License.
|
|
||||||
|
|
||||||
|
|
||||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
|
||||||
patent license under the contributor's essential patent claims, to
|
|
||||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
|
||||||
propagate the contents of its contributor version.
|
|
||||||
|
|
||||||
|
|
||||||
In the following three paragraphs, a "patent license" is any express
|
|
||||||
agreement or commitment, however denominated, not to enforce a patent
|
|
||||||
(such as an express permission to practice a patent or covenant not to
|
|
||||||
sue for patent infringement). To "grant" such a patent license to a
|
|
||||||
party means to make such an agreement or commitment not to enforce a
|
|
||||||
patent against the party.
|
|
||||||
|
|
||||||
|
|
||||||
If you convey a covered work, knowingly relying on a patent license,
|
|
||||||
and the Corresponding Source of the work is not available for anyone
|
|
||||||
to copy, free of charge and under the terms of this License, through a
|
|
||||||
publicly available network server or other readily accessible means,
|
|
||||||
then you must either (1) cause the Corresponding Source to be so
|
|
||||||
available, or (2) arrange to deprive yourself of the benefit of the
|
|
||||||
patent license for this particular work, or (3) arrange, in a manner
|
|
||||||
consistent with the requirements of this License, to extend the patent
|
|
||||||
license to downstream recipients. "Knowingly relying" means you have
|
|
||||||
actual knowledge that, but for the patent license, your conveying the
|
|
||||||
covered work in a country, or your recipient's use of the covered work
|
|
||||||
in a country, would infringe one or more identifiable patents in that
|
|
||||||
country that you have reason to believe are valid.
|
|
||||||
|
|
||||||
|
|
||||||
If, pursuant to or in connection with a single transaction or
|
|
||||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
|
||||||
covered work, and grant a patent license to some of the parties
|
|
||||||
receiving the covered work authorizing them to use, propagate, modify
|
|
||||||
or convey a specific copy of the covered work, then the patent license
|
|
||||||
you grant is automatically extended to all recipients of the covered
|
|
||||||
work and works based on it.
|
|
||||||
|
|
||||||
|
|
||||||
A patent license is "discriminatory" if it does not include within
|
|
||||||
the scope of its coverage, prohibits the exercise of, or is
|
|
||||||
conditioned on the non-exercise of one or more of the rights that are
|
|
||||||
specifically granted under this License. You may not convey a covered
|
|
||||||
work if you are a party to an arrangement with a third party that is
|
|
||||||
in the business of distributing software, under which you make payment
|
|
||||||
to the third party based on the extent of your activity of conveying
|
|
||||||
the work, and under which the third party grants, to any of the
|
|
||||||
parties who would receive the covered work from you, a discriminatory
|
|
||||||
patent license (a) in connection with copies of the covered work
|
|
||||||
conveyed by you (or copies made from those copies), or (b) primarily
|
|
||||||
for and in connection with specific products or compilations that
|
|
||||||
contain the covered work, unless you entered into that arrangement,
|
|
||||||
or that patent license was granted, prior to 28 March 2007.
|
|
||||||
|
|
||||||
|
|
||||||
Nothing in this License shall be construed as excluding or limiting
|
|
||||||
any implied license or other defenses to infringement that may
|
|
||||||
otherwise be available to you under applicable patent law.
|
|
||||||
|
|
||||||
|
|
||||||
12. No Surrender of Others' Freedom.
|
|
||||||
|
|
||||||
|
|
||||||
If conditions are imposed on you (whether by court order, agreement or
|
|
||||||
otherwise) that contradict the conditions of this License, they do not
|
|
||||||
excuse you from the conditions of this License. If you cannot convey a
|
|
||||||
covered work so as to satisfy simultaneously your obligations under this
|
|
||||||
License and any other pertinent obligations, then as a consequence you may
|
|
||||||
not convey it at all. For example, if you agree to terms that obligate you
|
|
||||||
to collect a royalty for further conveying from those to whom you convey
|
|
||||||
the Program, the only way you could satisfy both those terms and this
|
|
||||||
License would be to refrain entirely from conveying the Program.
|
|
||||||
|
|
||||||
|
|
||||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
|
||||||
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, if you modify the
|
|
||||||
Program, your modified version must prominently offer all users
|
|
||||||
interacting with it remotely through a computer network (if your version
|
|
||||||
supports such interaction) an opportunity to receive the Corresponding
|
|
||||||
Source of your version by providing access to the Corresponding Source
|
|
||||||
from a network server at no charge, through some standard or customary
|
|
||||||
means of facilitating copying of software. This Corresponding Source
|
|
||||||
shall include the Corresponding Source for any work covered by version 3
|
|
||||||
of the GNU General Public License that is incorporated pursuant to the
|
|
||||||
following paragraph.
|
|
||||||
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
|
||||||
permission to link or combine any covered work with a work licensed
|
|
||||||
under version 3 of the GNU General Public License into a single
|
|
||||||
combined work, and to convey the resulting work. The terms of this
|
|
||||||
License will continue to apply to the part which is the covered work,
|
|
||||||
but the work with which it is combined will remain governed by version
|
|
||||||
3 of the GNU General Public License.
|
|
||||||
|
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
|
||||||
|
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
|
||||||
the GNU Affero General Public License from time to time. Such new versions
|
|
||||||
will be similar in spirit to the present version, but may differ in detail to
|
|
||||||
address new problems or concerns.
|
|
||||||
|
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
|
||||||
Program specifies that a certain numbered version of the GNU Affero General
|
|
||||||
Public License "or any later version" applies to it, you have the
|
|
||||||
option of following the terms and conditions either of that numbered
|
|
||||||
version or of any later version published by the Free Software
|
|
||||||
Foundation. If the Program does not specify a version number of the
|
|
||||||
GNU Affero General Public License, you may choose any version ever published
|
|
||||||
by the Free Software Foundation.
|
|
||||||
|
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
|
||||||
versions of the GNU Affero General Public License can be used, that proxy's
|
|
||||||
public statement of acceptance of a version permanently authorizes you
|
|
||||||
to choose that version for the Program.
|
|
||||||
|
|
||||||
|
|
||||||
Later license versions may give you additional or different
|
|
||||||
permissions. However, no additional obligations are imposed on any
|
|
||||||
author or copyright holder as a result of your choosing to follow a
|
|
||||||
later version.
|
|
||||||
|
|
||||||
|
|
||||||
15. Disclaimer of Warranty.
|
|
||||||
|
|
||||||
|
|
||||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
|
||||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
|
||||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
|
||||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
||||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
|
||||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
|
||||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
|
||||||
|
|
||||||
|
|
||||||
16. Limitation of Liability.
|
|
||||||
|
|
||||||
|
|
||||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
|
||||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
|
||||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
|
||||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
|
||||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
|
||||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
|
||||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
|
||||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
|
||||||
SUCH DAMAGES.
|
|
||||||
|
|
||||||
|
|
||||||
17. Interpretation of Sections 15 and 16.
|
|
||||||
|
|
||||||
|
|
||||||
If the disclaimer of warranty and limitation of liability provided
|
|
||||||
above cannot be given local legal effect according to their terms,
|
|
||||||
reviewing courts shall apply local law that most closely approximates
|
|
||||||
an absolute waiver of all civil liability in connection with the
|
|
||||||
Program, unless a warranty or assumption of liability accompanies a
|
|
||||||
copy of the Program in return for a fee.
|
|
||||||
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
|
|
||||||
How to Apply These Terms to Your New Programs
|
|
||||||
|
|
||||||
|
|
||||||
If you develop a new program, and you want it to be of the greatest
|
|
||||||
possible use to the public, the best way to achieve this is to make it
|
|
||||||
free software which everyone can redistribute and change under these terms.
|
|
||||||
|
|
||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest
|
|
||||||
to attach them to the start of each source file to most effectively
|
|
||||||
state the exclusion of warranty; and each file should have at least
|
|
||||||
the "copyright" line and a pointer to where the full notice is found.
|
|
||||||
|
|
||||||
|
|
||||||
<one line to give the program's name and a brief idea of what it does.>
|
|
||||||
Copyright (C) <year> <name of author>
|
|
||||||
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
|
||||||
|
|
||||||
|
|
||||||
If your software can interact with users remotely through a computer
|
|
||||||
network, you should also make sure that it provides a way for users to
|
|
||||||
get its source. For example, if your program is a web application, its
|
|
||||||
interface could display a "Source" link that leads users to an archive
|
|
||||||
of the code. There are many ways you could offer source, and different
|
|
||||||
solutions will be better for different programs; see section 13 for the
|
|
||||||
specific requirements.
|
|
||||||
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
|
||||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
|
||||||
<https://www.gnu.org/licenses/>.
|
|
||||||
|
|
@ -29,6 +29,8 @@ Also... we're hiring! Check out our [jobs](https://zed.dev/jobs) page for open r
|
||||||
|
|
||||||
### Licensing
|
### Licensing
|
||||||
|
|
||||||
|
Zed source code is licensed primarily under GPL-3.0-or-later, with Apache-2.0 components where marked.
|
||||||
|
|
||||||
License information for third party dependencies must be correctly provided for CI to pass.
|
License information for third party dependencies must be correctly provided for CI to pass.
|
||||||
|
|
||||||
We use [`cargo-about`](https://github.com/EmbarkStudios/cargo-about) to automatically comply with open source licenses. If CI is failing, check the following:
|
We use [`cargo-about`](https://github.com/EmbarkStudios/cargo-about) to automatically comply with open source licenses. If CI is failing, check the following:
|
||||||
|
|
|
||||||
|
|
@ -380,15 +380,7 @@
|
||||||
"shift-backspace": "agent::ArchiveSelectedThread",
|
"shift-backspace": "agent::ArchiveSelectedThread",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"context": "RulesLibrary",
|
|
||||||
"bindings": {
|
|
||||||
"new": "rules_library::NewRule",
|
|
||||||
"ctrl-n": "rules_library::NewRule",
|
|
||||||
"ctrl-shift-s": "rules_library::ToggleDefaultRule",
|
|
||||||
"ctrl-w": "workspace::CloseWindow",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"context": "BufferSearchBar",
|
"context": "BufferSearchBar",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
|
|
|
||||||
|
|
@ -427,15 +427,6 @@
|
||||||
"backspace": "agent::ArchiveSelectedThread",
|
"backspace": "agent::ArchiveSelectedThread",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"context": "RulesLibrary",
|
|
||||||
"use_key_equivalents": true,
|
|
||||||
"bindings": {
|
|
||||||
"cmd-n": "rules_library::NewRule",
|
|
||||||
"cmd-shift-s": "rules_library::ToggleDefaultRule",
|
|
||||||
"cmd-w": "workspace::CloseWindow",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"context": "BufferSearchBar",
|
"context": "BufferSearchBar",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
|
|
|
||||||
|
|
@ -383,15 +383,6 @@
|
||||||
"shift-backspace": "agent::ArchiveSelectedThread",
|
"shift-backspace": "agent::ArchiveSelectedThread",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"context": "RulesLibrary",
|
|
||||||
"use_key_equivalents": true,
|
|
||||||
"bindings": {
|
|
||||||
"ctrl-n": "rules_library::NewRule",
|
|
||||||
"ctrl-shift-s": "rules_library::ToggleDefaultRule",
|
|
||||||
"ctrl-w": "workspace::CloseWindow",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"context": "BufferSearchBar",
|
"context": "BufferSearchBar",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,6 @@ parking_lot = { workspace = true, optional = true }
|
||||||
image.workspace = true
|
image.workspace = true
|
||||||
portable-pty.workspace = true
|
portable-pty.workspace = true
|
||||||
project.workspace = true
|
project.workspace = true
|
||||||
prompt_store.workspace = true
|
|
||||||
sandbox.workspace = true
|
sandbox.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,9 @@ use gpui::{
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use language::language_settings::FormatOnSave;
|
use language::language_settings::FormatOnSave;
|
||||||
use language::{Anchor, Buffer, BufferSnapshot, LanguageRegistry, Point, ToPoint, text_diff};
|
use language::{
|
||||||
|
Anchor, Buffer, BufferEditSource, BufferSnapshot, LanguageRegistry, Point, ToPoint, text_diff,
|
||||||
|
};
|
||||||
use markdown::{Markdown, MarkdownOptions};
|
use markdown::{Markdown, MarkdownOptions};
|
||||||
pub use mention::*;
|
pub use mention::*;
|
||||||
use project::lsp_store::{FormatTrigger, LspFormatTarget};
|
use project::lsp_store::{FormatTrigger, LspFormatTarget};
|
||||||
|
|
@ -773,6 +775,7 @@ impl ContentBlock {
|
||||||
None,
|
None,
|
||||||
MarkdownOptions {
|
MarkdownOptions {
|
||||||
render_mermaid_diagrams: true,
|
render_mermaid_diagrams: true,
|
||||||
|
render_metadata_blocks: true,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
cx,
|
cx,
|
||||||
|
|
@ -2912,7 +2915,9 @@ impl AcpThread {
|
||||||
});
|
});
|
||||||
|
|
||||||
let format_on_save = buffer.update(cx, |buffer, cx| {
|
let format_on_save = buffer.update(cx, |buffer, cx| {
|
||||||
|
buffer.start_transaction();
|
||||||
buffer.edit(edits, None, cx);
|
buffer.edit(edits, None, cx);
|
||||||
|
buffer.end_transaction_with_source(BufferEditSource::Agent, cx);
|
||||||
|
|
||||||
let settings =
|
let settings =
|
||||||
language::language_settings::LanguageSettings::for_buffer(buffer, cx);
|
language::language_settings::LanguageSettings::for_buffer(buffer, cx);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
use agent_client_protocol::schema as acp;
|
use agent_client_protocol::schema as acp;
|
||||||
use anyhow::{Context as _, Result, bail};
|
use anyhow::{Context as _, Result, bail};
|
||||||
use file_icons::FileIcons;
|
use file_icons::FileIcons;
|
||||||
use prompt_store::{PromptId, UserPromptId};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
|
|
@ -37,10 +36,6 @@ pub enum MentionUri {
|
||||||
id: acp::SessionId,
|
id: acp::SessionId,
|
||||||
name: String,
|
name: String,
|
||||||
},
|
},
|
||||||
Rule {
|
|
||||||
id: PromptId,
|
|
||||||
name: String,
|
|
||||||
},
|
|
||||||
Diagnostics {
|
Diagnostics {
|
||||||
#[serde(default = "default_include_errors")]
|
#[serde(default = "default_include_errors")]
|
||||||
include_errors: bool,
|
include_errors: bool,
|
||||||
|
|
@ -205,13 +200,6 @@ impl MentionUri {
|
||||||
id: acp::SessionId::new(thread_id),
|
id: acp::SessionId::new(thread_id),
|
||||||
name,
|
name,
|
||||||
})
|
})
|
||||||
} else if let Some(rule_id) = path.strip_prefix("/agent/rule/") {
|
|
||||||
let name = single_query_param(&url, "name")?.context("Missing rule name")?;
|
|
||||||
let rule_id = UserPromptId(rule_id.parse()?);
|
|
||||||
Ok(Self::Rule {
|
|
||||||
id: rule_id.into(),
|
|
||||||
name,
|
|
||||||
})
|
|
||||||
} else if path == "/agent/diagnostics" {
|
} else if path == "/agent/diagnostics" {
|
||||||
let mut include_errors = default_include_errors();
|
let mut include_errors = default_include_errors();
|
||||||
let mut include_warnings = false;
|
let mut include_warnings = false;
|
||||||
|
|
@ -342,7 +330,6 @@ impl MentionUri {
|
||||||
MentionUri::PastedImage { name } => name.clone(),
|
MentionUri::PastedImage { name } => name.clone(),
|
||||||
MentionUri::Symbol { name, .. } => name.clone(),
|
MentionUri::Symbol { name, .. } => name.clone(),
|
||||||
MentionUri::Thread { name, .. } => name.clone(),
|
MentionUri::Thread { name, .. } => name.clone(),
|
||||||
MentionUri::Rule { name, .. } => name.clone(),
|
|
||||||
MentionUri::Diagnostics { .. } => "Diagnostics".to_string(),
|
MentionUri::Diagnostics { .. } => "Diagnostics".to_string(),
|
||||||
MentionUri::TerminalSelection { line_count } => {
|
MentionUri::TerminalSelection { line_count } => {
|
||||||
if *line_count == 1 {
|
if *line_count == 1 {
|
||||||
|
|
@ -443,7 +430,6 @@ impl MentionUri {
|
||||||
.unwrap_or_else(|| IconName::Folder.path().into()),
|
.unwrap_or_else(|| IconName::Folder.path().into()),
|
||||||
MentionUri::Symbol { .. } => IconName::Code.path().into(),
|
MentionUri::Symbol { .. } => IconName::Code.path().into(),
|
||||||
MentionUri::Thread { .. } => IconName::Thread.path().into(),
|
MentionUri::Thread { .. } => IconName::Thread.path().into(),
|
||||||
MentionUri::Rule { .. } => IconName::Reader.path().into(),
|
|
||||||
MentionUri::Diagnostics { .. } => IconName::Warning.path().into(),
|
MentionUri::Diagnostics { .. } => IconName::Warning.path().into(),
|
||||||
MentionUri::TerminalSelection { .. } => IconName::Terminal.path().into(),
|
MentionUri::TerminalSelection { .. } => IconName::Terminal.path().into(),
|
||||||
MentionUri::Selection { .. } => IconName::Reader.path().into(),
|
MentionUri::Selection { .. } => IconName::Reader.path().into(),
|
||||||
|
|
@ -526,12 +512,6 @@ impl MentionUri {
|
||||||
url.query_pairs_mut().append_pair("name", name);
|
url.query_pairs_mut().append_pair("name", name);
|
||||||
url
|
url
|
||||||
}
|
}
|
||||||
MentionUri::Rule { name, id } => {
|
|
||||||
let mut url = Url::parse("zed:///").unwrap();
|
|
||||||
url.set_path(&format!("/agent/rule/{id}"));
|
|
||||||
url.query_pairs_mut().append_pair("name", name);
|
|
||||||
url
|
|
||||||
}
|
|
||||||
MentionUri::Diagnostics {
|
MentionUri::Diagnostics {
|
||||||
include_errors,
|
include_errors,
|
||||||
include_warnings,
|
include_warnings,
|
||||||
|
|
@ -811,20 +791,6 @@ mod tests {
|
||||||
assert_eq!(parsed.to_uri().to_string(), thread_uri);
|
assert_eq!(parsed.to_uri().to_string(), thread_uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_rule_uri() {
|
|
||||||
let rule_uri = "zed:///agent/rule/d8694ff2-90d5-4b6f-be33-33c1763acd52?name=Some+rule";
|
|
||||||
let parsed = MentionUri::parse(rule_uri, PathStyle::local()).unwrap();
|
|
||||||
match &parsed {
|
|
||||||
MentionUri::Rule { id, name } => {
|
|
||||||
assert_eq!(id.to_string(), "d8694ff2-90d5-4b6f-be33-33c1763acd52");
|
|
||||||
assert_eq!(name, "Some rule");
|
|
||||||
}
|
|
||||||
_ => panic!("Expected Rule variant"),
|
|
||||||
}
|
|
||||||
assert_eq!(parsed.to_uri().to_string(), rule_uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_skill_uri_round_trip() {
|
fn test_parse_skill_uri_round_trip() {
|
||||||
let skill_uri = MentionUri::Skill {
|
let skill_uri = MentionUri::Skill {
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,7 @@ zed_env_vars.workspace = true
|
||||||
zstd.workspace = true
|
zstd.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
assets.workspace = true
|
||||||
async-io.workspace = true
|
async-io.workspace = true
|
||||||
agent_servers = { workspace = true, "features" = ["test-support"] }
|
agent_servers = { workspace = true, "features" = ["test-support"] }
|
||||||
client = { workspace = true, "features" = ["test-support"] }
|
client = { workspace = true, "features" = ["test-support"] }
|
||||||
|
|
@ -103,6 +104,7 @@ reqwest_client.workspace = true
|
||||||
settings = { workspace = true, "features" = ["test-support"] }
|
settings = { workspace = true, "features" = ["test-support"] }
|
||||||
|
|
||||||
theme = { workspace = true, "features" = ["test-support"] }
|
theme = { workspace = true, "features" = ["test-support"] }
|
||||||
|
theme_settings.workspace = true
|
||||||
|
|
||||||
unindent = { workspace = true }
|
unindent = { workspace = true }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use std::{
|
use std::{
|
||||||
|
any::Any,
|
||||||
future::Future,
|
future::Future,
|
||||||
path::Path,
|
path::Path,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
|
|
@ -14,26 +15,40 @@ use agent_settings::{AgentSettings, ToolRules};
|
||||||
use criterion::{
|
use criterion::{
|
||||||
BatchSize, BenchmarkId, Criterion, Throughput, black_box, criterion_group, criterion_main,
|
BatchSize, BenchmarkId, Criterion, Throughput, black_box, criterion_group, criterion_main,
|
||||||
};
|
};
|
||||||
use futures::{pin_mut, task::noop_waker};
|
use editor::{Editor, EditorStyle};
|
||||||
use gpui::{AppContext as _, BackgroundExecutor, Entity, TestAppContext, UpdateGlobal as _};
|
use futures::{StreamExt as _, pin_mut, task::noop_waker};
|
||||||
|
use gpui::{
|
||||||
|
AnyWindowHandle, AppContext as _, BackgroundExecutor, Entity, Focusable as _, TestAppContext,
|
||||||
|
UpdateGlobal as _,
|
||||||
|
};
|
||||||
|
use language::{FakeLspAdapter, rust_lang};
|
||||||
use language_model::fake_provider::FakeLanguageModel;
|
use language_model::fake_provider::FakeLanguageModel;
|
||||||
use project::{FakeFs, Project};
|
use project::{FakeFs, Project};
|
||||||
use prompt_store::ProjectContext;
|
use prompt_store::ProjectContext;
|
||||||
use rand::{Rng as _, SeedableRng as _, rngs::StdRng};
|
use rand::{Rng as _, SeedableRng as _, rngs::StdRng};
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
use settings::{Settings as _, SettingsStore};
|
use settings::{Settings as _, SettingsStore};
|
||||||
|
use ui::IntoElement as _;
|
||||||
|
|
||||||
const SEED: u64 = 0x5EED_5EED;
|
const SEED: u64 = 0x5EED_5EED;
|
||||||
const OLD_TEXT_CHUNK_SIZE: usize = 512;
|
const OLD_TEXT_CHUNK_SIZE: usize = 512;
|
||||||
const NEW_TEXT_CHUNK_SIZE: usize = 512;
|
const NEW_TEXT_CHUNK_SIZE: usize = 512;
|
||||||
|
|
||||||
|
const FILE_PROJECT_PATH: &str = "root/src/workspace_snapshot.rs";
|
||||||
|
const FILE_ABS_PATH: &str = "/root/src/workspace_snapshot.rs";
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct EditOp {
|
||||||
|
old_text: String,
|
||||||
|
new_text: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct EditFixture {
|
struct EditFixture {
|
||||||
name: &'static str,
|
name: &'static str,
|
||||||
old_file_text: String,
|
old_file_text: String,
|
||||||
expected_file_text: String,
|
expected_file_text: String,
|
||||||
old_text: String,
|
edits: Vec<EditOp>,
|
||||||
new_text: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct BenchmarkHarness {
|
struct BenchmarkHarness {
|
||||||
|
|
@ -43,6 +58,12 @@ struct BenchmarkHarness {
|
||||||
partial_payloads: Vec<Value>,
|
partial_payloads: Vec<Value>,
|
||||||
final_payload: Value,
|
final_payload: Value,
|
||||||
expected_file_text: String,
|
expected_file_text: String,
|
||||||
|
editor: Option<Entity<Editor>>,
|
||||||
|
window: Option<AnyWindowHandle>,
|
||||||
|
// Keeps the LSP buffer-registration handle and the fake language server alive
|
||||||
|
// for the lifetime of the benchmark so `didChange`/diagnostics keep flowing
|
||||||
|
// while edits are applied.
|
||||||
|
keep_alive: Vec<Box<dyn Any>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for BenchmarkHarness {
|
impl Drop for BenchmarkHarness {
|
||||||
|
|
@ -50,19 +71,18 @@ impl Drop for BenchmarkHarness {
|
||||||
// Release our handles to the entities first.
|
// Release our handles to the entities first.
|
||||||
self.edit_tool.take();
|
self.edit_tool.take();
|
||||||
self.thread.take();
|
self.thread.take();
|
||||||
|
self.editor.take();
|
||||||
|
self.keep_alive.clear();
|
||||||
|
|
||||||
if let Some(cx) = self.cx.take() {
|
if let Some(mut cx) = self.cx.take() {
|
||||||
// `ActionLog` holds buffers strongly via `tracked_buffers`, and spawns a background
|
// Close the editor window so the editor entity and the buffer handles
|
||||||
// diff-maintenance task that also captures a strong `Entity<Buffer>`. Releasing the
|
// it holds are released, then pump the executor so cancelled editor /
|
||||||
// last handle to the action log only marks its entity for deferred release; the
|
// action-log background tasks drop their captured handles before the
|
||||||
// entity's value (and the buffer handles inside) is not actually dropped until
|
// leak detector runs on `TestAppContext` drop.
|
||||||
// `flush_effects` runs `release_dropped_entities`. Even then, the cancelled task's
|
if let Some(window) = self.window.take() {
|
||||||
// captured handle does not drop until the executor pumps the cancellation through.
|
cx.update_window(window, |_, window, _| window.remove_window())
|
||||||
//
|
.ok();
|
||||||
// Without this two-step teardown, GPUI's test leak detector panics on
|
}
|
||||||
// `TestAppContext` drop because the buffer still appears alive. See
|
|
||||||
// `ActionLog::track_buffer_internal` and `LeakDetector::drop` in
|
|
||||||
// `crates/gpui/src/app/entity_map.rs`.
|
|
||||||
cx.update(|_| {});
|
cx.update(|_| {});
|
||||||
cx.executor().run_until_parked();
|
cx.executor().run_until_parked();
|
||||||
cx.quit();
|
cx.quit();
|
||||||
|
|
@ -76,9 +96,10 @@ fn edit_file_tool_streaming(c: &mut Criterion) {
|
||||||
group.sample_size(10);
|
group.sample_size(10);
|
||||||
|
|
||||||
for fixture in fixtures {
|
for fixture in fixtures {
|
||||||
group.throughput(Throughput::Bytes(fixture.new_text.len() as u64));
|
let new_bytes: usize = fixture.edits.iter().map(|edit| edit.new_text.len()).sum();
|
||||||
|
group.throughput(Throughput::Bytes(new_bytes as u64));
|
||||||
group.bench_with_input(
|
group.bench_with_input(
|
||||||
BenchmarkId::new(fixture.name, fixture.old_text.len()),
|
BenchmarkId::new(fixture.name, fixture.old_file_text.len()),
|
||||||
&fixture,
|
&fixture,
|
||||||
|bench, fixture| {
|
|bench, fixture| {
|
||||||
bench.iter_batched(
|
bench.iter_batched(
|
||||||
|
|
@ -107,26 +128,168 @@ fn edit_file_tool_streaming(c: &mut Criterion) {
|
||||||
fn setup_harness(fixture: EditFixture) -> BenchmarkHarness {
|
fn setup_harness(fixture: EditFixture) -> BenchmarkHarness {
|
||||||
let mut cx = init_context();
|
let mut cx = init_context();
|
||||||
let executor = cx.executor();
|
let executor = cx.executor();
|
||||||
let (edit_tool, thread) = block_on_executor(
|
let parts = block_on_executor(
|
||||||
&executor,
|
&executor,
|
||||||
setup_edit_tool(&mut cx, fixture.old_file_text.clone()),
|
setup_editor_and_tool(&mut cx, fixture.old_file_text.clone()),
|
||||||
);
|
);
|
||||||
let partial_payloads = streamed_partial_payloads(&fixture.old_text, &fixture.new_text);
|
// Let the LSP handshake, initial parse, and first layout settle before timing.
|
||||||
|
cx.executor().run_until_parked();
|
||||||
|
|
||||||
|
let partial_payloads = streamed_partial_payloads(&fixture.edits);
|
||||||
let final_payload = json!({
|
let final_payload = json!({
|
||||||
"path": "root/src/workspace_snapshot.rs",
|
"path": FILE_PROJECT_PATH,
|
||||||
"edits": [{
|
"edits": fixture
|
||||||
"old_text": fixture.old_text,
|
.edits
|
||||||
"new_text": fixture.new_text,
|
.iter()
|
||||||
}],
|
.map(|edit| json!({ "old_text": edit.old_text, "new_text": edit.new_text }))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
});
|
});
|
||||||
|
|
||||||
BenchmarkHarness {
|
BenchmarkHarness {
|
||||||
cx: Some(cx),
|
cx: Some(cx),
|
||||||
edit_tool: Some(edit_tool),
|
edit_tool: Some(parts.edit_tool),
|
||||||
thread: Some(thread),
|
thread: Some(parts.thread),
|
||||||
partial_payloads,
|
partial_payloads,
|
||||||
final_payload,
|
final_payload,
|
||||||
expected_file_text: fixture.expected_file_text,
|
expected_file_text: fixture.expected_file_text,
|
||||||
|
editor: Some(parts.editor),
|
||||||
|
window: Some(parts.window),
|
||||||
|
keep_alive: parts.keep_alive,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HarnessParts {
|
||||||
|
edit_tool: Arc<EditFileTool>,
|
||||||
|
thread: Entity<Thread>,
|
||||||
|
editor: Entity<Editor>,
|
||||||
|
window: AnyWindowHandle,
|
||||||
|
keep_alive: Vec<Box<dyn Any>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a project + edit tool, opens the target buffer in an editor view inside
|
||||||
|
/// a window, and attaches a fake Rust language server. This mirrors the real app:
|
||||||
|
/// the edited file is open in a pane with a language server, so each buffer edit
|
||||||
|
/// drives the editor's observer cascade (matching brackets, code actions, outline,
|
||||||
|
/// bracket colorization), a tree-sitter reparse, and an LSP `didChange` +
|
||||||
|
/// diagnostics round-trip — the costs that dominate a real agent edit.
|
||||||
|
async fn setup_editor_and_tool(cx: &mut TestAppContext, file_text: String) -> HarnessParts {
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
fs.insert_tree(
|
||||||
|
"/root",
|
||||||
|
json!({
|
||||||
|
"src": {
|
||||||
|
"workspace_snapshot.rs": file_text,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let project = Project::test(fs, [Path::new("/root")], cx).await;
|
||||||
|
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
|
||||||
|
language_registry.add(rust_lang());
|
||||||
|
let mut fake_servers = language_registry.register_fake_lsp(
|
||||||
|
"Rust",
|
||||||
|
FakeLspAdapter {
|
||||||
|
capabilities: lsp::ServerCapabilities {
|
||||||
|
text_document_sync: Some(lsp::TextDocumentSyncCapability::Kind(
|
||||||
|
lsp::TextDocumentSyncKind::INCREMENTAL,
|
||||||
|
)),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let context_server_registry =
|
||||||
|
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||||
|
let model = Arc::new(FakeLanguageModel::default());
|
||||||
|
let thread = cx.new(|cx| {
|
||||||
|
Thread::new(
|
||||||
|
project.clone(),
|
||||||
|
cx.new(|_cx| ProjectContext::default()),
|
||||||
|
context_server_registry,
|
||||||
|
Templates::new(),
|
||||||
|
Some(model),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let action_log: Entity<ActionLog> =
|
||||||
|
thread.read_with(cx, |thread, _cx| thread.action_log().clone());
|
||||||
|
let edit_tool = Arc::new(EditFileTool::new(
|
||||||
|
project.clone(),
|
||||||
|
thread.downgrade(),
|
||||||
|
action_log,
|
||||||
|
language_registry,
|
||||||
|
));
|
||||||
|
|
||||||
|
// Open the same buffer the tool will edit and register it with the language
|
||||||
|
// servers so edits produce `didChange` notifications.
|
||||||
|
let buffer = project
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
project.open_local_buffer(FILE_ABS_PATH, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("failed to open buffer");
|
||||||
|
let lsp_handle = project.update(cx, |project, cx| {
|
||||||
|
project.register_buffer_with_language_servers(&buffer, cx)
|
||||||
|
});
|
||||||
|
|
||||||
|
let fake_server = fake_servers
|
||||||
|
.next()
|
||||||
|
.await
|
||||||
|
.expect("fake language server should start");
|
||||||
|
// Publish diagnostics on every edit, mirroring a real server reacting to
|
||||||
|
// `didChange`, so the editor's diagnostics path runs per edit.
|
||||||
|
let server = fake_server.clone();
|
||||||
|
fake_server.handle_notification::<lsp::notification::DidChangeTextDocument, _>(
|
||||||
|
move |params, _cx| {
|
||||||
|
server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
|
||||||
|
uri: params.text_document.uri.clone(),
|
||||||
|
version: Some(params.text_document.version),
|
||||||
|
diagnostics: vec![lsp::Diagnostic {
|
||||||
|
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 1)),
|
||||||
|
severity: Some(lsp::DiagnosticSeverity::WARNING),
|
||||||
|
message: "bench diagnostic".to_string(),
|
||||||
|
..Default::default()
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Attach an editor view in a window and lay it out once so the viewport-gated
|
||||||
|
// observers (bracket colorization, selection highlights) have a visible range.
|
||||||
|
let window = cx.add_window(|window, cx| {
|
||||||
|
let mut editor = Editor::for_buffer(buffer.clone(), Some(project.clone()), window, cx);
|
||||||
|
editor.set_style(EditorStyle::default(), window, cx);
|
||||||
|
window.focus(&editor.focus_handle(cx), cx);
|
||||||
|
editor
|
||||||
|
});
|
||||||
|
let editor = window.root(cx).expect("window should have an editor root");
|
||||||
|
let window: AnyWindowHandle = window.into();
|
||||||
|
// Lay out and paint a real frame so the editor establishes a viewport (this
|
||||||
|
// is what makes the viewport-gated observers like bracket colorization run).
|
||||||
|
{
|
||||||
|
let mut visual_cx = gpui::VisualTestContext::from_window(window, &*cx);
|
||||||
|
visual_cx.draw(
|
||||||
|
gpui::point(gpui::px(0.0), gpui::px(0.0)),
|
||||||
|
gpui::size(gpui::px(1024.0), gpui::px(768.0)),
|
||||||
|
|_, _| editor.clone().into_any_element(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let keep_alive: Vec<Box<dyn Any>> = vec![
|
||||||
|
Box::new(lsp_handle),
|
||||||
|
Box::new(fake_server),
|
||||||
|
Box::new(fake_servers),
|
||||||
|
Box::new(buffer),
|
||||||
|
];
|
||||||
|
|
||||||
|
HarnessParts {
|
||||||
|
edit_tool,
|
||||||
|
thread,
|
||||||
|
editor,
|
||||||
|
window,
|
||||||
|
keep_alive,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -135,6 +298,9 @@ fn init_context() -> TestAppContext {
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
let settings_store = SettingsStore::test(cx);
|
let settings_store = SettingsStore::test(cx);
|
||||||
cx.set_global(settings_store);
|
cx.set_global(settings_store);
|
||||||
|
assets::Assets.load_test_fonts(cx);
|
||||||
|
theme_settings::init(theme::LoadThemes::JustBase, cx);
|
||||||
|
editor::init(cx);
|
||||||
SettingsStore::update_global(cx, |store: &mut SettingsStore, cx| {
|
SettingsStore::update_global(cx, |store: &mut SettingsStore, cx| {
|
||||||
store.update_user_settings(cx, |settings| {
|
store.update_user_settings(cx, |settings| {
|
||||||
settings
|
settings
|
||||||
|
|
@ -142,6 +308,7 @@ fn init_context() -> TestAppContext {
|
||||||
.all_languages
|
.all_languages
|
||||||
.defaults
|
.defaults
|
||||||
.ensure_final_newline_on_save = Some(false);
|
.ensure_final_newline_on_save = Some(false);
|
||||||
|
settings.project.all_languages.defaults.colorize_brackets = Some(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -161,48 +328,6 @@ fn init_context() -> TestAppContext {
|
||||||
cx
|
cx
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn setup_edit_tool(
|
|
||||||
cx: &mut TestAppContext,
|
|
||||||
file_text: String,
|
|
||||||
) -> (Arc<EditFileTool>, Entity<Thread>) {
|
|
||||||
let fs = FakeFs::new(cx.executor());
|
|
||||||
fs.insert_tree(
|
|
||||||
"/root",
|
|
||||||
json!({
|
|
||||||
"src": {
|
|
||||||
"workspace_snapshot.rs": file_text,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let project = Project::test(fs, [Path::new("/root")], cx).await;
|
|
||||||
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
|
|
||||||
let context_server_registry =
|
|
||||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
|
||||||
let model = Arc::new(FakeLanguageModel::default());
|
|
||||||
let thread = cx.new(|cx| {
|
|
||||||
Thread::new(
|
|
||||||
project.clone(),
|
|
||||||
cx.new(|_cx| ProjectContext::default()),
|
|
||||||
context_server_registry,
|
|
||||||
Templates::new(),
|
|
||||||
Some(model),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
let action_log: Entity<ActionLog> =
|
|
||||||
thread.read_with(cx, |thread, _cx| thread.action_log().clone());
|
|
||||||
|
|
||||||
let edit_tool = Arc::new(EditFileTool::new(
|
|
||||||
project,
|
|
||||||
thread.downgrade(),
|
|
||||||
action_log,
|
|
||||||
language_registry,
|
|
||||||
));
|
|
||||||
(edit_tool, thread)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_streamed_edit(harness: &mut BenchmarkHarness) -> EditFileToolOutput {
|
fn run_streamed_edit(harness: &mut BenchmarkHarness) -> EditFileToolOutput {
|
||||||
let (mut sender, input): (_, ToolInput<EditFileToolInput>) = ToolInput::test();
|
let (mut sender, input): (_, ToolInput<EditFileToolInput>) = ToolInput::test();
|
||||||
for payload in &harness.partial_payloads {
|
for payload in &harness.partial_payloads {
|
||||||
|
|
@ -247,33 +372,36 @@ fn block_on_executor<R>(executor: &BackgroundExecutor, future: impl Future<Outpu
|
||||||
panic!("future did not complete while running edit_file_tool benchmark");
|
panic!("future did not complete while running edit_file_tool benchmark");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn streamed_partial_payloads(old_text: &str, new_text: &str) -> Vec<Value> {
|
/// Builds the streamed partial payloads for a (possibly multi-edit) session,
|
||||||
let path = "root/src/workspace_snapshot.rs";
|
/// mirroring how the agent reveals one edit at a time: earlier edits stay
|
||||||
let mut payloads = Vec::new();
|
/// complete in the array while the current edit streams its `old_text` then its
|
||||||
|
/// `new_text` in chunks.
|
||||||
|
fn streamed_partial_payloads(edits: &[EditOp]) -> Vec<Value> {
|
||||||
|
let path = FILE_PROJECT_PATH;
|
||||||
|
let mut payloads = vec![json!({ "path": path }), json!({ "path": path })];
|
||||||
|
|
||||||
payloads.push(json!({ "path": path }));
|
for index in 0..edits.len() {
|
||||||
payloads.push(json!({ "path": path }));
|
let completed: Vec<Value> = edits[..index]
|
||||||
|
.iter()
|
||||||
|
.map(|edit| json!({ "old_text": edit.old_text, "new_text": edit.new_text }))
|
||||||
|
.collect();
|
||||||
|
let edit = &edits[index];
|
||||||
|
|
||||||
for old_end in chunk_ends(old_text, OLD_TEXT_CHUNK_SIZE) {
|
for old_end in chunk_ends(&edit.old_text, OLD_TEXT_CHUNK_SIZE) {
|
||||||
payloads.push(json!({
|
let mut arr = completed.clone();
|
||||||
"path": path,
|
arr.push(json!({ "old_text": &edit.old_text[..old_end] }));
|
||||||
"edits": [{ "old_text": &old_text[..old_end] }],
|
payloads.push(json!({ "path": path, "edits": arr }));
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
payloads.push(json!({
|
let mut arr = completed.clone();
|
||||||
"path": path,
|
arr.push(json!({ "old_text": edit.old_text, "new_text": "" }));
|
||||||
"edits": [{ "old_text": old_text, "new_text": "" }],
|
payloads.push(json!({ "path": path, "edits": arr }));
|
||||||
}));
|
|
||||||
|
|
||||||
for new_end in chunk_ends(new_text, NEW_TEXT_CHUNK_SIZE) {
|
for new_end in chunk_ends(&edit.new_text, NEW_TEXT_CHUNK_SIZE) {
|
||||||
payloads.push(json!({
|
let mut arr = completed.clone();
|
||||||
"path": path,
|
arr.push(json!({ "old_text": edit.old_text, "new_text": &edit.new_text[..new_end] }));
|
||||||
"edits": [{
|
payloads.push(json!({ "path": path, "edits": arr }));
|
||||||
"old_text": old_text,
|
}
|
||||||
"new_text": &new_text[..new_end],
|
|
||||||
}],
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
payloads
|
payloads
|
||||||
|
|
@ -326,6 +454,7 @@ fn fixtures() -> Vec<EditFixture> {
|
||||||
EditPattern::InsertHelperBlocks { every_nth_line: 9 },
|
EditPattern::InsertHelperBlocks { every_nth_line: 9 },
|
||||||
SEED + 3,
|
SEED + 3,
|
||||||
),
|
),
|
||||||
|
make_large_multi_edit_fixture("large_multi_edit", 80, 16, SEED + 4),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -375,11 +504,106 @@ fn make_fixture(
|
||||||
name,
|
name,
|
||||||
old_file_text,
|
old_file_text,
|
||||||
expected_file_text,
|
expected_file_text,
|
||||||
old_text,
|
edits: vec![EditOp { old_text, new_text }],
|
||||||
new_text,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn make_large_multi_edit_fixture(
|
||||||
|
name: &'static str,
|
||||||
|
function_count: usize,
|
||||||
|
edit_count: usize,
|
||||||
|
seed: u64,
|
||||||
|
) -> EditFixture {
|
||||||
|
const HEADER_LINES: usize = 10;
|
||||||
|
const FUNCTION_LINES: usize = 12;
|
||||||
|
const FUNCTION_BODY_LINES: usize = 11;
|
||||||
|
|
||||||
|
let mut rng = StdRng::seed_from_u64(seed);
|
||||||
|
let old_lines = random_rust_module(&mut rng, function_count);
|
||||||
|
let old_file_text = old_lines.join("\n");
|
||||||
|
|
||||||
|
let step = (function_count / edit_count).max(1);
|
||||||
|
let mut picks: Vec<usize> = (0..edit_count)
|
||||||
|
.map(|k| (k * step).min(function_count - 1))
|
||||||
|
.collect();
|
||||||
|
picks.dedup();
|
||||||
|
|
||||||
|
let replacements: Vec<(usize, Vec<String>)> = picks
|
||||||
|
.iter()
|
||||||
|
.map(|&function_index| {
|
||||||
|
(
|
||||||
|
function_index,
|
||||||
|
large_function_lines(&mut rng, function_index),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let edits = replacements
|
||||||
|
.iter()
|
||||||
|
.map(|(function_index, new_function)| {
|
||||||
|
let start = HEADER_LINES + function_index * FUNCTION_LINES;
|
||||||
|
let end = start + FUNCTION_BODY_LINES;
|
||||||
|
EditOp {
|
||||||
|
old_text: old_lines[start..end].join("\n"),
|
||||||
|
new_text: new_function.join("\n"),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut new_lines = old_lines;
|
||||||
|
for (function_index, new_function) in replacements.iter().rev() {
|
||||||
|
let start = HEADER_LINES + function_index * FUNCTION_LINES;
|
||||||
|
let end = start + FUNCTION_BODY_LINES;
|
||||||
|
new_lines.splice(start..end, new_function.iter().cloned());
|
||||||
|
}
|
||||||
|
let expected_file_text = new_lines.join("\n");
|
||||||
|
|
||||||
|
EditFixture {
|
||||||
|
name,
|
||||||
|
old_file_text,
|
||||||
|
expected_file_text,
|
||||||
|
edits,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn large_function_lines(rng: &mut StdRng, index: usize) -> Vec<String> {
|
||||||
|
let function_name = identifier(rng, index + 40_000);
|
||||||
|
let argument_name = identifier(rng, index + 41_000);
|
||||||
|
|
||||||
|
let mut lines = vec![
|
||||||
|
format!(
|
||||||
|
" pub fn {function_name}(&mut self, {argument_name}: usize) -> Result<usize> {{"
|
||||||
|
),
|
||||||
|
format!(" let mut accumulator = {argument_name};"),
|
||||||
|
];
|
||||||
|
|
||||||
|
let body_lines = rng.random_range(30..42);
|
||||||
|
for body_index in 0..body_lines {
|
||||||
|
let local_name = identifier(rng, index + 50_000 + body_index);
|
||||||
|
let multiplier = rng.random_range(2..19);
|
||||||
|
let offset = rng.random_range(1..256);
|
||||||
|
match body_index % 4 {
|
||||||
|
0 => lines.push(format!(
|
||||||
|
" let {local_name} = accumulator.saturating_mul({multiplier}).saturating_add({offset});"
|
||||||
|
)),
|
||||||
|
1 => lines.push(format!(
|
||||||
|
" accumulator = {local_name}.saturating_sub(self.version % {offset}.max(1));"
|
||||||
|
)),
|
||||||
|
2 => lines.push(format!(
|
||||||
|
" if {local_name} % {multiplier} == 0 {{ accumulator = accumulator.saturating_add({local_name}); }}"
|
||||||
|
)),
|
||||||
|
_ => lines.push(format!(
|
||||||
|
" self.buffers.insert(\"{local_name}\".to_string(), accumulator);"
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(" self.version = self.version.saturating_add(accumulator);".to_string());
|
||||||
|
lines.push(" Ok(accumulator)".to_string());
|
||||||
|
lines.push(" }".to_string());
|
||||||
|
lines
|
||||||
|
}
|
||||||
|
|
||||||
fn edit_range(lines: &[String], pattern: &EditPattern) -> std::ops::Range<usize> {
|
fn edit_range(lines: &[String], pattern: &EditPattern) -> std::ops::Range<usize> {
|
||||||
let mut range = match pattern {
|
let mut range = match pattern {
|
||||||
EditPattern::LocalizedRewrite {
|
EditPattern::LocalizedRewrite {
|
||||||
|
|
|
||||||
|
|
@ -316,17 +316,6 @@ impl UserMessage {
|
||||||
MentionUri::Thread { .. } => {
|
MentionUri::Thread { .. } => {
|
||||||
write!(&mut thread_context, "\n{}\n", content).ok();
|
write!(&mut thread_context, "\n{}\n", content).ok();
|
||||||
}
|
}
|
||||||
MentionUri::Rule { .. } => {
|
|
||||||
write!(
|
|
||||||
&mut rules_context,
|
|
||||||
"\n{}",
|
|
||||||
MarkdownCodeBlock {
|
|
||||||
tag: "",
|
|
||||||
text: content
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
MentionUri::Fetch { url } => {
|
MentionUri::Fetch { url } => {
|
||||||
write!(&mut fetch_context, "\nFetch: {}\n\n{}", url, content).ok();
|
write!(&mut fetch_context, "\nFetch: {}\n\n{}", url, content).ok();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ use collections::HashSet;
|
||||||
use futures::{FutureExt, channel::oneshot};
|
use futures::{FutureExt, channel::oneshot};
|
||||||
use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity};
|
use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity};
|
||||||
use language::language_settings::{self, FormatOnSave};
|
use language::language_settings::{self, FormatOnSave};
|
||||||
use language::{Buffer, BufferEvent, LanguageRegistry};
|
use language::{Buffer, BufferEditSource, BufferEvent, LanguageRegistry};
|
||||||
use language_model::LanguageModelToolResultContent;
|
use language_model::LanguageModelToolResultContent;
|
||||||
use project::lsp_store::{FormatTrigger, LspFormatTarget};
|
use project::lsp_store::{FormatTrigger, LspFormatTarget};
|
||||||
use project::{AgentLocation, Project, ProjectPath};
|
use project::{AgentLocation, Project, ProjectPath};
|
||||||
|
|
@ -620,8 +620,12 @@ impl EditPipeline {
|
||||||
|
|
||||||
log::debug!("new_text_chunk: done=true, final_text='{}'", final_text);
|
log::debug!("new_text_chunk: done=true, final_text='{}'", final_text);
|
||||||
|
|
||||||
if !final_text.is_empty() {
|
let mut char_ops = if final_text.is_empty() {
|
||||||
let char_ops = streaming_diff.push_new(&final_text);
|
Vec::new()
|
||||||
|
} else {
|
||||||
|
streaming_diff.push_new(&final_text)
|
||||||
|
};
|
||||||
|
char_ops.extend(streaming_diff.finish());
|
||||||
apply_char_operations(
|
apply_char_operations(
|
||||||
&char_ops,
|
&char_ops,
|
||||||
buffer,
|
buffer,
|
||||||
|
|
@ -630,17 +634,6 @@ impl EditPipeline {
|
||||||
&context.action_log,
|
&context.action_log,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
let remaining_ops = streaming_diff.finish();
|
|
||||||
apply_char_operations(
|
|
||||||
&remaining_ops,
|
|
||||||
buffer,
|
|
||||||
&original_snapshot,
|
|
||||||
&mut edit_cursor,
|
|
||||||
&context.action_log,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
|
|
||||||
let position = original_snapshot.anchor_before(edit_cursor);
|
let position = original_snapshot.anchor_before(edit_cursor);
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
|
|
@ -902,16 +895,17 @@ fn apply_char_operations(
|
||||||
action_log: &Entity<ActionLog>,
|
action_log: &Entity<ActionLog>,
|
||||||
cx: &mut AsyncApp,
|
cx: &mut AsyncApp,
|
||||||
) {
|
) {
|
||||||
|
let mut edits: Vec<_> = Vec::new();
|
||||||
for op in ops {
|
for op in ops {
|
||||||
match op {
|
match op {
|
||||||
CharOperation::Insert { text } => {
|
CharOperation::Insert { text } => {
|
||||||
let anchor = snapshot.anchor_after(*edit_cursor);
|
let anchor = snapshot.anchor_after(*edit_cursor);
|
||||||
agent_edit_buffer(&buffer, [(anchor..anchor, text.as_str())], action_log, cx);
|
edits.push((anchor..anchor, text.as_str().into()));
|
||||||
}
|
}
|
||||||
CharOperation::Delete { bytes } => {
|
CharOperation::Delete { bytes } => {
|
||||||
let delete_end = *edit_cursor + bytes;
|
let delete_end = *edit_cursor + bytes;
|
||||||
let anchor_range = snapshot.anchor_range_inside(*edit_cursor..delete_end);
|
let anchor_range = snapshot.anchor_range_inside(*edit_cursor..delete_end);
|
||||||
agent_edit_buffer(&buffer, [(anchor_range, "")], action_log, cx);
|
edits.push((anchor_range, Arc::<str>::from("")));
|
||||||
*edit_cursor = delete_end;
|
*edit_cursor = delete_end;
|
||||||
}
|
}
|
||||||
CharOperation::Keep { bytes } => {
|
CharOperation::Keep { bytes } => {
|
||||||
|
|
@ -919,6 +913,9 @@ fn apply_char_operations(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !edits.is_empty() {
|
||||||
|
agent_edit_buffer(buffer, edits, action_log, cx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extract_match(
|
fn extract_match(
|
||||||
|
|
@ -975,7 +972,9 @@ fn agent_edit_buffer<I, S, T>(
|
||||||
{
|
{
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
buffer.update(cx, |buffer, cx| {
|
buffer.update(cx, |buffer, cx| {
|
||||||
|
buffer.start_transaction();
|
||||||
buffer.edit(edits, None, cx);
|
buffer.edit(edits, None, cx);
|
||||||
|
buffer.end_transaction_with_source(BufferEditSource::Agent, cx);
|
||||||
});
|
});
|
||||||
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
|
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ path = "agent_skills.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
base64.workspace = true
|
||||||
const_format.workspace = true
|
const_format.workspace = true
|
||||||
fs.workspace = true
|
fs.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
|
|
@ -20,6 +21,7 @@ gpui.workspace = true
|
||||||
paths.workspace = true
|
paths.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_yaml_ng.workspace = true
|
serde_yaml_ng.workspace = true
|
||||||
|
url.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ use gpui::{Global, SharedString};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use url::Url;
|
||||||
use util::paths::component_matches_ignore_ascii_case;
|
use util::paths::component_matches_ignore_ascii_case;
|
||||||
|
|
||||||
/// First segment of the skills directory path: `.agents`.
|
/// First segment of the skills directory path: `.agents`.
|
||||||
|
|
@ -731,6 +732,58 @@ pub fn is_agents_skills_path(path: &Path) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The `zed://` scheme used by share links.
|
||||||
|
const SKILL_SHARE_LINK_SCHEME: &str = "zed";
|
||||||
|
/// The host (the part after `zed://`) that identifies a skill share link.
|
||||||
|
const SKILL_SHARE_LINK_HOST: &str = "skill";
|
||||||
|
/// The query parameter that carries the embedded `SKILL.md` payload.
|
||||||
|
const SKILL_SHARE_LINK_DATA_PARAM: &str = "data";
|
||||||
|
|
||||||
|
/// The `zed://` deep-link prefix for a shared skill. Opening a link with this
|
||||||
|
/// prefix prompts the recipient to review and install the embedded skill.
|
||||||
|
pub const SKILL_SHARE_LINK_PREFIX: &str =
|
||||||
|
concatcp!(SKILL_SHARE_LINK_SCHEME, "://", SKILL_SHARE_LINK_HOST);
|
||||||
|
|
||||||
|
/// Build a shareable `zed://skill?data=…` link that fully embeds the given
|
||||||
|
/// `SKILL.md` file contents.
|
||||||
|
///
|
||||||
|
/// The contents are base64url-encoded (no padding) so the link is
|
||||||
|
/// self-contained and URL-safe: the recipient doesn't need the skill to be
|
||||||
|
/// hosted anywhere. Recover the contents with [`decode_skill_share_link`].
|
||||||
|
pub fn encode_skill_share_link(skill_file_content: &str) -> String {
|
||||||
|
use base64::Engine as _;
|
||||||
|
let data =
|
||||||
|
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(skill_file_content.as_bytes());
|
||||||
|
let mut url = Url::parse(SKILL_SHARE_LINK_PREFIX).expect("skill share link prefix is valid");
|
||||||
|
url.query_pairs_mut()
|
||||||
|
.append_pair(SKILL_SHARE_LINK_DATA_PARAM, &data);
|
||||||
|
url.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recover the `SKILL.md` contents embedded in a `zed://skill?data=…` link
|
||||||
|
/// produced by [`encode_skill_share_link`].
|
||||||
|
pub fn decode_skill_share_link(link: &str) -> Result<String> {
|
||||||
|
use base64::Engine as _;
|
||||||
|
let url = Url::parse(link).context("skill share link is not a valid URL")?;
|
||||||
|
anyhow::ensure!(
|
||||||
|
url.scheme() == SKILL_SHARE_LINK_SCHEME && url.host_str() == Some(SKILL_SHARE_LINK_HOST),
|
||||||
|
"not a skill share link"
|
||||||
|
);
|
||||||
|
let data = url
|
||||||
|
.query_pairs()
|
||||||
|
.find_map(|(key, value)| (key == SKILL_SHARE_LINK_DATA_PARAM).then_some(value))
|
||||||
|
.context("skill share link is missing the `data` parameter")?;
|
||||||
|
let bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
|
||||||
|
.decode(data.as_bytes())
|
||||||
|
.context("skill share link `data` is not valid base64")?;
|
||||||
|
anyhow::ensure!(
|
||||||
|
bytes.len() <= MAX_SKILL_FILE_SIZE,
|
||||||
|
"shared skill exceeds the maximum size of {MAX_SKILL_FILE_SIZE} bytes"
|
||||||
|
);
|
||||||
|
let content = String::from_utf8(bytes).context("skill share link `data` is not valid UTF-8")?;
|
||||||
|
Ok(content)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
@ -1959,4 +2012,25 @@ description: A skill with no body content
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn skill_share_link_round_trips() {
|
||||||
|
let content =
|
||||||
|
"---\nname: my-skill\ndescription: Does a thing.\n---\n\n## Steps\n\nDo the thing.\n";
|
||||||
|
let link = encode_skill_share_link(content);
|
||||||
|
let data = link
|
||||||
|
.strip_prefix("zed://skill?data=")
|
||||||
|
.expect("link should start with the skill share prefix");
|
||||||
|
// base64url (no-pad) output must not require percent-encoding.
|
||||||
|
assert!(!data.contains('+') && !data.contains('/') && !data.contains('='));
|
||||||
|
assert_eq!(decode_skill_share_link(&link).unwrap(), content);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decode_skill_share_link_rejects_non_skill_links() {
|
||||||
|
assert!(decode_skill_share_link("zed://settings/agent.skills").is_err());
|
||||||
|
assert!(decode_skill_share_link("zed://skill").is_err());
|
||||||
|
assert!(decode_skill_share_link("zed://skill?other=1").is_err());
|
||||||
|
assert!(decode_skill_share_link("zed://skill?data=!!!notbase64").is_err());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,6 @@ release_channel.workspace = true
|
||||||
remote.workspace = true
|
remote.workspace = true
|
||||||
remote_connection.workspace = true
|
remote_connection.workspace = true
|
||||||
rope.workspace = true
|
rope.workspace = true
|
||||||
rules_library.workspace = true
|
|
||||||
skill_creator.workspace = true
|
skill_creator.workspace = true
|
||||||
schemars.workspace = true
|
schemars.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,10 @@ use crate::ExpandMessageEditor;
|
||||||
use crate::ManageProfiles;
|
use crate::ManageProfiles;
|
||||||
use crate::agent_connection_store::AgentConnectionStore;
|
use crate::agent_connection_store::AgentConnectionStore;
|
||||||
use crate::completion_provider::AgentContextSource;
|
use crate::completion_provider::AgentContextSource;
|
||||||
use crate::terminal_thread_metadata_store::{TerminalThreadMetadata, TerminalThreadMetadataStore};
|
use crate::terminal_thread_metadata_store::{
|
||||||
|
TerminalThreadMetadata, TerminalThreadMetadataStore, compose_terminal_thread_title,
|
||||||
|
terminal_title_without_prefix,
|
||||||
|
};
|
||||||
use crate::thread_metadata_store::{ThreadId, ThreadMetadataStore, ThreadMetadataStoreEvent};
|
use crate::thread_metadata_store::{ThreadId, ThreadMetadataStore, ThreadMetadataStoreEvent};
|
||||||
use crate::{
|
use crate::{
|
||||||
AddContextServer, AgentDiffPane, ConversationView, CopyThreadToClipboard, Follow,
|
AddContextServer, AgentDiffPane, ConversationView, CopyThreadToClipboard, Follow,
|
||||||
|
|
@ -75,7 +78,6 @@ use gpui::{
|
||||||
use language::LanguageRegistry;
|
use language::LanguageRegistry;
|
||||||
use language_model::LanguageModelRegistry;
|
use language_model::LanguageModelRegistry;
|
||||||
use project::{Project, ProjectPath, Worktree};
|
use project::{Project, ProjectPath, Worktree};
|
||||||
use prompt_store::PromptStore;
|
|
||||||
use settings::TerminalDockPosition;
|
use settings::TerminalDockPosition;
|
||||||
use settings::{NotifyWhenAgentWaiting, Settings, update_settings_file};
|
use settings::{NotifyWhenAgentWaiting, Settings, update_settings_file};
|
||||||
use skill_creator::{SkillCreatorOpenMode, is_supported_skill_url, open_skill_creator};
|
use skill_creator::{SkillCreatorOpenMode, is_supported_skill_url, open_skill_creator};
|
||||||
|
|
@ -870,6 +872,7 @@ struct AgentTerminal {
|
||||||
title_editor_initial_title: Option<String>,
|
title_editor_initial_title: Option<String>,
|
||||||
title_editor_subscription: Option<Subscription>,
|
title_editor_subscription: Option<Subscription>,
|
||||||
last_known_title: String,
|
last_known_title: String,
|
||||||
|
last_known_terminal_title: String,
|
||||||
last_observed_program: Option<String>,
|
last_observed_program: Option<String>,
|
||||||
working_directory: Option<PathBuf>,
|
working_directory: Option<PathBuf>,
|
||||||
created_at: DateTime<Utc>,
|
created_at: DateTime<Utc>,
|
||||||
|
|
@ -880,11 +883,7 @@ struct AgentTerminal {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AgentTerminal {
|
impl AgentTerminal {
|
||||||
fn title(&self, cx: &App) -> SharedString {
|
fn terminal_title_for_view(view: &TerminalView, cx: &App) -> SharedString {
|
||||||
let view = self.view.read(cx);
|
|
||||||
let title = if let Some(custom_title) = view.custom_title() {
|
|
||||||
SharedString::from(custom_title)
|
|
||||||
} else {
|
|
||||||
let terminal = view.terminal().read(cx);
|
let terminal = view.terminal().read(cx);
|
||||||
if terminal.breadcrumb_text.is_empty() {
|
if terminal.breadcrumb_text.is_empty() {
|
||||||
let title = terminal.title(true);
|
let title = terminal.title(true);
|
||||||
|
|
@ -896,16 +895,46 @@ impl AgentTerminal {
|
||||||
} else {
|
} else {
|
||||||
terminal.breadcrumb_text.clone().into()
|
terminal.breadcrumb_text.clone().into()
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
if title.is_empty() && !self.last_known_title.is_empty() {
|
fn current_terminal_title(&self, cx: &App) -> SharedString {
|
||||||
SharedString::from(self.last_known_title.clone())
|
let view = self.view.read(cx);
|
||||||
|
Self::terminal_title_for_view(view, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn terminal_title(&self, cx: &App) -> SharedString {
|
||||||
|
let title = self.current_terminal_title(cx);
|
||||||
|
if title.is_empty() && !self.last_known_terminal_title.is_empty() {
|
||||||
|
SharedString::from(self.last_known_terminal_title.clone())
|
||||||
} else {
|
} else {
|
||||||
title
|
title
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn title(&self, cx: &App) -> SharedString {
|
||||||
|
let terminal_title = self.terminal_title(cx);
|
||||||
|
let custom_title = self.custom_title(cx);
|
||||||
|
compose_terminal_thread_title(
|
||||||
|
terminal_title.as_ref(),
|
||||||
|
custom_title.as_ref().map(|title| title.as_ref()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn editable_title(&self, cx: &App) -> SharedString {
|
||||||
|
if let Some(custom_title) = self.custom_title(cx) {
|
||||||
|
custom_title
|
||||||
|
} else {
|
||||||
|
let terminal_title = self.terminal_title(cx);
|
||||||
|
SharedString::from(terminal_title_without_prefix(terminal_title.as_ref()).to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn refresh_title(&mut self, cx: &mut App) -> bool {
|
fn refresh_title(&mut self, cx: &mut App) -> bool {
|
||||||
|
let terminal_title = self.current_terminal_title(cx);
|
||||||
|
if !terminal_title.is_empty() {
|
||||||
|
self.last_known_terminal_title = terminal_title.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
let title = self.title(cx);
|
let title = self.title(cx);
|
||||||
let changed = self.last_known_title != title.as_ref();
|
let changed = self.last_known_title != title.as_ref();
|
||||||
if changed {
|
if changed {
|
||||||
|
|
@ -1019,7 +1048,6 @@ pub struct AgentPanel {
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
language_registry: Arc<LanguageRegistry>,
|
language_registry: Arc<LanguageRegistry>,
|
||||||
thread_store: Entity<ThreadStore>,
|
thread_store: Entity<ThreadStore>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
|
||||||
connection_store: Entity<AgentConnectionStore>,
|
connection_store: Entity<AgentConnectionStore>,
|
||||||
context_server_registry: Entity<ContextServerRegistry>,
|
context_server_registry: Entity<ContextServerRegistry>,
|
||||||
configuration: Option<Entity<AgentConfiguration>>,
|
configuration: Option<Entity<AgentConfiguration>>,
|
||||||
|
|
@ -1140,13 +1168,8 @@ impl AgentPanel {
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
mut cx: AsyncWindowContext,
|
mut cx: AsyncWindowContext,
|
||||||
) -> Task<Result<Entity<Self>>> {
|
) -> Task<Result<Entity<Self>>> {
|
||||||
let prompt_store = cx.update(|_window, cx| PromptStore::global(cx));
|
|
||||||
let kvp = cx.update(|_window, cx| KeyValueStore::global(cx)).ok();
|
let kvp = cx.update(|_window, cx| KeyValueStore::global(cx)).ok();
|
||||||
cx.spawn(async move |cx| {
|
cx.spawn(async move |cx| {
|
||||||
let prompt_store = match prompt_store {
|
|
||||||
Ok(prompt_store) => prompt_store.await.ok(),
|
|
||||||
Err(_) => None,
|
|
||||||
};
|
|
||||||
let workspace_id = workspace
|
let workspace_id = workspace
|
||||||
.read_with(cx, |workspace, _| workspace.database_id())
|
.read_with(cx, |workspace, _| workspace.database_id())
|
||||||
.ok()
|
.ok()
|
||||||
|
|
@ -1271,7 +1294,7 @@ impl AgentPanel {
|
||||||
};
|
};
|
||||||
|
|
||||||
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| Self::new(workspace, prompt_store, window, cx));
|
let panel = cx.new(|cx| Self::new(workspace, window, cx));
|
||||||
|
|
||||||
panel.update(cx, |panel, cx| {
|
panel.update(cx, |panel, cx| {
|
||||||
let is_via_collab = panel.project.read(cx).is_via_collab();
|
let is_via_collab = panel.project.read(cx).is_via_collab();
|
||||||
|
|
@ -1351,12 +1374,7 @@ impl AgentPanel {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn new(
|
pub(crate) fn new(workspace: &Workspace, _window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
workspace: &Workspace,
|
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
|
||||||
_window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> Self {
|
|
||||||
let fs = workspace.app_state().fs.clone();
|
let fs = workspace.app_state().fs.clone();
|
||||||
let user_store = workspace.app_state().user_store.clone();
|
let user_store = workspace.app_state().user_store.clone();
|
||||||
let project = workspace.project();
|
let project = workspace.project();
|
||||||
|
|
@ -1438,7 +1456,6 @@ impl AgentPanel {
|
||||||
project: project.clone(),
|
project: project.clone(),
|
||||||
fs: fs.clone(),
|
fs: fs.clone(),
|
||||||
language_registry,
|
language_registry,
|
||||||
prompt_store,
|
|
||||||
connection_store,
|
connection_store,
|
||||||
configuration: None,
|
configuration: None,
|
||||||
configuration_subscription: None,
|
configuration_subscription: None,
|
||||||
|
|
@ -1517,10 +1534,6 @@ impl AgentPanel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn prompt_store(&self) -> &Option<Entity<PromptStore>> {
|
|
||||||
&self.prompt_store
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn thread_store(&self) -> &Entity<ThreadStore> {
|
pub fn thread_store(&self) -> &Entity<ThreadStore> {
|
||||||
&self.thread_store
|
&self.thread_store
|
||||||
}
|
}
|
||||||
|
|
@ -1981,14 +1994,16 @@ impl AgentPanel {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let last_known_terminal_title = initial_title
|
||||||
|
.map(|title| title.to_string())
|
||||||
|
.unwrap_or_default();
|
||||||
let mut terminal = AgentTerminal {
|
let mut terminal = AgentTerminal {
|
||||||
view: terminal_view,
|
view: terminal_view,
|
||||||
title_editor: None,
|
title_editor: None,
|
||||||
title_editor_initial_title: None,
|
title_editor_initial_title: None,
|
||||||
title_editor_subscription: None,
|
title_editor_subscription: None,
|
||||||
last_known_title: initial_title
|
last_known_title: last_known_terminal_title.clone(),
|
||||||
.map(|title| title.to_string())
|
last_known_terminal_title,
|
||||||
.unwrap_or_default(),
|
|
||||||
last_observed_program: None,
|
last_observed_program: None,
|
||||||
working_directory,
|
working_directory,
|
||||||
created_at: created_at.unwrap_or_else(Utc::now),
|
created_at: created_at.unwrap_or_else(Utc::now),
|
||||||
|
|
@ -2164,7 +2179,7 @@ impl AgentPanel {
|
||||||
let project = self.project.read(cx);
|
let project = self.project.read(cx);
|
||||||
Some(TerminalThreadMetadata {
|
Some(TerminalThreadMetadata {
|
||||||
terminal_id,
|
terminal_id,
|
||||||
title: terminal.title(cx),
|
title: terminal.terminal_title(cx),
|
||||||
custom_title: terminal.custom_title(cx),
|
custom_title: terminal.custom_title(cx),
|
||||||
created_at: terminal.created_at,
|
created_at: terminal.created_at,
|
||||||
worktree_paths: project.worktree_paths(cx),
|
worktree_paths: project.worktree_paths(cx),
|
||||||
|
|
@ -2242,10 +2257,7 @@ impl AgentPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn terminal_restore_initial_title(metadata: &TerminalThreadMetadata) -> Option<SharedString> {
|
fn terminal_restore_initial_title(metadata: &TerminalThreadMetadata) -> Option<SharedString> {
|
||||||
metadata
|
(!metadata.title.is_empty()).then(|| metadata.title.clone())
|
||||||
.custom_title
|
|
||||||
.clone()
|
|
||||||
.or_else(|| (!metadata.title.is_empty()).then(|| metadata.title.clone()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn edit_terminal_title(
|
fn edit_terminal_title(
|
||||||
|
|
@ -2263,7 +2275,7 @@ impl AgentPanel {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let title = terminal.title(cx).to_string();
|
let title = terminal.editable_title(cx).to_string();
|
||||||
let title_editor_initial_title = title.clone();
|
let title_editor_initial_title = title.clone();
|
||||||
let title_editor = cx.new(|cx| {
|
let title_editor = cx.new(|cx| {
|
||||||
let mut editor = Editor::single_line(window, cx);
|
let mut editor = Editor::single_line(window, cx);
|
||||||
|
|
@ -2331,7 +2343,7 @@ impl AgentPanel {
|
||||||
if !title_editor.read(cx).is_focused(window) {
|
if !title_editor.read(cx).is_focused(window) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let Some((terminal_view, initial_title)) =
|
let Some((terminal_view, initial_title, terminal_title)) =
|
||||||
self.terminals.get(&terminal_id).and_then(|terminal| {
|
self.terminals.get(&terminal_id).and_then(|terminal| {
|
||||||
terminal
|
terminal
|
||||||
.title_editor
|
.title_editor
|
||||||
|
|
@ -2341,25 +2353,23 @@ impl AgentPanel {
|
||||||
(
|
(
|
||||||
terminal.view.clone(),
|
terminal.view.clone(),
|
||||||
terminal.title_editor_initial_title.clone(),
|
terminal.title_editor_initial_title.clone(),
|
||||||
|
terminal.terminal_title(cx),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let new_title = title_editor.read(cx).text(cx).trim().to_string();
|
let new_title = title_editor.read(cx).text(cx);
|
||||||
if initial_title.as_deref().map(str::trim) == Some(new_title.as_str()) {
|
if initial_title.as_deref() == Some(new_title.as_str()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let label = if new_title.is_empty() {
|
let label = if new_title.trim().is_empty()
|
||||||
None
|
|| new_title == terminal_title_without_prefix(terminal_title.as_ref())
|
||||||
} else {
|
{
|
||||||
let terminal_title = terminal_view.read(cx).terminal().read(cx).title(true);
|
|
||||||
if new_title == terminal_title {
|
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(new_title)
|
Some(new_title)
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
cx.defer(move |cx| {
|
cx.defer(move |cx| {
|
||||||
|
|
@ -3251,6 +3261,13 @@ impl AgentPanel {
|
||||||
self.open_skill_creator(SkillCreatorOpenMode::Url { initial_url }, cx);
|
self.open_skill_creator(SkillCreatorOpenMode::Url { initial_url }, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Open the skill creator pre-filled with a skill received from a
|
||||||
|
/// `zed://skill` share link, so the user can review it and choose a scope
|
||||||
|
/// before installing.
|
||||||
|
pub fn install_shared_skill(&mut self, content: String, cx: &mut Context<Self>) {
|
||||||
|
self.open_skill_creator(SkillCreatorOpenMode::Install { content }, cx);
|
||||||
|
}
|
||||||
|
|
||||||
fn open_skill_creator(&mut self, open_mode: SkillCreatorOpenMode, cx: &mut Context<Self>) {
|
fn open_skill_creator(&mut self, open_mode: SkillCreatorOpenMode, cx: &mut Context<Self>) {
|
||||||
let this = cx.weak_entity();
|
let this = cx.weak_entity();
|
||||||
let on_saved: Rc<dyn Fn(&mut App)> = Rc::new(move |cx: &mut App| {
|
let on_saved: Rc<dyn Fn(&mut App)> = Rc::new(move |cx: &mut App| {
|
||||||
|
|
@ -4361,7 +4378,6 @@ impl AgentPanel {
|
||||||
workspace.clone(),
|
workspace.clone(),
|
||||||
project,
|
project,
|
||||||
thread_store,
|
thread_store,
|
||||||
self.prompt_store.clone(),
|
|
||||||
source,
|
source,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
|
@ -6053,7 +6069,7 @@ impl Dismissable for TrialEndUpsell {
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
impl AgentPanel {
|
impl AgentPanel {
|
||||||
pub fn test_new(workspace: &Workspace, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
pub fn test_new(workspace: &Workspace, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
Self::new(workspace, None, window, cx)
|
Self::new(workspace, window, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Drops a thread's `ConversationView` from `retained_threads` without
|
/// Drops a thread's `ConversationView` from `retained_threads` without
|
||||||
|
|
@ -6560,7 +6576,7 @@ mod tests {
|
||||||
|
|
||||||
// Set up workspace A: with an active thread.
|
// Set up workspace A: with an active thread.
|
||||||
let panel_a = workspace_a.update_in(cx, |workspace, window, cx| {
|
let panel_a = workspace_a.update_in(cx, |workspace, window, cx| {
|
||||||
cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
|
cx.new(|cx| AgentPanel::new(workspace, window, cx))
|
||||||
});
|
});
|
||||||
|
|
||||||
panel_a.update_in(cx, |panel, window, cx| {
|
panel_a.update_in(cx, |panel, window, cx| {
|
||||||
|
|
@ -6586,7 +6602,7 @@ mod tests {
|
||||||
|
|
||||||
// Set up workspace B: ClaudeCode, no active thread.
|
// Set up workspace B: ClaudeCode, no active thread.
|
||||||
let panel_b = workspace_b.update_in(cx, |workspace, window, cx| {
|
let panel_b = workspace_b.update_in(cx, |workspace, window, cx| {
|
||||||
cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
|
cx.new(|cx| AgentPanel::new(workspace, window, cx))
|
||||||
});
|
});
|
||||||
|
|
||||||
panel_b.update(cx, |panel, _cx| {
|
panel_b.update(cx, |panel, _cx| {
|
||||||
|
|
@ -6689,7 +6705,7 @@ mod tests {
|
||||||
|
|
||||||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||||
cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
|
cx.new(|cx| AgentPanel::new(workspace, window, cx))
|
||||||
});
|
});
|
||||||
|
|
||||||
panel.update_in(cx, |panel, window, cx| {
|
panel.update_in(cx, |panel, window, cx| {
|
||||||
|
|
@ -6884,7 +6900,7 @@ mod tests {
|
||||||
|
|
||||||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
let panel_a = workspace_a.update_in(cx, |workspace, window, cx| {
|
let panel_a = workspace_a.update_in(cx, |workspace, window, cx| {
|
||||||
cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
|
cx.new(|cx| AgentPanel::new(workspace, window, cx))
|
||||||
});
|
});
|
||||||
panel_a
|
panel_a
|
||||||
.update_in(cx, |panel, window, cx| {
|
.update_in(cx, |panel, window, cx| {
|
||||||
|
|
@ -6961,7 +6977,7 @@ mod tests {
|
||||||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
|
|
||||||
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||||
cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
|
cx.new(|cx| AgentPanel::new(workspace, window, cx))
|
||||||
});
|
});
|
||||||
|
|
||||||
panel.update_in(cx, |panel, window, cx| {
|
panel.update_in(cx, |panel, window, cx| {
|
||||||
|
|
@ -7053,7 +7069,7 @@ mod tests {
|
||||||
});
|
});
|
||||||
|
|
||||||
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||||
cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
|
cx.new(|cx| AgentPanel::new(workspace, window, cx))
|
||||||
});
|
});
|
||||||
|
|
||||||
// Open a restored thread using a flaky server so the initial connect
|
// Open a restored thread using a flaky server so the initial connect
|
||||||
|
|
@ -7152,7 +7168,7 @@ mod tests {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
|
|
@ -7252,12 +7268,12 @@ mod tests {
|
||||||
|
|
||||||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
let panel_a = workspace_a.update_in(cx, |workspace, window, cx| {
|
let panel_a = workspace_a.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
let panel_b = workspace_b.update_in(cx, |workspace, window, cx| {
|
let panel_b = workspace_b.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
|
|
@ -7632,7 +7648,7 @@ mod tests {
|
||||||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
|
|
||||||
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
|
|
@ -7819,7 +7835,7 @@ mod tests {
|
||||||
|
|
||||||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
|
|
@ -8048,7 +8064,7 @@ mod tests {
|
||||||
|
|
||||||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
|
|
@ -8134,7 +8150,7 @@ mod tests {
|
||||||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
|
|
||||||
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
|
|
@ -8224,7 +8240,7 @@ mod tests {
|
||||||
let mut cx = VisualTestContext::from_window(multi_workspace.into(), cx);
|
let mut cx = VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
|
|
||||||
let panel = workspace.update_in(&mut cx, |workspace, window, cx| {
|
let panel = workspace.update_in(&mut cx, |workspace, window, cx| {
|
||||||
cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
|
cx.new(|cx| AgentPanel::new(workspace, window, cx))
|
||||||
});
|
});
|
||||||
|
|
||||||
(panel, cx)
|
(panel, cx)
|
||||||
|
|
@ -8271,7 +8287,7 @@ mod tests {
|
||||||
register_test_sidebar(threads_list_active, &mut cx);
|
register_test_sidebar(threads_list_active, &mut cx);
|
||||||
|
|
||||||
let panel = workspace.update_in(&mut cx, |workspace, window, cx| {
|
let panel = workspace.update_in(&mut cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
workspace.focus_panel::<AgentPanel>(window, cx);
|
workspace.focus_panel::<AgentPanel>(window, cx);
|
||||||
panel
|
panel
|
||||||
|
|
@ -8401,7 +8417,7 @@ mod tests {
|
||||||
let mut cx = VisualTestContext::from_window(multi_workspace.into(), cx);
|
let mut cx = VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
|
|
||||||
let panel = workspace.update_in(&mut cx, |workspace, window, cx| {
|
let panel = workspace.update_in(&mut cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
|
|
@ -8514,7 +8530,7 @@ mod tests {
|
||||||
let mut cx = VisualTestContext::from_window(multi_workspace.into(), cx);
|
let mut cx = VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
|
|
||||||
let panel = workspace.update_in(&mut cx, |workspace, window, cx| {
|
let panel = workspace.update_in(&mut cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
|
|
@ -9001,6 +9017,182 @@ mod tests {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_terminal_custom_title_recomposes_with_live_spinner(cx: &mut TestAppContext) {
|
||||||
|
let (panel, mut cx) = setup_panel(cx).await;
|
||||||
|
let terminal_id = panel
|
||||||
|
.update_in(&mut cx, |panel, window, cx| {
|
||||||
|
panel.insert_test_terminal("Fix bug", true, window, cx)
|
||||||
|
})
|
||||||
|
.expect("test terminal should be inserted");
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
let terminal_entity = panel.read_with(&cx, |panel, _cx| {
|
||||||
|
panel
|
||||||
|
.terminals
|
||||||
|
.get(&terminal_id)
|
||||||
|
.expect("terminal should remain in the panel")
|
||||||
|
.view
|
||||||
|
.clone()
|
||||||
|
});
|
||||||
|
let terminal_entity =
|
||||||
|
terminal_entity.read_with(&cx, |terminal_view, _cx| terminal_view.terminal().clone());
|
||||||
|
|
||||||
|
terminal_entity.update(&mut cx, |terminal, cx| {
|
||||||
|
terminal.breadcrumb_text = "⠋ Thinking".to_string();
|
||||||
|
cx.emit(TerminalEvent::BreadcrumbsChanged);
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
panel.read_with(&cx, |panel, cx| {
|
||||||
|
let terminals = panel.terminals(cx);
|
||||||
|
assert_eq!(terminals.len(), 1);
|
||||||
|
assert_eq!(terminals[0].title.as_ref(), "⠋ Fix bug");
|
||||||
|
let metadata = panel
|
||||||
|
.terminal_metadata(terminal_id, cx)
|
||||||
|
.expect("terminal metadata should be available");
|
||||||
|
assert_eq!(metadata.title.as_ref(), "⠋ Thinking");
|
||||||
|
assert_eq!(
|
||||||
|
metadata.custom_title.as_ref().map(|title| title.as_ref()),
|
||||||
|
Some("Fix bug")
|
||||||
|
);
|
||||||
|
assert_eq!(metadata.display_title().as_ref(), "⠋ Fix bug");
|
||||||
|
});
|
||||||
|
|
||||||
|
terminal_entity.update(&mut cx, |terminal, cx| {
|
||||||
|
terminal.breadcrumb_text = "⠙ Thinking".to_string();
|
||||||
|
cx.emit(TerminalEvent::BreadcrumbsChanged);
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
panel.read_with(&cx, |panel, cx| {
|
||||||
|
let terminals = panel.terminals(cx);
|
||||||
|
assert_eq!(terminals.len(), 1);
|
||||||
|
assert_eq!(terminals[0].title.as_ref(), "⠙ Fix bug");
|
||||||
|
let metadata = panel
|
||||||
|
.terminal_metadata(terminal_id, cx)
|
||||||
|
.expect("terminal metadata should be available");
|
||||||
|
assert_eq!(metadata.title.as_ref(), "⠙ Thinking");
|
||||||
|
assert_eq!(metadata.display_title().as_ref(), "⠙ Fix bug");
|
||||||
|
});
|
||||||
|
|
||||||
|
terminal_entity.update(&mut cx, |terminal, cx| {
|
||||||
|
terminal.breadcrumb_text = "Thinking".to_string();
|
||||||
|
cx.emit(TerminalEvent::BreadcrumbsChanged);
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
panel.read_with(&cx, |panel, cx| {
|
||||||
|
let terminals = panel.terminals(cx);
|
||||||
|
assert_eq!(terminals.len(), 1);
|
||||||
|
assert_eq!(terminals[0].title.as_ref(), "Fix bug");
|
||||||
|
let metadata = panel
|
||||||
|
.terminal_metadata(terminal_id, cx)
|
||||||
|
.expect("terminal metadata should be available");
|
||||||
|
assert_eq!(metadata.title.as_ref(), "Thinking");
|
||||||
|
assert_eq!(metadata.display_title().as_ref(), "Fix bug");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_terminal_title_editor_excludes_spinner_prefix(cx: &mut TestAppContext) {
|
||||||
|
let (panel, mut cx) = setup_panel(cx).await;
|
||||||
|
let terminal_id = panel
|
||||||
|
.update_in(&mut cx, |panel, window, cx| {
|
||||||
|
panel.insert_test_terminal("Initial Custom Title", true, window, cx)
|
||||||
|
})
|
||||||
|
.expect("test terminal should be inserted");
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
let terminal_view = panel.read_with(&cx, |panel, _cx| {
|
||||||
|
panel
|
||||||
|
.terminals
|
||||||
|
.get(&terminal_id)
|
||||||
|
.expect("terminal should remain in the panel")
|
||||||
|
.view
|
||||||
|
.clone()
|
||||||
|
});
|
||||||
|
terminal_view.update(&mut cx, |terminal_view, cx| {
|
||||||
|
terminal_view.set_custom_title(None, cx);
|
||||||
|
});
|
||||||
|
let terminal_entity =
|
||||||
|
terminal_view.read_with(&cx, |terminal_view, _cx| terminal_view.terminal().clone());
|
||||||
|
terminal_entity.update(&mut cx, |terminal, cx| {
|
||||||
|
terminal.breadcrumb_text = "⠋ Thinking".to_string();
|
||||||
|
cx.emit(TerminalEvent::BreadcrumbsChanged);
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
panel.update_in(&mut cx, |panel, window, cx| {
|
||||||
|
panel.edit_terminal_title(terminal_id, window, cx);
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
let title_editor = panel.read_with(&cx, |panel, cx| {
|
||||||
|
let terminal = panel
|
||||||
|
.terminals
|
||||||
|
.get(&terminal_id)
|
||||||
|
.expect("terminal should remain in the panel");
|
||||||
|
let title_editor = terminal
|
||||||
|
.title_editor
|
||||||
|
.as_ref()
|
||||||
|
.expect("terminal title editor should be active while editing")
|
||||||
|
.clone();
|
||||||
|
assert_eq!(title_editor.read(cx).text(cx), "Thinking");
|
||||||
|
title_editor
|
||||||
|
});
|
||||||
|
|
||||||
|
title_editor.update_in(&mut cx, |editor, window, cx| {
|
||||||
|
editor.set_text("Fix bug", window, cx);
|
||||||
|
editor.focus_handle(cx).focus(window, cx);
|
||||||
|
});
|
||||||
|
panel.update_in(&mut cx, |panel, window, cx| {
|
||||||
|
panel.handle_terminal_title_editor_event(
|
||||||
|
terminal_id,
|
||||||
|
&title_editor,
|
||||||
|
&editor::EditorEvent::BufferEdited,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
terminal_view.read_with(&cx, |terminal_view, _cx| {
|
||||||
|
assert_eq!(terminal_view.custom_title(), Some("Fix bug"));
|
||||||
|
});
|
||||||
|
panel.read_with(&cx, |panel, cx| {
|
||||||
|
let terminals = panel.terminals(cx);
|
||||||
|
assert_eq!(terminals.len(), 1);
|
||||||
|
assert_eq!(terminals[0].title.as_ref(), "⠋ Fix bug");
|
||||||
|
let metadata = panel
|
||||||
|
.terminal_metadata(terminal_id, cx)
|
||||||
|
.expect("terminal metadata should be available");
|
||||||
|
assert_eq!(metadata.title.as_ref(), "⠋ Thinking");
|
||||||
|
assert_eq!(
|
||||||
|
metadata.custom_title.as_ref().map(|title| title.as_ref()),
|
||||||
|
Some("Fix bug")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
panel.update_in(&mut cx, |panel, window, cx| {
|
||||||
|
panel.stop_editing_terminal_title(terminal_id, false, window, cx);
|
||||||
|
panel.edit_terminal_title(terminal_id, window, cx);
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
panel.read_with(&cx, |panel, cx| {
|
||||||
|
let terminal = panel
|
||||||
|
.terminals
|
||||||
|
.get(&terminal_id)
|
||||||
|
.expect("terminal should remain in the panel");
|
||||||
|
let title_editor = terminal
|
||||||
|
.title_editor
|
||||||
|
.as_ref()
|
||||||
|
.expect("terminal title editor should be active while editing");
|
||||||
|
assert_eq!(title_editor.read(cx).text(cx), "Fix bug");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_terminal_bell_marks_and_activation_clears_notification(cx: &mut TestAppContext) {
|
async fn test_terminal_bell_marks_and_activation_clears_notification(cx: &mut TestAppContext) {
|
||||||
let (panel, mut cx) = setup_panel(cx).await;
|
let (panel, mut cx) = setup_panel(cx).await;
|
||||||
|
|
@ -9581,7 +9773,7 @@ mod tests {
|
||||||
|
|
||||||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
let panel_a = workspace_a.update_in(cx, |workspace, window, cx| {
|
let panel_a = workspace_a.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
|
|
@ -10173,7 +10365,7 @@ mod tests {
|
||||||
let mut cx = VisualTestContext::from_window(multi_workspace.into(), cx);
|
let mut cx = VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
|
|
||||||
let panel = workspace.update_in(&mut cx, |workspace, window, cx| {
|
let panel = workspace.update_in(&mut cx, |workspace, window, cx| {
|
||||||
cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
|
cx.new(|cx| AgentPanel::new(workspace, window, cx))
|
||||||
});
|
});
|
||||||
|
|
||||||
// Open thread A and send a message. With empty next_prompt_updates it
|
// Open thread A and send a message. With empty next_prompt_updates it
|
||||||
|
|
@ -10442,7 +10634,7 @@ mod tests {
|
||||||
|
|
||||||
// Set up workspace A with agent_a
|
// Set up workspace A with agent_a
|
||||||
let panel_a = workspace_a.update_in(cx, |workspace, window, cx| {
|
let panel_a = workspace_a.update_in(cx, |workspace, window, cx| {
|
||||||
cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
|
cx.new(|cx| AgentPanel::new(workspace, window, cx))
|
||||||
});
|
});
|
||||||
panel_a.update(cx, |panel, _cx| {
|
panel_a.update(cx, |panel, _cx| {
|
||||||
panel.selected_agent = agent_a.clone();
|
panel.selected_agent = agent_a.clone();
|
||||||
|
|
@ -10450,7 +10642,7 @@ mod tests {
|
||||||
|
|
||||||
// Set up workspace B with agent_b
|
// Set up workspace B with agent_b
|
||||||
let panel_b = workspace_b.update_in(cx, |workspace, window, cx| {
|
let panel_b = workspace_b.update_in(cx, |workspace, window, cx| {
|
||||||
cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
|
cx.new(|cx| AgentPanel::new(workspace, window, cx))
|
||||||
});
|
});
|
||||||
panel_b.update(cx, |panel, _cx| {
|
panel_b.update(cx, |panel, _cx| {
|
||||||
panel.selected_agent = agent_b.clone();
|
panel.selected_agent = agent_b.clone();
|
||||||
|
|
@ -10521,7 +10713,7 @@ mod tests {
|
||||||
};
|
};
|
||||||
|
|
||||||
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
|
|
@ -10578,7 +10770,7 @@ mod tests {
|
||||||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
|
|
||||||
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
|
|
@ -10668,7 +10860,7 @@ mod tests {
|
||||||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
|
|
||||||
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
|
|
@ -10756,7 +10948,7 @@ mod tests {
|
||||||
workspace.update(cx, |workspace, _cx| workspace.set_random_database_id());
|
workspace.update(cx, |workspace, _cx| workspace.set_random_database_id());
|
||||||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
|
|
@ -10866,7 +11058,7 @@ mod tests {
|
||||||
workspace.update(cx, |workspace, _cx| workspace.set_random_database_id());
|
workspace.update(cx, |workspace, _cx| workspace.set_random_database_id());
|
||||||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
|
|
@ -10972,7 +11164,7 @@ mod tests {
|
||||||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
|
|
||||||
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
|
|
@ -11471,7 +11663,7 @@ mod tests {
|
||||||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
|
|
||||||
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||||
cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
|
cx.new(|cx| AgentPanel::new(workspace, window, cx))
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.run_until_parked();
|
cx.run_until_parked();
|
||||||
|
|
@ -11572,7 +11764,7 @@ mod tests {
|
||||||
|
|
||||||
// Create the agent panel and add it to the workspace.
|
// Create the agent panel and add it to the workspace.
|
||||||
let panel = workspace.update_in(&mut cx, |workspace, window, cx| {
|
let panel = workspace.update_in(&mut cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
|
|
@ -11782,7 +11974,7 @@ mod tests {
|
||||||
let mut cx = VisualTestContext::from_window(multi_workspace.into(), cx);
|
let mut cx = VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
|
|
||||||
let panel = workspace.update_in(&mut cx, |workspace, window, cx| {
|
let panel = workspace.update_in(&mut cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
|
|
@ -12019,7 +12211,7 @@ mod tests {
|
||||||
|
|
||||||
// Set up panel_a with an active thread and type draft text.
|
// Set up panel_a with an active thread and type draft text.
|
||||||
let panel_a = workspace_a.update_in(cx, |workspace, window, cx| {
|
let panel_a = workspace_a.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
|
|
@ -12043,7 +12235,7 @@ mod tests {
|
||||||
|
|
||||||
// Set up panel_b on workspace_b — starts as a fresh, empty panel.
|
// Set up panel_b on workspace_b — starts as a fresh, empty panel.
|
||||||
let panel_b = workspace_b.update_in(cx, |workspace, window, cx| {
|
let panel_b = workspace_b.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
|
|
@ -12113,7 +12305,7 @@ mod tests {
|
||||||
|
|
||||||
// Set up panel_a with draft text.
|
// Set up panel_a with draft text.
|
||||||
let panel_a = workspace_a.update_in(cx, |workspace, window, cx| {
|
let panel_a = workspace_a.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
|
|
@ -12137,7 +12329,7 @@ mod tests {
|
||||||
|
|
||||||
// Set up panel_b with its OWN content — this is a non-fresh panel.
|
// Set up panel_b with its OWN content — this is a non-fresh panel.
|
||||||
let panel_b = workspace_b.update_in(cx, |workspace, window, cx| {
|
let panel_b = workspace_b.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
|
|
@ -12181,42 +12373,4 @@ mod tests {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Regression test: NewThread must produce a connected thread even when
|
|
||||||
/// the PromptStore fails to initialize (e.g. LMDB permission error).
|
|
||||||
/// Before the fix, `NativeAgentServer::connect` propagated the
|
|
||||||
/// PromptStore error with `?`, which put every new ConversationView
|
|
||||||
/// into LoadError and made it impossible to start any native-agent
|
|
||||||
/// thread.
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_new_thread_with_prompt_store_error(cx: &mut TestAppContext) {
|
|
||||||
let (panel, mut cx) = setup_panel(cx).await;
|
|
||||||
|
|
||||||
// NativeAgentServer::connect needs a global Fs.
|
|
||||||
let fs = FakeFs::new(cx.executor());
|
|
||||||
cx.update(|_, cx| {
|
|
||||||
<dyn fs::Fs>::set_global(fs.clone(), cx);
|
|
||||||
});
|
|
||||||
cx.run_until_parked();
|
|
||||||
|
|
||||||
// Dispatch NewThread, which goes through the real NativeAgentServer
|
|
||||||
// path. In tests the PromptStore LMDB open fails with
|
|
||||||
// "Permission denied"; the fix (.log_err() instead of ?) lets
|
|
||||||
// the connection succeed anyway.
|
|
||||||
panel.update_in(&mut cx, |panel, window, cx| {
|
|
||||||
panel.new_thread(&NewThread, window, cx);
|
|
||||||
});
|
|
||||||
cx.run_until_parked();
|
|
||||||
|
|
||||||
panel.read_with(&cx, |panel, cx| {
|
|
||||||
assert!(
|
|
||||||
panel.active_conversation_view().is_some(),
|
|
||||||
"panel should have a conversation view after NewThread"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
panel.active_agent_thread(cx).is_some(),
|
|
||||||
"panel should have an active, connected agent thread"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ use language_model::{
|
||||||
ConfiguredModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
|
ConfiguredModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
|
||||||
};
|
};
|
||||||
use project::{AgentId, DisableAiSettings};
|
use project::{AgentId, DisableAiSettings};
|
||||||
use prompt_store::{PromptBuilder, rules_to_skills_migration};
|
use prompt_store::{self, PromptBuilder, rules_to_skills_migration};
|
||||||
use rope::Point;
|
use rope::Point;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
@ -550,7 +550,7 @@ pub fn init(
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) {
|
) {
|
||||||
agent::ThreadStore::init_global(cx);
|
agent::ThreadStore::init_global(cx);
|
||||||
rules_library::init(cx);
|
prompt_store::init(cx);
|
||||||
skill_creator::init(cx);
|
skill_creator::init(cx);
|
||||||
if !is_eval {
|
if !is_eval {
|
||||||
// Initializing the language model from the user settings messes with the eval, so we only initialize them when
|
// Initializing the language model from the user settings messes with the eval, so we only initialize them when
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,9 @@ use futures::{
|
||||||
stream::BoxStream,
|
stream::BoxStream,
|
||||||
};
|
};
|
||||||
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Subscription, Task};
|
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Subscription, Task};
|
||||||
use language::{Buffer, IndentKind, LanguageName, Point, TransactionId, line_diff};
|
use language::{
|
||||||
|
Buffer, BufferEditSource, IndentKind, LanguageName, Point, TransactionId, line_diff,
|
||||||
|
};
|
||||||
use language_model::{
|
use language_model::{
|
||||||
CompletionIntent, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
|
CompletionIntent, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
|
||||||
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||||
|
|
@ -978,7 +980,7 @@ impl CodegenAlternative {
|
||||||
buffer.finalize_last_transaction(cx);
|
buffer.finalize_last_transaction(cx);
|
||||||
buffer.start_transaction(cx);
|
buffer.start_transaction(cx);
|
||||||
buffer.edit(edits, None, cx);
|
buffer.edit(edits, None, cx);
|
||||||
buffer.end_transaction(cx)
|
buffer.end_transaction_with_source(BufferEditSource::Agent, cx)
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(transaction) = transaction {
|
if let Some(transaction) = transaction {
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,6 @@ use markdown::{
|
||||||
};
|
};
|
||||||
use parking_lot::{Mutex, RwLock};
|
use parking_lot::{Mutex, RwLock};
|
||||||
use project::{AgentId, AgentServerStore, Project, ProjectEntryId, ProjectPath};
|
use project::{AgentId, AgentServerStore, Project, ProjectEntryId, ProjectPath};
|
||||||
use prompt_store::{PromptId, PromptStore};
|
|
||||||
|
|
||||||
use crate::message_editor::SessionCapabilities;
|
use crate::message_editor::SessionCapabilities;
|
||||||
use crate::{AgentThreadSource, DEFAULT_THREAD_TITLE, resolve_agent_image};
|
use crate::{AgentThreadSource, DEFAULT_THREAD_TITLE, resolve_agent_image};
|
||||||
|
|
@ -75,7 +74,6 @@ use workspace::{
|
||||||
path_link::sanitize_path_text,
|
path_link::sanitize_path_text,
|
||||||
};
|
};
|
||||||
use zed_actions::agent::{Chat, ToggleModelSelector};
|
use zed_actions::agent::{Chat, ToggleModelSelector};
|
||||||
use zed_actions::assistant::OpenRulesLibrary;
|
|
||||||
|
|
||||||
use super::config_options::ConfigOptionsView;
|
use super::config_options::ConfigOptionsView;
|
||||||
use super::entry_view_state::EntryViewState;
|
use super::entry_view_state::EntryViewState;
|
||||||
|
|
@ -531,7 +529,6 @@ pub struct ConversationView {
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
thread_store: Option<Entity<ThreadStore>>,
|
thread_store: Option<Entity<ThreadStore>>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
|
||||||
pub(crate) thread_id: ThreadId,
|
pub(crate) thread_id: ThreadId,
|
||||||
pub(crate) root_session_id: Option<acp::SessionId>,
|
pub(crate) root_session_id: Option<acp::SessionId>,
|
||||||
server_state: ServerState,
|
server_state: ServerState,
|
||||||
|
|
@ -738,7 +735,6 @@ impl ConversationView {
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
thread_store: Option<Entity<ThreadStore>>,
|
thread_store: Option<Entity<ThreadStore>>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
|
||||||
source: AgentThreadSource,
|
source: AgentThreadSource,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
|
|
@ -795,7 +791,6 @@ impl ConversationView {
|
||||||
workspace,
|
workspace,
|
||||||
project: project.clone(),
|
project: project.clone(),
|
||||||
thread_store,
|
thread_store,
|
||||||
prompt_store,
|
|
||||||
thread_id,
|
thread_id,
|
||||||
root_session_id: resume_session_id.clone(),
|
root_session_id: resume_session_id.clone(),
|
||||||
server_state: Self::initial_state(
|
server_state: Self::initial_state(
|
||||||
|
|
@ -1104,7 +1099,6 @@ impl ConversationView {
|
||||||
self.workspace.clone(),
|
self.workspace.clone(),
|
||||||
self.project.downgrade(),
|
self.project.downgrade(),
|
||||||
self.thread_store.clone(),
|
self.thread_store.clone(),
|
||||||
self.prompt_store.clone(),
|
|
||||||
session_capabilities.clone(),
|
session_capabilities.clone(),
|
||||||
self.agent.agent_id(),
|
self.agent.agent_id(),
|
||||||
)
|
)
|
||||||
|
|
@ -1273,7 +1267,6 @@ impl ConversationView {
|
||||||
self.project.downgrade(),
|
self.project.downgrade(),
|
||||||
self.code_span_resolver.clone(),
|
self.code_span_resolver.clone(),
|
||||||
self.thread_store.clone(),
|
self.thread_store.clone(),
|
||||||
self.prompt_store.clone(),
|
|
||||||
initial_content,
|
initial_content,
|
||||||
subscriptions,
|
subscriptions,
|
||||||
window,
|
window,
|
||||||
|
|
@ -2492,7 +2485,6 @@ impl ConversationView {
|
||||||
workspace.clone(),
|
workspace.clone(),
|
||||||
project.clone(),
|
project.clone(),
|
||||||
None,
|
None,
|
||||||
None,
|
|
||||||
session_capabilities.clone(),
|
session_capabilities.clone(),
|
||||||
agent_name.clone(),
|
agent_name.clone(),
|
||||||
"",
|
"",
|
||||||
|
|
@ -3721,7 +3713,6 @@ pub(crate) mod tests {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project,
|
project,
|
||||||
Some(thread_store),
|
Some(thread_store),
|
||||||
None,
|
|
||||||
AgentThreadSource::AgentPanel,
|
AgentThreadSource::AgentPanel,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
|
@ -3858,7 +3849,6 @@ pub(crate) mod tests {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project,
|
project,
|
||||||
Some(thread_store),
|
Some(thread_store),
|
||||||
None,
|
|
||||||
AgentThreadSource::AgentPanel,
|
AgentThreadSource::AgentPanel,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
|
@ -3940,7 +3930,6 @@ pub(crate) mod tests {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project,
|
project,
|
||||||
Some(thread_store),
|
Some(thread_store),
|
||||||
None,
|
|
||||||
AgentThreadSource::AgentPanel,
|
AgentThreadSource::AgentPanel,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
|
@ -4079,7 +4068,6 @@ pub(crate) mod tests {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project.clone(),
|
project.clone(),
|
||||||
Some(thread_store),
|
Some(thread_store),
|
||||||
None,
|
|
||||||
AgentThreadSource::AgentPanel,
|
AgentThreadSource::AgentPanel,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
|
@ -4364,7 +4352,7 @@ pub(crate) mod tests {
|
||||||
let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), cx);
|
let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), cx);
|
||||||
|
|
||||||
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| crate::AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| crate::AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
workspace.focus_panel::<crate::AgentPanel>(window, cx);
|
workspace.focus_panel::<crate::AgentPanel>(window, cx);
|
||||||
panel
|
panel
|
||||||
|
|
@ -4405,7 +4393,6 @@ pub(crate) mod tests {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project.clone(),
|
project.clone(),
|
||||||
Some(thread_store),
|
Some(thread_store),
|
||||||
None,
|
|
||||||
AgentThreadSource::AgentPanel,
|
AgentThreadSource::AgentPanel,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
|
@ -4504,7 +4491,6 @@ pub(crate) mod tests {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project.clone(),
|
project.clone(),
|
||||||
Some(thread_store),
|
Some(thread_store),
|
||||||
None,
|
|
||||||
AgentThreadSource::AgentPanel,
|
AgentThreadSource::AgentPanel,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
|
@ -4580,7 +4566,6 @@ pub(crate) mod tests {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project.clone(),
|
project.clone(),
|
||||||
Some(thread_store),
|
Some(thread_store),
|
||||||
None,
|
|
||||||
AgentThreadSource::AgentPanel,
|
AgentThreadSource::AgentPanel,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
|
@ -4648,7 +4633,6 @@ pub(crate) mod tests {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project.clone(),
|
project.clone(),
|
||||||
Some(thread_store),
|
Some(thread_store),
|
||||||
None,
|
|
||||||
AgentThreadSource::AgentPanel,
|
AgentThreadSource::AgentPanel,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
|
@ -4724,7 +4708,7 @@ pub(crate) mod tests {
|
||||||
let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), cx);
|
let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), cx);
|
||||||
|
|
||||||
let panel = workspace1.update_in(cx, |workspace, window, cx| {
|
let panel = workspace1.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| crate::AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| crate::AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
|
|
||||||
// Open the dock and activate the agent panel so it's visible
|
// Open the dock and activate the agent panel so it's visible
|
||||||
|
|
@ -4770,7 +4754,6 @@ pub(crate) mod tests {
|
||||||
workspace1.downgrade(),
|
workspace1.downgrade(),
|
||||||
project1.clone(),
|
project1.clone(),
|
||||||
Some(thread_store),
|
Some(thread_store),
|
||||||
None,
|
|
||||||
AgentThreadSource::AgentPanel,
|
AgentThreadSource::AgentPanel,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
|
@ -4992,7 +4975,6 @@ pub(crate) mod tests {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project,
|
project,
|
||||||
Some(thread_store),
|
Some(thread_store),
|
||||||
None,
|
|
||||||
AgentThreadSource::AgentPanel,
|
AgentThreadSource::AgentPanel,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
|
@ -5651,7 +5633,6 @@ pub(crate) mod tests {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project.clone(),
|
project.clone(),
|
||||||
Some(thread_store.clone()),
|
Some(thread_store.clone()),
|
||||||
None,
|
|
||||||
AgentThreadSource::AgentPanel,
|
AgentThreadSource::AgentPanel,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
|
@ -8113,9 +8094,17 @@ pub(crate) mod tests {
|
||||||
async fn test_permission_row_hidden_when_inline_bounds_unavailable(cx: &mut TestAppContext) {
|
async fn test_permission_row_hidden_when_inline_bounds_unavailable(cx: &mut TestAppContext) {
|
||||||
init_test(cx);
|
init_test(cx);
|
||||||
|
|
||||||
let (_view, thread_view, _entry_ix, cx) =
|
let (_view, thread_view, entry_ix, cx) =
|
||||||
setup_pending_permission_thread("perm-no-bounds", cx).await;
|
setup_pending_permission_thread("perm-no-bounds", cx).await;
|
||||||
|
|
||||||
|
// Pin the scroll top to the entry so it isn't treated as above the
|
||||||
|
// viewport, forcing the unmeasured-bounds path we want to exercise.
|
||||||
|
thread_view.read_with(cx, |view, _cx| {
|
||||||
|
view.list_state.scroll_to(ListOffset {
|
||||||
|
item_ix: entry_ix,
|
||||||
|
offset_in_item: px(0.0),
|
||||||
|
});
|
||||||
|
});
|
||||||
thread_view.update_in(cx, |view, window, cx| {
|
thread_view.update_in(cx, |view, window, cx| {
|
||||||
assert!(
|
assert!(
|
||||||
view.render_main_agent_awaiting_permission(window, cx)
|
view.render_main_agent_awaiting_permission(window, cx)
|
||||||
|
|
@ -8176,8 +8165,8 @@ pub(crate) mod tests {
|
||||||
let (_view, thread_view, entry_ix, cx) =
|
let (_view, thread_view, entry_ix, cx) =
|
||||||
setup_pending_permission_thread("perm-scroll", cx).await;
|
setup_pending_permission_thread("perm-scroll", cx).await;
|
||||||
|
|
||||||
// Start off-screen below the viewport — row visible because the item
|
// Start off-screen below the viewport. The row is visible because the
|
||||||
// has bounds that do not intersect the viewport.
|
// item has bounds that do not intersect the viewport.
|
||||||
draw_thread_list_at(
|
draw_thread_list_at(
|
||||||
&thread_view,
|
&thread_view,
|
||||||
ListOffset {
|
ListOffset {
|
||||||
|
|
@ -8221,6 +8210,69 @@ pub(crate) mod tests {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_permission_row_shown_when_inline_prompt_is_above_viewport(
|
||||||
|
cx: &mut TestAppContext,
|
||||||
|
) {
|
||||||
|
init_test(cx);
|
||||||
|
|
||||||
|
let (_view, thread_view, entry_ix, cx) =
|
||||||
|
setup_pending_permission_thread("perm-above", cx).await;
|
||||||
|
|
||||||
|
let thread = thread_view.read_with(cx, |view, _cx| view.thread.clone());
|
||||||
|
thread.update(cx, |thread, cx| {
|
||||||
|
let result = thread.handle_session_update(
|
||||||
|
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(
|
||||||
|
"More content".into(),
|
||||||
|
)),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
result.is_ok(),
|
||||||
|
"following assistant message should be accepted"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
draw_thread_list_at(
|
||||||
|
&thread_view,
|
||||||
|
ListOffset {
|
||||||
|
item_ix: entry_ix + 1,
|
||||||
|
offset_in_item: px(0.0),
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
thread_view.read_with(cx, |view, _cx| {
|
||||||
|
assert!(
|
||||||
|
entry_ix < view.list_state.logical_scroll_top().item_ix,
|
||||||
|
"The tool call entry should be above the logical scroll top"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
thread_view.update_in(cx, |view, window, cx| {
|
||||||
|
assert!(
|
||||||
|
view.render_main_agent_awaiting_permission(window, cx)
|
||||||
|
.is_some(),
|
||||||
|
"Floating row should be visible when the inline prompt is above the viewport"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Scrolling up to the entry brings it back into view.
|
||||||
|
draw_thread_list_at(
|
||||||
|
&thread_view,
|
||||||
|
ListOffset {
|
||||||
|
item_ix: entry_ix,
|
||||||
|
offset_in_item: px(0.0),
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
thread_view.update_in(cx, |view, window, cx| {
|
||||||
|
assert!(
|
||||||
|
view.render_main_agent_awaiting_permission(window, cx)
|
||||||
|
.is_none(),
|
||||||
|
"Floating row should disappear after scrolling brings the inline prompt into view"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_permission_row_disappears_when_authorized(cx: &mut TestAppContext) {
|
async fn test_permission_row_disappears_when_authorized(cx: &mut TestAppContext) {
|
||||||
init_test(cx);
|
init_test(cx);
|
||||||
|
|
@ -8556,7 +8608,6 @@ pub(crate) mod tests {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project,
|
project,
|
||||||
Some(thread_store),
|
Some(thread_store),
|
||||||
None,
|
|
||||||
AgentThreadSource::AgentPanel,
|
AgentThreadSource::AgentPanel,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
|
|
||||||
|
|
@ -683,7 +683,6 @@ impl ThreadView {
|
||||||
project: WeakEntity<Project>,
|
project: WeakEntity<Project>,
|
||||||
code_span_resolver: AgentCodeSpanResolver,
|
code_span_resolver: AgentCodeSpanResolver,
|
||||||
thread_store: Option<Entity<ThreadStore>>,
|
thread_store: Option<Entity<ThreadStore>>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
|
||||||
initial_content: Option<AgentInitialContent>,
|
initial_content: Option<AgentInitialContent>,
|
||||||
mut subscriptions: Vec<Subscription>,
|
mut subscriptions: Vec<Subscription>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
|
|
@ -703,7 +702,6 @@ impl ThreadView {
|
||||||
workspace.clone(),
|
workspace.clone(),
|
||||||
project.clone(),
|
project.clone(),
|
||||||
thread_store,
|
thread_store,
|
||||||
prompt_store,
|
|
||||||
session_capabilities.clone(),
|
session_capabilities.clone(),
|
||||||
agent_id.clone(),
|
agent_id.clone(),
|
||||||
&placeholder,
|
&placeholder,
|
||||||
|
|
@ -3047,15 +3045,6 @@ impl ThreadView {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true when the entry has been measured and sits entirely below
|
|
||||||
/// the current viewport.
|
|
||||||
fn entry_is_below_viewport(&self, entry_ix: usize) -> bool {
|
|
||||||
let viewport_bounds = self.list_state.viewport_bounds();
|
|
||||||
self.list_state
|
|
||||||
.bounds_for_item(entry_ix)
|
|
||||||
.is_some_and(|entry_bounds| entry_bounds.top() >= viewport_bounds.bottom())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn render_main_agent_awaiting_permission(
|
pub(crate) fn render_main_agent_awaiting_permission(
|
||||||
&self,
|
&self,
|
||||||
window: &Window,
|
window: &Window,
|
||||||
|
|
@ -3073,9 +3062,13 @@ impl ThreadView {
|
||||||
let thread = self.thread.read(cx);
|
let thread = self.thread.read(cx);
|
||||||
let (entry_ix, tool_call) = thread.tool_call(&tool_call_id)?;
|
let (entry_ix, tool_call) = thread.tool_call(&tool_call_id)?;
|
||||||
|
|
||||||
if !self.entry_is_below_viewport(entry_ix) {
|
let scroll_icon = if self.list_state.item_is_above_viewport(entry_ix)? {
|
||||||
|
IconName::ArrowUp
|
||||||
|
} else if self.list_state.item_is_below_viewport(entry_ix)? {
|
||||||
|
IconName::ArrowDown
|
||||||
|
} else {
|
||||||
return None;
|
return None;
|
||||||
}
|
};
|
||||||
|
|
||||||
let focus_handle = self.focus_handle(cx);
|
let focus_handle = self.focus_handle(cx);
|
||||||
|
|
||||||
|
|
@ -3118,7 +3111,7 @@ impl ThreadView {
|
||||||
Button::new("main-agent-permission-scroll-to", "Scroll")
|
Button::new("main-agent-permission-scroll-to", "Scroll")
|
||||||
.label_size(LabelSize::Small)
|
.label_size(LabelSize::Small)
|
||||||
.end_icon(
|
.end_icon(
|
||||||
Icon::new(IconName::ArrowDown)
|
Icon::new(scroll_icon)
|
||||||
.size(IconSize::XSmall)
|
.size(IconSize::XSmall)
|
||||||
.color(Color::Default),
|
.color(Color::Default),
|
||||||
)
|
)
|
||||||
|
|
@ -10014,17 +10007,6 @@ pub(crate) fn open_link(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MentionUri::Rule { id, .. } => {
|
|
||||||
let PromptId::User { uuid } = id else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
window.dispatch_action(
|
|
||||||
Box::new(OpenRulesLibrary {
|
|
||||||
prompt_to_select: Some(uuid.0),
|
|
||||||
}),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
MentionUri::Fetch { url } => {
|
MentionUri::Fetch { url } => {
|
||||||
cx.open_url(url.as_str());
|
cx.open_url(url.as_str());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,7 @@ use gpui::{
|
||||||
ScrollHandle, TextStyleRefinement, WeakEntity, Window,
|
ScrollHandle, TextStyleRefinement, WeakEntity, Window,
|
||||||
};
|
};
|
||||||
use language::language_settings::SoftWrap;
|
use language::language_settings::SoftWrap;
|
||||||
use project::{AgentId, Project};
|
use project::{AgentId, Project, project_settings::DiagnosticSeverity};
|
||||||
use prompt_store::PromptStore;
|
|
||||||
use rope::Point;
|
use rope::Point;
|
||||||
use settings::Settings as _;
|
use settings::Settings as _;
|
||||||
use terminal_view::TerminalView;
|
use terminal_view::TerminalView;
|
||||||
|
|
@ -25,7 +24,6 @@ pub struct EntryViewState {
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
project: WeakEntity<Project>,
|
project: WeakEntity<Project>,
|
||||||
thread_store: Option<Entity<ThreadStore>>,
|
thread_store: Option<Entity<ThreadStore>>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
|
||||||
entries: Vec<Entry>,
|
entries: Vec<Entry>,
|
||||||
session_capabilities: SharedSessionCapabilities,
|
session_capabilities: SharedSessionCapabilities,
|
||||||
agent_id: AgentId,
|
agent_id: AgentId,
|
||||||
|
|
@ -36,7 +34,6 @@ impl EntryViewState {
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
project: WeakEntity<Project>,
|
project: WeakEntity<Project>,
|
||||||
thread_store: Option<Entity<ThreadStore>>,
|
thread_store: Option<Entity<ThreadStore>>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
|
||||||
session_capabilities: SharedSessionCapabilities,
|
session_capabilities: SharedSessionCapabilities,
|
||||||
agent_id: AgentId,
|
agent_id: AgentId,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
|
@ -44,7 +41,6 @@ impl EntryViewState {
|
||||||
workspace,
|
workspace,
|
||||||
project,
|
project,
|
||||||
thread_store,
|
thread_store,
|
||||||
prompt_store,
|
|
||||||
entries: Vec::new(),
|
entries: Vec::new(),
|
||||||
session_capabilities,
|
session_capabilities,
|
||||||
agent_id,
|
agent_id,
|
||||||
|
|
@ -86,7 +82,6 @@ impl EntryViewState {
|
||||||
self.workspace.clone(),
|
self.workspace.clone(),
|
||||||
self.project.clone(),
|
self.project.clone(),
|
||||||
self.thread_store.clone(),
|
self.thread_store.clone(),
|
||||||
self.prompt_store.clone(),
|
|
||||||
self.session_capabilities.clone(),
|
self.session_capabilities.clone(),
|
||||||
self.agent_id.clone(),
|
self.agent_id.clone(),
|
||||||
"Edit message - @ to include context",
|
"Edit message - @ to include context",
|
||||||
|
|
@ -444,7 +439,8 @@ fn create_editor_diff(
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
editor.set_show_gutter(false, cx);
|
editor.set_show_gutter(false, cx);
|
||||||
editor.disable_inline_diagnostics();
|
editor.disable_diagnostics(cx);
|
||||||
|
editor.set_max_diagnostics_severity(DiagnosticSeverity::Off, cx);
|
||||||
editor.disable_expand_excerpt_buttons(cx);
|
editor.disable_expand_excerpt_buttons(cx);
|
||||||
editor.set_show_vertical_scrollbar(false, cx);
|
editor.set_show_vertical_scrollbar(false, cx);
|
||||||
editor.set_minimap_visibility(MinimapVisibility::Disabled, window, cx);
|
editor.set_minimap_visibility(MinimapVisibility::Disabled, window, cx);
|
||||||
|
|
@ -545,7 +541,6 @@ mod tests {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
thread_store,
|
thread_store,
|
||||||
None,
|
|
||||||
Arc::new(RwLock::new(SessionCapabilities::default())),
|
Arc::new(RwLock::new(SessionCapabilities::default())),
|
||||||
"Test Agent".into(),
|
"Test Agent".into(),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ use language_model::{ConfigurationError, ConfiguredModel, LanguageModelRegistry}
|
||||||
use multi_buffer::MultiBufferRow;
|
use multi_buffer::MultiBufferRow;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use project::{DisableAiSettings, Project};
|
use project::{DisableAiSettings, Project};
|
||||||
use prompt_store::{PromptBuilder, PromptStore};
|
use prompt_store::PromptBuilder;
|
||||||
use settings::{Settings, SettingsStore};
|
use settings::{Settings, SettingsStore};
|
||||||
|
|
||||||
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
|
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
|
||||||
|
|
@ -228,7 +228,6 @@ impl InlineAssistant {
|
||||||
};
|
};
|
||||||
let agent_panel = agent_panel.read(cx);
|
let agent_panel = agent_panel.read(cx);
|
||||||
|
|
||||||
let prompt_store = agent_panel.prompt_store().as_ref().cloned();
|
|
||||||
let thread_store = agent_panel.thread_store().clone();
|
let thread_store = agent_panel.thread_store().clone();
|
||||||
|
|
||||||
let handle_assist =
|
let handle_assist =
|
||||||
|
|
@ -240,7 +239,6 @@ impl InlineAssistant {
|
||||||
cx.entity().downgrade(),
|
cx.entity().downgrade(),
|
||||||
workspace.project().downgrade(),
|
workspace.project().downgrade(),
|
||||||
thread_store,
|
thread_store,
|
||||||
prompt_store,
|
|
||||||
action.prompt.clone(),
|
action.prompt.clone(),
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
|
@ -254,7 +252,6 @@ impl InlineAssistant {
|
||||||
cx.entity().downgrade(),
|
cx.entity().downgrade(),
|
||||||
workspace.project().downgrade(),
|
workspace.project().downgrade(),
|
||||||
thread_store,
|
thread_store,
|
||||||
prompt_store,
|
|
||||||
action.prompt.clone(),
|
action.prompt.clone(),
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
|
@ -437,7 +434,6 @@ impl InlineAssistant {
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
project: WeakEntity<Project>,
|
project: WeakEntity<Project>,
|
||||||
thread_store: Entity<ThreadStore>,
|
thread_store: Entity<ThreadStore>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
|
||||||
initial_prompt: Option<String>,
|
initial_prompt: Option<String>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
codegen_ranges: &[Range<Anchor>],
|
codegen_ranges: &[Range<Anchor>],
|
||||||
|
|
@ -483,7 +479,6 @@ impl InlineAssistant {
|
||||||
session_id,
|
session_id,
|
||||||
self.fs.clone(),
|
self.fs.clone(),
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
prompt_store.clone(),
|
|
||||||
project.clone(),
|
project.clone(),
|
||||||
workspace.clone(),
|
workspace.clone(),
|
||||||
window,
|
window,
|
||||||
|
|
@ -574,7 +569,6 @@ impl InlineAssistant {
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
project: WeakEntity<Project>,
|
project: WeakEntity<Project>,
|
||||||
thread_store: Entity<ThreadStore>,
|
thread_store: Entity<ThreadStore>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
|
||||||
initial_prompt: Option<String>,
|
initial_prompt: Option<String>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
|
|
@ -592,7 +586,6 @@ impl InlineAssistant {
|
||||||
workspace,
|
workspace,
|
||||||
project,
|
project,
|
||||||
thread_store,
|
thread_store,
|
||||||
prompt_store,
|
|
||||||
initial_prompt,
|
initial_prompt,
|
||||||
window,
|
window,
|
||||||
&codegen_ranges,
|
&codegen_ranges,
|
||||||
|
|
@ -1915,7 +1908,6 @@ pub mod evals {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
thread_store,
|
thread_store,
|
||||||
None,
|
|
||||||
Some(prompt),
|
Some(prompt),
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ use language_model::{LanguageModel, LanguageModelRegistry};
|
||||||
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
|
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use prompt_store::PromptStore;
|
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::cmp;
|
use std::cmp;
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
|
@ -1237,7 +1236,6 @@ impl PromptEditor<BufferCodegen> {
|
||||||
session_id: Uuid,
|
session_id: Uuid,
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
thread_store: Entity<ThreadStore>,
|
thread_store: Entity<ThreadStore>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
|
||||||
project: WeakEntity<Project>,
|
project: WeakEntity<Project>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
|
|
@ -1276,8 +1274,7 @@ impl PromptEditor<BufferCodegen> {
|
||||||
editor
|
editor
|
||||||
});
|
});
|
||||||
|
|
||||||
let mention_set = cx
|
let mention_set = cx.new(|_cx| MentionSet::new(project, Some(thread_store.clone())));
|
||||||
.new(|_cx| MentionSet::new(project, Some(thread_store.clone()), prompt_store.clone()));
|
|
||||||
|
|
||||||
let model_selector_menu_handle = PopoverMenuHandle::default();
|
let model_selector_menu_handle = PopoverMenuHandle::default();
|
||||||
|
|
||||||
|
|
@ -1393,7 +1390,6 @@ impl PromptEditor<TerminalCodegen> {
|
||||||
session_id: Uuid,
|
session_id: Uuid,
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
thread_store: Entity<ThreadStore>,
|
thread_store: Entity<ThreadStore>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
|
||||||
project: WeakEntity<Project>,
|
project: WeakEntity<Project>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
|
|
@ -1427,8 +1423,7 @@ impl PromptEditor<TerminalCodegen> {
|
||||||
editor
|
editor
|
||||||
});
|
});
|
||||||
|
|
||||||
let mention_set = cx
|
let mention_set = cx.new(|_cx| MentionSet::new(project, Some(thread_store.clone())));
|
||||||
.new(|_cx| MentionSet::new(project, Some(thread_store.clone()), prompt_store.clone()));
|
|
||||||
|
|
||||||
let model_selector_menu_handle = PopoverMenuHandle::default();
|
let model_selector_menu_handle = PopoverMenuHandle::default();
|
||||||
|
|
||||||
|
|
@ -1705,7 +1700,6 @@ mod tests {
|
||||||
session_id,
|
session_id,
|
||||||
fs,
|
fs,
|
||||||
thread_store,
|
thread_store,
|
||||||
None,
|
|
||||||
project,
|
project,
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
window,
|
window,
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,6 @@ use language_model::{LanguageModelImage, LanguageModelImageExt};
|
||||||
use multi_buffer::MultiBufferRow;
|
use multi_buffer::MultiBufferRow;
|
||||||
use postage::stream::Stream as _;
|
use postage::stream::Stream as _;
|
||||||
use project::{Project, ProjectItem, ProjectPath, Worktree};
|
use project::{Project, ProjectItem, ProjectPath, Worktree};
|
||||||
use prompt_store::{PromptId, PromptStore};
|
|
||||||
use rope::Point;
|
use rope::Point;
|
||||||
use std::{
|
use std::{
|
||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
|
|
@ -61,21 +60,15 @@ pub struct MentionImage {
|
||||||
pub struct MentionSet {
|
pub struct MentionSet {
|
||||||
project: WeakEntity<Project>,
|
project: WeakEntity<Project>,
|
||||||
thread_store: Option<Entity<ThreadStore>>,
|
thread_store: Option<Entity<ThreadStore>>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
|
||||||
mentions: HashMap<CreaseId, (MentionUri, MentionTask)>,
|
mentions: HashMap<CreaseId, (MentionUri, MentionTask)>,
|
||||||
crease_entities: HashMap<CreaseId, Entity<LoadingContext>>,
|
crease_entities: HashMap<CreaseId, Entity<LoadingContext>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MentionSet {
|
impl MentionSet {
|
||||||
pub fn new(
|
pub fn new(project: WeakEntity<Project>, thread_store: Option<Entity<ThreadStore>>) -> Self {
|
||||||
project: WeakEntity<Project>,
|
|
||||||
thread_store: Option<Entity<ThreadStore>>,
|
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
project,
|
project,
|
||||||
thread_store,
|
thread_store,
|
||||||
prompt_store,
|
|
||||||
mentions: HashMap::default(),
|
mentions: HashMap::default(),
|
||||||
crease_entities: HashMap::default(),
|
crease_entities: HashMap::default(),
|
||||||
}
|
}
|
||||||
|
|
@ -153,7 +146,6 @@ impl MentionSet {
|
||||||
line_range,
|
line_range,
|
||||||
..
|
..
|
||||||
} => self.confirm_mention_for_symbol(abs_path, line_range, cx),
|
} => self.confirm_mention_for_symbol(abs_path, line_range, cx),
|
||||||
MentionUri::Rule { id, .. } => self.confirm_mention_for_rule(id, cx),
|
|
||||||
MentionUri::Skill {
|
MentionUri::Skill {
|
||||||
skill_file_path, ..
|
skill_file_path, ..
|
||||||
} => self.confirm_mention_for_skill(skill_file_path, cx),
|
} => self.confirm_mention_for_skill(skill_file_path, cx),
|
||||||
|
|
@ -327,7 +319,6 @@ impl MentionSet {
|
||||||
line_range,
|
line_range,
|
||||||
..
|
..
|
||||||
} => self.confirm_mention_for_symbol(abs_path, line_range, cx),
|
} => self.confirm_mention_for_symbol(abs_path, line_range, cx),
|
||||||
MentionUri::Rule { id, .. } => self.confirm_mention_for_rule(id, cx),
|
|
||||||
MentionUri::Skill {
|
MentionUri::Skill {
|
||||||
skill_file_path, ..
|
skill_file_path, ..
|
||||||
} => self.confirm_mention_for_skill(skill_file_path, cx),
|
} => self.confirm_mention_for_skill(skill_file_path, cx),
|
||||||
|
|
@ -515,24 +506,6 @@ impl MentionSet {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn confirm_mention_for_rule(
|
|
||||||
&mut self,
|
|
||||||
id: PromptId,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> Task<Result<Mention>> {
|
|
||||||
let Some(prompt_store) = self.prompt_store.as_ref() else {
|
|
||||||
return Task::ready(Err(anyhow!("Missing prompt store")));
|
|
||||||
};
|
|
||||||
let prompt = prompt_store.read(cx).load(id, cx);
|
|
||||||
cx.spawn(async move |_, _| {
|
|
||||||
let prompt = prompt.await?;
|
|
||||||
Ok(Mention::Text {
|
|
||||||
content: prompt,
|
|
||||||
tracked_buffers: Vec::new(),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn confirm_mention_for_selection(
|
pub fn confirm_mention_for_selection(
|
||||||
&mut self,
|
&mut self,
|
||||||
source_range: Range<text::Anchor>,
|
source_range: Range<text::Anchor>,
|
||||||
|
|
@ -773,7 +746,7 @@ mod tests {
|
||||||
fs.insert_tree("/project", json!({"file": ""})).await;
|
fs.insert_tree("/project", json!({"file": ""})).await;
|
||||||
let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
|
let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
|
||||||
let thread_store = None;
|
let thread_store = None;
|
||||||
let mention_set = cx.new(|_cx| MentionSet::new(project.downgrade(), thread_store, None));
|
let mention_set = cx.new(|_cx| MentionSet::new(project.downgrade(), thread_store));
|
||||||
|
|
||||||
let task = mention_set.update(cx, |mention_set, cx| {
|
let task = mention_set.update(cx, |mention_set, cx| {
|
||||||
mention_set.confirm_mention_for_thread(acp::SessionId::new("thread-1"), cx)
|
mention_set.confirm_mention_for_thread(acp::SessionId::new("thread-1"), cx)
|
||||||
|
|
@ -799,7 +772,7 @@ mod tests {
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
|
let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
|
||||||
let mention_set = cx.new(|_cx| MentionSet::new(project.downgrade(), None, None));
|
let mention_set = cx.new(|_cx| MentionSet::new(project.downgrade(), None));
|
||||||
|
|
||||||
let mention_task = mention_set.update(cx, |mention_set, cx| {
|
let mention_task = mention_set.update(cx, |mention_set, cx| {
|
||||||
let http_client = project.read(cx).client().http_client();
|
let http_client = project.read(cx).client().http_client();
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@ use project::AgentId;
|
||||||
use project::{
|
use project::{
|
||||||
CompletionIntent, InlayHint, InlayHintLabel, InlayId, Project, ProjectPath, Worktree,
|
CompletionIntent, InlayHint, InlayHintLabel, InlayId, Project, ProjectPath, Worktree,
|
||||||
};
|
};
|
||||||
use prompt_store::PromptStore;
|
|
||||||
use rope::Point;
|
use rope::Point;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::{cmp::min, fmt::Write, ops::Range, rc::Rc, sync::Arc};
|
use std::{cmp::min, fmt::Write, ops::Range, rc::Rc, sync::Arc};
|
||||||
|
|
@ -453,7 +452,6 @@ impl MessageEditor {
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
project: WeakEntity<Project>,
|
project: WeakEntity<Project>,
|
||||||
thread_store: Option<Entity<ThreadStore>>,
|
thread_store: Option<Entity<ThreadStore>>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
|
||||||
session_capabilities: SharedSessionCapabilities,
|
session_capabilities: SharedSessionCapabilities,
|
||||||
agent_id: AgentId,
|
agent_id: AgentId,
|
||||||
placeholder: &str,
|
placeholder: &str,
|
||||||
|
|
@ -506,8 +504,7 @@ impl MessageEditor {
|
||||||
|
|
||||||
editor
|
editor
|
||||||
});
|
});
|
||||||
let mention_set =
|
let mention_set = cx.new(|_cx| MentionSet::new(project, thread_store.clone()));
|
||||||
cx.new(|_cx| MentionSet::new(project, thread_store.clone(), prompt_store.clone()));
|
|
||||||
let completion_provider = Rc::new(PromptCompletionProvider::new(
|
let completion_provider = Rc::new(PromptCompletionProvider::new(
|
||||||
MessageEditorCompletionDelegate {
|
MessageEditorCompletionDelegate {
|
||||||
session_capabilities: session_capabilities.clone(),
|
session_capabilities: session_capabilities.clone(),
|
||||||
|
|
@ -2475,7 +2472,6 @@ mod tests {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
None,
|
|
||||||
Default::default(),
|
Default::default(),
|
||||||
"Test Agent".into(),
|
"Test Agent".into(),
|
||||||
"Test",
|
"Test",
|
||||||
|
|
@ -2576,7 +2572,6 @@ mod tests {
|
||||||
workspace_handle.clone(),
|
workspace_handle.clone(),
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
None,
|
|
||||||
session_capabilities.clone(),
|
session_capabilities.clone(),
|
||||||
"Claude Agent".into(),
|
"Claude Agent".into(),
|
||||||
"Test",
|
"Test",
|
||||||
|
|
@ -2742,7 +2737,6 @@ mod tests {
|
||||||
workspace_handle,
|
workspace_handle,
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
None,
|
|
||||||
session_capabilities.clone(),
|
session_capabilities.clone(),
|
||||||
"Test Agent".into(),
|
"Test Agent".into(),
|
||||||
"Test",
|
"Test",
|
||||||
|
|
@ -2915,7 +2909,6 @@ mod tests {
|
||||||
workspace_handle,
|
workspace_handle,
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
None,
|
None,
|
||||||
None,
|
|
||||||
session_capabilities.clone(),
|
session_capabilities.clone(),
|
||||||
"Test Agent".into(),
|
"Test Agent".into(),
|
||||||
"Test",
|
"Test",
|
||||||
|
|
@ -3064,7 +3057,6 @@ mod tests {
|
||||||
workspace_handle,
|
workspace_handle,
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
Some(thread_store),
|
Some(thread_store),
|
||||||
None,
|
|
||||||
session_capabilities.clone(),
|
session_capabilities.clone(),
|
||||||
"Test Agent".into(),
|
"Test Agent".into(),
|
||||||
"Test",
|
"Test",
|
||||||
|
|
@ -3556,7 +3548,6 @@ mod tests {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
None,
|
|
||||||
Default::default(),
|
Default::default(),
|
||||||
"Test Agent".into(),
|
"Test Agent".into(),
|
||||||
"Test",
|
"Test",
|
||||||
|
|
@ -3657,7 +3648,6 @@ mod tests {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
None,
|
|
||||||
Default::default(),
|
Default::default(),
|
||||||
"Test Agent".into(),
|
"Test Agent".into(),
|
||||||
"Test",
|
"Test",
|
||||||
|
|
@ -3726,7 +3716,6 @@ mod tests {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
None,
|
|
||||||
Default::default(),
|
Default::default(),
|
||||||
"Test Agent".into(),
|
"Test Agent".into(),
|
||||||
"Test",
|
"Test",
|
||||||
|
|
@ -3779,7 +3768,6 @@ mod tests {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
None,
|
|
||||||
Default::default(),
|
Default::default(),
|
||||||
"Test Agent".into(),
|
"Test Agent".into(),
|
||||||
"Test",
|
"Test",
|
||||||
|
|
@ -3836,7 +3824,6 @@ mod tests {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
None,
|
|
||||||
Default::default(),
|
Default::default(),
|
||||||
"Test Agent".into(),
|
"Test Agent".into(),
|
||||||
"Test",
|
"Test",
|
||||||
|
|
@ -3894,7 +3881,6 @@ mod tests {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
None,
|
|
||||||
Default::default(),
|
Default::default(),
|
||||||
"Test Agent".into(),
|
"Test Agent".into(),
|
||||||
"Test",
|
"Test",
|
||||||
|
|
@ -3956,7 +3942,6 @@ mod tests {
|
||||||
workspace_handle,
|
workspace_handle,
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
None,
|
|
||||||
Default::default(),
|
Default::default(),
|
||||||
"Test Agent".into(),
|
"Test Agent".into(),
|
||||||
"Test",
|
"Test",
|
||||||
|
|
@ -4116,7 +4101,6 @@ mod tests {
|
||||||
workspace_handle,
|
workspace_handle,
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
None,
|
|
||||||
Default::default(),
|
Default::default(),
|
||||||
"Test Agent".into(),
|
"Test Agent".into(),
|
||||||
"Test",
|
"Test",
|
||||||
|
|
@ -4236,7 +4220,6 @@ mod tests {
|
||||||
workspace_handle,
|
workspace_handle,
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
Some(thread_store.clone()),
|
Some(thread_store.clone()),
|
||||||
None,
|
|
||||||
Default::default(),
|
Default::default(),
|
||||||
"Test Agent".into(),
|
"Test Agent".into(),
|
||||||
"Test",
|
"Test",
|
||||||
|
|
@ -4315,7 +4298,6 @@ mod tests {
|
||||||
workspace_handle,
|
workspace_handle,
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
Some(thread_store),
|
Some(thread_store),
|
||||||
None,
|
|
||||||
Default::default(),
|
Default::default(),
|
||||||
"Test Agent".into(),
|
"Test Agent".into(),
|
||||||
"Test",
|
"Test",
|
||||||
|
|
@ -4493,7 +4475,6 @@ mod tests {
|
||||||
workspace_handle,
|
workspace_handle,
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
Some(thread_store),
|
Some(thread_store),
|
||||||
None,
|
|
||||||
Default::default(),
|
Default::default(),
|
||||||
"Test Agent".into(),
|
"Test Agent".into(),
|
||||||
"Test",
|
"Test",
|
||||||
|
|
@ -4905,7 +4886,6 @@ mod tests {
|
||||||
workspace_handle,
|
workspace_handle,
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
Some(thread_store),
|
Some(thread_store),
|
||||||
None,
|
|
||||||
Default::default(),
|
Default::default(),
|
||||||
"Test Agent".into(),
|
"Test Agent".into(),
|
||||||
"Test",
|
"Test",
|
||||||
|
|
@ -5160,7 +5140,6 @@ mod tests {
|
||||||
workspace_handle,
|
workspace_handle,
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
Some(thread_store),
|
Some(thread_store),
|
||||||
None,
|
|
||||||
Default::default(),
|
Default::default(),
|
||||||
"Test Agent".into(),
|
"Test Agent".into(),
|
||||||
"Test",
|
"Test",
|
||||||
|
|
@ -5253,7 +5232,6 @@ mod tests {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
None,
|
None,
|
||||||
None,
|
|
||||||
Default::default(),
|
Default::default(),
|
||||||
"Test Agent".into(),
|
"Test Agent".into(),
|
||||||
"Test",
|
"Test",
|
||||||
|
|
@ -5402,7 +5380,6 @@ mod tests {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
None,
|
None,
|
||||||
None,
|
|
||||||
Default::default(),
|
Default::default(),
|
||||||
"Test Agent".into(),
|
"Test Agent".into(),
|
||||||
"Test",
|
"Test",
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ use language_models::provider::anthropic::telemetry::{
|
||||||
AnthropicCompletionType, AnthropicEventData, AnthropicEventType, report_anthropic_event,
|
AnthropicCompletionType, AnthropicEventData, AnthropicEventType, report_anthropic_event,
|
||||||
};
|
};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use prompt_store::{PromptBuilder, PromptStore};
|
use prompt_store::PromptBuilder;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use terminal_view::TerminalView;
|
use terminal_view::TerminalView;
|
||||||
use ui::prelude::*;
|
use ui::prelude::*;
|
||||||
|
|
@ -64,7 +64,6 @@ impl TerminalInlineAssistant {
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
project: WeakEntity<Project>,
|
project: WeakEntity<Project>,
|
||||||
thread_store: Entity<ThreadStore>,
|
thread_store: Entity<ThreadStore>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
|
||||||
initial_prompt: Option<String>,
|
initial_prompt: Option<String>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
|
|
@ -89,7 +88,6 @@ impl TerminalInlineAssistant {
|
||||||
session_id,
|
session_id,
|
||||||
self.fs.clone(),
|
self.fs.clone(),
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
prompt_store.clone(),
|
|
||||||
project.clone(),
|
project.clone(),
|
||||||
workspace.clone(),
|
workspace.clone(),
|
||||||
window,
|
window,
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,76 @@ impl TerminalThreadMetadata {
|
||||||
pub fn main_worktree_paths(&self) -> &PathList {
|
pub fn main_worktree_paths(&self) -> &PathList {
|
||||||
self.worktree_paths.main_worktree_path_list()
|
self.worktree_paths.main_worktree_path_list()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn display_title(&self) -> SharedString {
|
||||||
|
compose_terminal_thread_title(
|
||||||
|
self.title.as_ref(),
|
||||||
|
self.custom_title.as_ref().map(|title| title.as_ref()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn compose_terminal_thread_title(
|
||||||
|
terminal_title: &str,
|
||||||
|
custom_title: Option<&str>,
|
||||||
|
) -> SharedString {
|
||||||
|
let Some(custom_title) = custom_title.filter(|title| !title.trim().is_empty()) else {
|
||||||
|
return SharedString::from(terminal_title.to_string());
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(prefix) = terminal_title_prefix(terminal_title) {
|
||||||
|
SharedString::from(format!("{prefix}{custom_title}"))
|
||||||
|
} else {
|
||||||
|
SharedString::from(custom_title.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn terminal_title_without_prefix(title: &str) -> &str {
|
||||||
|
terminal_title_prefix(title)
|
||||||
|
.map(|prefix| &title[prefix.len()..])
|
||||||
|
.unwrap_or(title)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn terminal_title_prefix(title: &str) -> Option<&str> {
|
||||||
|
let mut prefix_byte_len = 0;
|
||||||
|
let mut saw_prefix_character = false;
|
||||||
|
let mut saw_whitespace_after_prefix = false;
|
||||||
|
|
||||||
|
let mut chars = title.chars().peekable();
|
||||||
|
while let Some(character) = chars.next() {
|
||||||
|
if character.is_alphanumeric() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if character.is_whitespace() {
|
||||||
|
if !saw_prefix_character {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
prefix_byte_len += character.len_utf8();
|
||||||
|
saw_whitespace_after_prefix = true;
|
||||||
|
|
||||||
|
while let Some(character) = chars.peek() {
|
||||||
|
if !character.is_whitespace() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
prefix_byte_len += character.len_utf8();
|
||||||
|
chars.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
saw_prefix_character = true;
|
||||||
|
prefix_byte_len += character.len_utf8();
|
||||||
|
}
|
||||||
|
|
||||||
|
if saw_whitespace_after_prefix {
|
||||||
|
Some(&title[..prefix_byte_len])
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TerminalThreadMetadataStore {
|
pub struct TerminalThreadMetadataStore {
|
||||||
|
|
@ -563,6 +633,32 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_terminal_title_prefix_preserves_non_alphanumeric_prefixes() {
|
||||||
|
assert_eq!(terminal_title_prefix("✳ Thinking"), Some("✳ "));
|
||||||
|
assert_eq!(terminal_title_prefix(">>> Thinking"), Some(">>> "));
|
||||||
|
assert_eq!(terminal_title_prefix("⠋ Running"), Some("⠋ "));
|
||||||
|
assert_eq!(terminal_title_prefix("* Claude"), Some("* "));
|
||||||
|
assert_eq!(terminal_title_prefix("✳Thinking"), None);
|
||||||
|
assert_eq!(terminal_title_prefix("Thinking"), None);
|
||||||
|
assert_eq!(terminal_title_prefix(" Thinking"), None);
|
||||||
|
assert_eq!(terminal_title_prefix("✳"), None);
|
||||||
|
assert_eq!(terminal_title_prefix("v1 Running"), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_terminal_thread_display_title_combines_raw_and_custom_titles() {
|
||||||
|
let mut metadata = metadata(
|
||||||
|
"⠋ Thinking",
|
||||||
|
WorktreePaths::from_folder_paths(&PathList::default()),
|
||||||
|
);
|
||||||
|
metadata.custom_title = Some("Fix bug".into());
|
||||||
|
assert_eq!(metadata.display_title().as_ref(), "⠋ Fix bug");
|
||||||
|
|
||||||
|
metadata.title = "Thinking".into();
|
||||||
|
assert_eq!(metadata.display_title().as_ref(), "Fix bug");
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_change_worktree_paths_reindexes_terminal_metadata(cx: &mut TestAppContext) {
|
async fn test_change_worktree_paths_reindexes_terminal_metadata(cx: &mut TestAppContext) {
|
||||||
init_test(cx);
|
init_test(cx);
|
||||||
|
|
|
||||||
|
|
@ -1888,7 +1888,7 @@ mod tests {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let mut vcx = VisualTestContext::from_window(multi_workspace.into(), cx);
|
let mut vcx = VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
let panel = workspace_entity.update_in(&mut vcx, |workspace, window, cx| {
|
let panel = workspace_entity.update_in(&mut vcx, |workspace, window, cx| {
|
||||||
cx.new(|cx| crate::AgentPanel::new(workspace, None, window, cx))
|
cx.new(|cx| crate::AgentPanel::new(workspace, window, cx))
|
||||||
});
|
});
|
||||||
(panel, vcx)
|
(panel, vcx)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -103,13 +103,16 @@ impl Component for EndTrialUpsell {
|
||||||
"End of Trial Upsell Banner"
|
"End of Trial Upsell Banner"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
|
fn description() -> &'static str {
|
||||||
Some(
|
"A banner shown in the agent panel when a user's trial has ended, \
|
||||||
|
inviting them to upgrade to a paid plan to continue using the agent."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
|
||||||
v_flex()
|
v_flex()
|
||||||
.child(EndTrialUpsell {
|
.child(EndTrialUpsell {
|
||||||
dismiss_upsell: Arc::new(|_, _| {}),
|
dismiss_upsell: Arc::new(|_, _| {}),
|
||||||
})
|
})
|
||||||
.into_any_element(),
|
.into_any_element()
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ use gpui::{
|
||||||
pulsating_between,
|
pulsating_between,
|
||||||
};
|
};
|
||||||
use language::Buffer;
|
use language::Buffer;
|
||||||
use prompt_store::PromptId;
|
|
||||||
use rope::Point;
|
use rope::Point;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use theme_settings::ThemeSettings;
|
use theme_settings::ThemeSettings;
|
||||||
|
|
@ -195,9 +194,6 @@ fn open_mention_uri(
|
||||||
MentionUri::Thread { id, name } => {
|
MentionUri::Thread { id, name } => {
|
||||||
open_thread(workspace, id, name, window, cx);
|
open_thread(workspace, id, name, window, cx);
|
||||||
}
|
}
|
||||||
MentionUri::Rule { id, .. } => {
|
|
||||||
open_rule(workspace, id, window, cx);
|
|
||||||
}
|
|
||||||
MentionUri::Skill {
|
MentionUri::Skill {
|
||||||
skill_file_path, ..
|
skill_file_path, ..
|
||||||
} => {
|
} => {
|
||||||
|
|
@ -360,23 +356,3 @@ fn open_thread(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open_rule(
|
|
||||||
_workspace: &mut Workspace,
|
|
||||||
id: PromptId,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Workspace>,
|
|
||||||
) {
|
|
||||||
use zed_actions::assistant::OpenRulesLibrary;
|
|
||||||
|
|
||||||
let PromptId::User { uuid } = id else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
window.dispatch_action(
|
|
||||||
Box::new(OpenRulesLibrary {
|
|
||||||
prompt_to_select: Some(uuid.0),
|
|
||||||
}),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -376,7 +376,13 @@ impl Component for ZedAiOnboarding {
|
||||||
"Agent New User Onboarding"
|
"Agent New User Onboarding"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
|
fn description() -> &'static str {
|
||||||
|
"The onboarding surface shown to new agent panel users, \
|
||||||
|
guiding them through signing in to Zed and selecting a plan \
|
||||||
|
before they can start using the agent."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
|
||||||
fn onboarding(
|
fn onboarding(
|
||||||
sign_in_status: SignInStatus,
|
sign_in_status: SignInStatus,
|
||||||
plan: Option<Plan>,
|
plan: Option<Plan>,
|
||||||
|
|
@ -402,7 +408,6 @@ impl Component for ZedAiOnboarding {
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.min_w_0()
|
.min_w_0()
|
||||||
.gap_4()
|
.gap_4()
|
||||||
|
|
@ -436,7 +441,6 @@ impl Component for ZedAiOnboarding {
|
||||||
onboarding(SignInStatus::SignedIn, Some(Plan::ZedStudent), false),
|
onboarding(SignInStatus::SignedIn, Some(Plan::ZedStudent), false),
|
||||||
),
|
),
|
||||||
])
|
])
|
||||||
.into_any_element(),
|
.into_any_element()
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -122,9 +122,6 @@ impl Model {
|
||||||
|
|
||||||
let mut supported_effort_levels = Vec::new();
|
let mut supported_effort_levels = Vec::new();
|
||||||
if let Some(effort) = entry.capabilities.as_ref().and_then(|e| e.effort.as_ref()) {
|
if let Some(effort) = entry.capabilities.as_ref().and_then(|e| e.effort.as_ref()) {
|
||||||
// The `xhigh` effort level reported by the API has no
|
|
||||||
// corresponding `Effort` variant in the request enum, so it is
|
|
||||||
// intentionally dropped here.
|
|
||||||
for (level, supported) in [
|
for (level, supported) in [
|
||||||
(Effort::Low, effort.low.as_ref()),
|
(Effort::Low, effort.low.as_ref()),
|
||||||
(Effort::Medium, effort.medium.as_ref()),
|
(Effort::Medium, effort.medium.as_ref()),
|
||||||
|
|
@ -148,7 +145,10 @@ impl Model {
|
||||||
AnthropicModelMode::Default
|
AnthropicModelMode::Default
|
||||||
};
|
};
|
||||||
|
|
||||||
let supports_speed = matches!(entry.id.as_str(), "claude-opus-4-6" | "claude-opus-4-7");
|
let supports_speed = matches!(
|
||||||
|
entry.id.as_str(),
|
||||||
|
"claude-opus-4-6" | "claude-opus-4-7" | "claude-opus-4-8"
|
||||||
|
);
|
||||||
|
|
||||||
let mut extra_beta_headers = Vec::new();
|
let mut extra_beta_headers = Vec::new();
|
||||||
if supports_speed {
|
if supports_speed {
|
||||||
|
|
@ -676,6 +676,8 @@ pub enum Effort {
|
||||||
Low,
|
Low,
|
||||||
Medium,
|
Medium,
|
||||||
High,
|
High,
|
||||||
|
#[serde(rename = "xhigh")]
|
||||||
|
#[strum(serialize = "xhigh")]
|
||||||
XHigh,
|
XHigh,
|
||||||
Max,
|
Max,
|
||||||
}
|
}
|
||||||
|
|
@ -1056,6 +1058,17 @@ mod tests {
|
||||||
assert_eq!(model.mode, AnthropicModelMode::Default);
|
assert_eq!(model.mode, AnthropicModelMode::Default);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_listed_enables_fast_mode_for_opus_4_8() {
|
||||||
|
let model = Model::from_listed(listed_entry(
|
||||||
|
"claude-opus-4-8",
|
||||||
|
ModelCapabilities::default(),
|
||||||
|
));
|
||||||
|
|
||||||
|
assert!(model.supports_speed);
|
||||||
|
assert_eq!(model.beta_headers().as_deref(), Some(FAST_MODE_BETA_HEADER));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn from_listed_collects_supported_effort_levels() {
|
fn from_listed_collects_supported_effort_levels() {
|
||||||
let entry = listed_entry(
|
let entry = listed_entry(
|
||||||
|
|
|
||||||
|
|
@ -300,6 +300,7 @@ pub fn into_anthropic(
|
||||||
"low" => Some(crate::Effort::Low),
|
"low" => Some(crate::Effort::Low),
|
||||||
"medium" => Some(crate::Effort::Medium),
|
"medium" => Some(crate::Effort::Medium),
|
||||||
"high" => Some(crate::Effort::High),
|
"high" => Some(crate::Effort::High),
|
||||||
|
"xhigh" => Some(crate::Effort::XHigh),
|
||||||
"max" => Some(crate::Effort::Max),
|
"max" => Some(crate::Effort::Max),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
|
@ -705,6 +706,44 @@ mod tests {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_xhigh_effort_is_serialized_for_adaptive_thinking() {
|
||||||
|
let request = LanguageModelRequest {
|
||||||
|
messages: vec![LanguageModelRequestMessage {
|
||||||
|
role: Role::User,
|
||||||
|
content: vec![MessageContent::Text("Hi".to_string())],
|
||||||
|
cache: false,
|
||||||
|
reasoning_details: None,
|
||||||
|
}],
|
||||||
|
thread_id: None,
|
||||||
|
prompt_id: None,
|
||||||
|
intent: None,
|
||||||
|
stop: vec![],
|
||||||
|
temperature: None,
|
||||||
|
tools: vec![],
|
||||||
|
tool_choice: None,
|
||||||
|
thinking_allowed: true,
|
||||||
|
thinking_effort: Some("xhigh".into()),
|
||||||
|
speed: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let anthropic_request = into_anthropic(
|
||||||
|
request,
|
||||||
|
"claude-opus-4-8".to_string(),
|
||||||
|
1.0,
|
||||||
|
128_000,
|
||||||
|
AnthropicModelMode::AdaptiveThinking,
|
||||||
|
AnthropicPromptCacheMode::Automatic,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
anthropic_request
|
||||||
|
.output_config
|
||||||
|
.and_then(|config| config.effort),
|
||||||
|
Some(crate::Effort::XHigh)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_no_cache_control_when_caching_disabled() {
|
fn test_no_cache_control_when_caching_disabled() {
|
||||||
let request = LanguageModelRequest {
|
let request = LanguageModelRequest {
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,12 @@ mod real_implementation {
|
||||||
|
|
||||||
impl Default for EchoCanceller {
|
impl Default for EchoCanceller {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self(Arc::new(Mutex::new(apm::AudioProcessingModule::new(
|
// Sound-effect playback only feeds this APM through `process_reverse_stream`
|
||||||
true, false, false, false,
|
// for AEC reference; gain/HPF/NS would be no-ops here, so we keep the
|
||||||
))))
|
// original (echo only) configuration via the legacy flag form.
|
||||||
|
Self(Arc::new(Mutex::new(
|
||||||
|
apm::AudioProcessingModule::from_flags(true, false, false, false),
|
||||||
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ pub enum BedrockAdaptiveThinkingEffort {
|
||||||
Medium,
|
Medium,
|
||||||
#[default]
|
#[default]
|
||||||
High,
|
High,
|
||||||
|
XHigh,
|
||||||
Max,
|
Max,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -17,6 +18,7 @@ impl BedrockAdaptiveThinkingEffort {
|
||||||
Self::Low => "low",
|
Self::Low => "low",
|
||||||
Self::Medium => "medium",
|
Self::Medium => "medium",
|
||||||
Self::High => "high",
|
Self::High => "high",
|
||||||
|
Self::XHigh => "xhigh",
|
||||||
Self::Max => "max",
|
Self::Max => "max",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -91,6 +93,13 @@ pub enum Model {
|
||||||
alias = "claude-opus-4-7-thinking-latest"
|
alias = "claude-opus-4-7-thinking-latest"
|
||||||
)]
|
)]
|
||||||
ClaudeOpus4_7,
|
ClaudeOpus4_7,
|
||||||
|
#[serde(
|
||||||
|
rename = "claude-opus-4-8",
|
||||||
|
alias = "claude-opus-4-8-latest",
|
||||||
|
alias = "claude-opus-4-8-thinking",
|
||||||
|
alias = "claude-opus-4-8-thinking-latest"
|
||||||
|
)]
|
||||||
|
ClaudeOpus4_8,
|
||||||
#[serde(
|
#[serde(
|
||||||
rename = "claude-sonnet-4-6",
|
rename = "claude-sonnet-4-6",
|
||||||
alias = "claude-sonnet-4-6-latest",
|
alias = "claude-sonnet-4-6-latest",
|
||||||
|
|
@ -210,7 +219,9 @@ impl Model {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_id(id: &str) -> anyhow::Result<Self> {
|
pub fn from_id(id: &str) -> anyhow::Result<Self> {
|
||||||
if id.starts_with("claude-opus-4-7") {
|
if id.starts_with("claude-opus-4-8") {
|
||||||
|
Ok(Self::ClaudeOpus4_8)
|
||||||
|
} else if id.starts_with("claude-opus-4-7") {
|
||||||
Ok(Self::ClaudeOpus4_7)
|
Ok(Self::ClaudeOpus4_7)
|
||||||
} else if id.starts_with("claude-opus-4-6") {
|
} else if id.starts_with("claude-opus-4-6") {
|
||||||
Ok(Self::ClaudeOpus4_6)
|
Ok(Self::ClaudeOpus4_6)
|
||||||
|
|
@ -240,6 +251,7 @@ impl Model {
|
||||||
Self::ClaudeOpus4_5 => "claude-opus-4-5",
|
Self::ClaudeOpus4_5 => "claude-opus-4-5",
|
||||||
Self::ClaudeOpus4_6 => "claude-opus-4-6",
|
Self::ClaudeOpus4_6 => "claude-opus-4-6",
|
||||||
Self::ClaudeOpus4_7 => "claude-opus-4-7",
|
Self::ClaudeOpus4_7 => "claude-opus-4-7",
|
||||||
|
Self::ClaudeOpus4_8 => "claude-opus-4-8",
|
||||||
Self::ClaudeSonnet4_6 => "claude-sonnet-4-6",
|
Self::ClaudeSonnet4_6 => "claude-sonnet-4-6",
|
||||||
Self::Llama4Scout17B => "llama-4-scout-17b",
|
Self::Llama4Scout17B => "llama-4-scout-17b",
|
||||||
Self::Llama4Maverick17B => "llama-4-maverick-17b",
|
Self::Llama4Maverick17B => "llama-4-maverick-17b",
|
||||||
|
|
@ -290,6 +302,7 @@ impl Model {
|
||||||
Self::ClaudeOpus4_5 => "anthropic.claude-opus-4-5-20251101-v1:0",
|
Self::ClaudeOpus4_5 => "anthropic.claude-opus-4-5-20251101-v1:0",
|
||||||
Self::ClaudeOpus4_6 => "anthropic.claude-opus-4-6-v1",
|
Self::ClaudeOpus4_6 => "anthropic.claude-opus-4-6-v1",
|
||||||
Self::ClaudeOpus4_7 => "anthropic.claude-opus-4-7",
|
Self::ClaudeOpus4_7 => "anthropic.claude-opus-4-7",
|
||||||
|
Self::ClaudeOpus4_8 => "anthropic.claude-opus-4-8",
|
||||||
Self::ClaudeSonnet4_6 => "anthropic.claude-sonnet-4-6",
|
Self::ClaudeSonnet4_6 => "anthropic.claude-sonnet-4-6",
|
||||||
Self::Llama4Scout17B => "meta.llama4-scout-17b-instruct-v1:0",
|
Self::Llama4Scout17B => "meta.llama4-scout-17b-instruct-v1:0",
|
||||||
Self::Llama4Maverick17B => "meta.llama4-maverick-17b-instruct-v1:0",
|
Self::Llama4Maverick17B => "meta.llama4-maverick-17b-instruct-v1:0",
|
||||||
|
|
@ -340,6 +353,7 @@ impl Model {
|
||||||
Self::ClaudeOpus4_5 => "Claude Opus 4.5",
|
Self::ClaudeOpus4_5 => "Claude Opus 4.5",
|
||||||
Self::ClaudeOpus4_6 => "Claude Opus 4.6",
|
Self::ClaudeOpus4_6 => "Claude Opus 4.6",
|
||||||
Self::ClaudeOpus4_7 => "Claude Opus 4.7",
|
Self::ClaudeOpus4_7 => "Claude Opus 4.7",
|
||||||
|
Self::ClaudeOpus4_8 => "Claude Opus 4.8",
|
||||||
Self::ClaudeSonnet4_6 => "Claude Sonnet 4.6",
|
Self::ClaudeSonnet4_6 => "Claude Sonnet 4.6",
|
||||||
Self::Llama4Scout17B => "Llama 4 Scout 17B",
|
Self::Llama4Scout17B => "Llama 4 Scout 17B",
|
||||||
Self::Llama4Maverick17B => "Llama 4 Maverick 17B",
|
Self::Llama4Maverick17B => "Llama 4 Maverick 17B",
|
||||||
|
|
@ -391,6 +405,7 @@ impl Model {
|
||||||
| Self::ClaudeOpus4_5
|
| Self::ClaudeOpus4_5
|
||||||
| Self::ClaudeOpus4_6
|
| Self::ClaudeOpus4_6
|
||||||
| Self::ClaudeOpus4_7
|
| Self::ClaudeOpus4_7
|
||||||
|
| Self::ClaudeOpus4_8
|
||||||
| Self::ClaudeSonnet4_6 => 1_000_000,
|
| Self::ClaudeSonnet4_6 => 1_000_000,
|
||||||
Self::ClaudeOpus4_1 => 200_000,
|
Self::ClaudeOpus4_1 => 200_000,
|
||||||
Self::Llama4Scout17B | Self::Llama4Maverick17B => 128_000,
|
Self::Llama4Scout17B | Self::Llama4Maverick17B => 128_000,
|
||||||
|
|
@ -425,7 +440,7 @@ impl Model {
|
||||||
| Self::ClaudeOpus4_5
|
| Self::ClaudeOpus4_5
|
||||||
| Self::ClaudeSonnet4_6 => 64_000,
|
| Self::ClaudeSonnet4_6 => 64_000,
|
||||||
Self::ClaudeOpus4_1 => 32_000,
|
Self::ClaudeOpus4_1 => 32_000,
|
||||||
Self::ClaudeOpus4_6 | Self::ClaudeOpus4_7 => 128_000,
|
Self::ClaudeOpus4_6 | Self::ClaudeOpus4_7 | Self::ClaudeOpus4_8 => 128_000,
|
||||||
Self::Llama4Scout17B
|
Self::Llama4Scout17B
|
||||||
| Self::Llama4Maverick17B
|
| Self::Llama4Maverick17B
|
||||||
| Self::Gemma3_4B
|
| Self::Gemma3_4B
|
||||||
|
|
@ -464,6 +479,7 @@ impl Model {
|
||||||
| Self::ClaudeOpus4_5
|
| Self::ClaudeOpus4_5
|
||||||
| Self::ClaudeOpus4_6
|
| Self::ClaudeOpus4_6
|
||||||
| Self::ClaudeOpus4_7
|
| Self::ClaudeOpus4_7
|
||||||
|
| Self::ClaudeOpus4_8
|
||||||
| Self::ClaudeSonnet4_6 => 1.0,
|
| Self::ClaudeSonnet4_6 => 1.0,
|
||||||
Self::Custom {
|
Self::Custom {
|
||||||
default_temperature,
|
default_temperature,
|
||||||
|
|
@ -482,6 +498,7 @@ impl Model {
|
||||||
| Self::ClaudeOpus4_5
|
| Self::ClaudeOpus4_5
|
||||||
| Self::ClaudeOpus4_6
|
| Self::ClaudeOpus4_6
|
||||||
| Self::ClaudeOpus4_7
|
| Self::ClaudeOpus4_7
|
||||||
|
| Self::ClaudeOpus4_8
|
||||||
| Self::ClaudeSonnet4_6 => true,
|
| Self::ClaudeSonnet4_6 => true,
|
||||||
Self::NovaLite | Self::NovaPro | Self::NovaPremier | Self::Nova2Lite => true,
|
Self::NovaLite | Self::NovaPro | Self::NovaPremier | Self::Nova2Lite => true,
|
||||||
Self::MistralLarge3 | Self::PixtralLarge | Self::MagistralSmall => true,
|
Self::MistralLarge3 | Self::PixtralLarge | Self::MagistralSmall => true,
|
||||||
|
|
@ -513,6 +530,7 @@ impl Model {
|
||||||
| Self::ClaudeOpus4_5
|
| Self::ClaudeOpus4_5
|
||||||
| Self::ClaudeOpus4_6
|
| Self::ClaudeOpus4_6
|
||||||
| Self::ClaudeOpus4_7
|
| Self::ClaudeOpus4_7
|
||||||
|
| Self::ClaudeOpus4_8
|
||||||
| Self::ClaudeSonnet4_6 => true,
|
| Self::ClaudeSonnet4_6 => true,
|
||||||
Self::NovaLite | Self::NovaPro => true,
|
Self::NovaLite | Self::NovaPro => true,
|
||||||
Self::PixtralLarge => true,
|
Self::PixtralLarge => true,
|
||||||
|
|
@ -531,6 +549,7 @@ impl Model {
|
||||||
| Self::ClaudeOpus4_5
|
| Self::ClaudeOpus4_5
|
||||||
| Self::ClaudeOpus4_6
|
| Self::ClaudeOpus4_6
|
||||||
| Self::ClaudeOpus4_7
|
| Self::ClaudeOpus4_7
|
||||||
|
| Self::ClaudeOpus4_8
|
||||||
| Self::ClaudeSonnet4_6 => true,
|
| Self::ClaudeSonnet4_6 => true,
|
||||||
Self::Custom {
|
Self::Custom {
|
||||||
cache_configuration,
|
cache_configuration,
|
||||||
|
|
@ -550,6 +569,7 @@ impl Model {
|
||||||
| Self::ClaudeOpus4_5
|
| Self::ClaudeOpus4_5
|
||||||
| Self::ClaudeOpus4_6
|
| Self::ClaudeOpus4_6
|
||||||
| Self::ClaudeOpus4_7
|
| Self::ClaudeOpus4_7
|
||||||
|
| Self::ClaudeOpus4_8
|
||||||
| Self::ClaudeSonnet4_6
|
| Self::ClaudeSonnet4_6
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -557,10 +577,14 @@ impl Model {
|
||||||
pub fn supports_adaptive_thinking(&self) -> bool {
|
pub fn supports_adaptive_thinking(&self) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
self,
|
self,
|
||||||
Self::ClaudeOpus4_6 | Self::ClaudeOpus4_7 | Self::ClaudeSonnet4_6
|
Self::ClaudeOpus4_6 | Self::ClaudeOpus4_7 | Self::ClaudeOpus4_8 | Self::ClaudeSonnet4_6
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn supports_xhigh_adaptive_thinking(&self) -> bool {
|
||||||
|
matches!(self, Self::ClaudeOpus4_8)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn thinking_mode(&self) -> BedrockModelMode {
|
pub fn thinking_mode(&self) -> BedrockModelMode {
|
||||||
if self.supports_adaptive_thinking() {
|
if self.supports_adaptive_thinking() {
|
||||||
BedrockModelMode::AdaptiveThinking {
|
BedrockModelMode::AdaptiveThinking {
|
||||||
|
|
@ -590,6 +614,7 @@ impl Model {
|
||||||
| Self::ClaudeOpus4_5
|
| Self::ClaudeOpus4_5
|
||||||
| Self::ClaudeOpus4_6
|
| Self::ClaudeOpus4_6
|
||||||
| Self::ClaudeOpus4_7
|
| Self::ClaudeOpus4_7
|
||||||
|
| Self::ClaudeOpus4_8
|
||||||
| Self::ClaudeSonnet4_6
|
| Self::ClaudeSonnet4_6
|
||||||
| Self::Nova2Lite
|
| Self::Nova2Lite
|
||||||
);
|
);
|
||||||
|
|
@ -650,6 +675,7 @@ impl Model {
|
||||||
| Self::ClaudeOpus4_5
|
| Self::ClaudeOpus4_5
|
||||||
| Self::ClaudeOpus4_6
|
| Self::ClaudeOpus4_6
|
||||||
| Self::ClaudeOpus4_7
|
| Self::ClaudeOpus4_7
|
||||||
|
| Self::ClaudeOpus4_8
|
||||||
| Self::ClaudeSonnet4_6
|
| Self::ClaudeSonnet4_6
|
||||||
| Self::Nova2Lite,
|
| Self::Nova2Lite,
|
||||||
"global",
|
"global",
|
||||||
|
|
@ -667,6 +693,7 @@ impl Model {
|
||||||
| Self::ClaudeOpus4_5
|
| Self::ClaudeOpus4_5
|
||||||
| Self::ClaudeOpus4_6
|
| Self::ClaudeOpus4_6
|
||||||
| Self::ClaudeOpus4_7
|
| Self::ClaudeOpus4_7
|
||||||
|
| Self::ClaudeOpus4_8
|
||||||
| Self::ClaudeSonnet4_6
|
| Self::ClaudeSonnet4_6
|
||||||
| Self::Llama4Scout17B
|
| Self::Llama4Scout17B
|
||||||
| Self::Llama4Maverick17B
|
| Self::Llama4Maverick17B
|
||||||
|
|
@ -689,6 +716,7 @@ impl Model {
|
||||||
| Self::ClaudeSonnet4_5
|
| Self::ClaudeSonnet4_5
|
||||||
| Self::ClaudeOpus4_6
|
| Self::ClaudeOpus4_6
|
||||||
| Self::ClaudeOpus4_7
|
| Self::ClaudeOpus4_7
|
||||||
|
| Self::ClaudeOpus4_8
|
||||||
| Self::ClaudeSonnet4_6
|
| Self::ClaudeSonnet4_6
|
||||||
| Self::NovaLite
|
| Self::NovaLite
|
||||||
| Self::NovaPro
|
| Self::NovaPro
|
||||||
|
|
@ -702,6 +730,7 @@ impl Model {
|
||||||
| Self::ClaudeSonnet4_5
|
| Self::ClaudeSonnet4_5
|
||||||
| Self::ClaudeOpus4_6
|
| Self::ClaudeOpus4_6
|
||||||
| Self::ClaudeOpus4_7
|
| Self::ClaudeOpus4_7
|
||||||
|
| Self::ClaudeOpus4_8
|
||||||
| Self::ClaudeSonnet4_6,
|
| Self::ClaudeSonnet4_6,
|
||||||
"au",
|
"au",
|
||||||
) => Ok(format!("{}.{}", region_group, model_id)),
|
) => Ok(format!("{}.{}", region_group, model_id)),
|
||||||
|
|
@ -779,6 +808,10 @@ mod tests {
|
||||||
Model::ClaudeOpus4_7.cross_region_inference_id("eu-west-1", false)?,
|
Model::ClaudeOpus4_7.cross_region_inference_id("eu-west-1", false)?,
|
||||||
"eu.anthropic.claude-opus-4-7"
|
"eu.anthropic.claude-opus-4-7"
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Model::ClaudeOpus4_8.cross_region_inference_id("eu-west-1", false)?,
|
||||||
|
"eu.anthropic.claude-opus-4-8"
|
||||||
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -813,6 +846,10 @@ mod tests {
|
||||||
Model::ClaudeOpus4_7.cross_region_inference_id("ap-southeast-2", false)?,
|
Model::ClaudeOpus4_7.cross_region_inference_id("ap-southeast-2", false)?,
|
||||||
"au.anthropic.claude-opus-4-7"
|
"au.anthropic.claude-opus-4-7"
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Model::ClaudeOpus4_8.cross_region_inference_id("ap-southeast-2", false)?,
|
||||||
|
"au.anthropic.claude-opus-4-8"
|
||||||
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -877,6 +914,10 @@ mod tests {
|
||||||
Model::ClaudeOpus4_7.cross_region_inference_id("us-east-1", true)?,
|
Model::ClaudeOpus4_7.cross_region_inference_id("us-east-1", true)?,
|
||||||
"global.anthropic.claude-opus-4-7"
|
"global.anthropic.claude-opus-4-7"
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Model::ClaudeOpus4_8.cross_region_inference_id("us-east-1", true)?,
|
||||||
|
"global.anthropic.claude-opus-4-8"
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Model::Nova2Lite.cross_region_inference_id("us-east-1", true)?,
|
Model::Nova2Lite.cross_region_inference_id("us-east-1", true)?,
|
||||||
"global.amazon.nova-2-lite-v1:0"
|
"global.amazon.nova-2-lite-v1:0"
|
||||||
|
|
@ -978,6 +1019,9 @@ mod tests {
|
||||||
assert!(!Model::ClaudeSonnet4.supports_adaptive_thinking());
|
assert!(!Model::ClaudeSonnet4.supports_adaptive_thinking());
|
||||||
assert!(Model::ClaudeOpus4_6.supports_adaptive_thinking());
|
assert!(Model::ClaudeOpus4_6.supports_adaptive_thinking());
|
||||||
assert!(Model::ClaudeSonnet4_6.supports_adaptive_thinking());
|
assert!(Model::ClaudeSonnet4_6.supports_adaptive_thinking());
|
||||||
|
assert!(!Model::ClaudeOpus4_7.supports_xhigh_adaptive_thinking());
|
||||||
|
assert!(Model::ClaudeOpus4_8.supports_xhigh_adaptive_thinking());
|
||||||
|
assert_eq!(BedrockAdaptiveThinkingEffort::XHigh.as_str(), "xhigh");
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Model::ClaudeSonnet4.thinking_mode(),
|
Model::ClaudeSonnet4.thinking_mode(),
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,7 @@ pub enum ChannelEvent {
|
||||||
|
|
||||||
impl EventEmitter<ChannelEvent> for ChannelStore {}
|
impl EventEmitter<ChannelEvent> for ChannelStore {}
|
||||||
|
|
||||||
enum OpenEntityHandle<E> {
|
enum OpenEntityHandle<E: 'static> {
|
||||||
Open(WeakEntity<E>),
|
Open(WeakEntity<E>),
|
||||||
Loading(Shared<Task<Result<Entity<E>, Arc<anyhow::Error>>>>),
|
Loading(Shared<Task<Result<Entity<E>, Arc<anyhow::Error>>>>),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ edition.workspace = true
|
||||||
name = "collab"
|
name = "collab"
|
||||||
version = "0.44.0"
|
version = "0.44.0"
|
||||||
publish.workspace = true
|
publish.workspace = true
|
||||||
license = "AGPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
../../LICENSE-AGPL
|
|
||||||
|
|
@ -48,9 +48,9 @@ pub fn register_component<T: Component>() {
|
||||||
let id = T::id();
|
let id = T::id();
|
||||||
let metadata = ComponentMetadata {
|
let metadata = ComponentMetadata {
|
||||||
id: id.clone(),
|
id: id.clone(),
|
||||||
description: T::description().map(Into::into),
|
description: SharedString::new_static(T::description()),
|
||||||
name: SharedString::new_static(T::name()),
|
name: SharedString::new_static(T::name()),
|
||||||
preview: Some(T::preview),
|
preview: T::preview,
|
||||||
scope: T::scope(),
|
scope: T::scope(),
|
||||||
sort_name: SharedString::new_static(T::sort_name()),
|
sort_name: SharedString::new_static(T::sort_name()),
|
||||||
status: T::status(),
|
status: T::status(),
|
||||||
|
|
@ -69,15 +69,12 @@ pub struct ComponentRegistry {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ComponentRegistry {
|
impl ComponentRegistry {
|
||||||
pub fn previews(&self) -> Vec<&ComponentMetadata> {
|
pub fn previews(&self) -> impl Iterator<Item = &ComponentMetadata> {
|
||||||
self.components
|
self.components.values()
|
||||||
.values()
|
|
||||||
.filter(|c| c.preview.is_some())
|
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sorted_previews(&self) -> Vec<ComponentMetadata> {
|
pub fn sorted_previews(&self) -> Vec<ComponentMetadata> {
|
||||||
let mut previews: Vec<ComponentMetadata> = self.previews().into_iter().cloned().collect();
|
let mut previews: Vec<_> = self.previews().cloned().collect();
|
||||||
previews.sort_by_key(|a| a.name());
|
previews.sort_by_key(|a| a.name());
|
||||||
previews
|
previews
|
||||||
}
|
}
|
||||||
|
|
@ -112,9 +109,9 @@ pub struct ComponentId(pub &'static str);
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ComponentMetadata {
|
pub struct ComponentMetadata {
|
||||||
id: ComponentId,
|
id: ComponentId,
|
||||||
description: Option<SharedString>,
|
description: SharedString,
|
||||||
name: SharedString,
|
name: SharedString,
|
||||||
preview: Option<fn(&mut Window, &mut App) -> Option<AnyElement>>,
|
preview: fn(&mut Window, &mut App) -> AnyElement,
|
||||||
scope: ComponentScope,
|
scope: ComponentScope,
|
||||||
sort_name: SharedString,
|
sort_name: SharedString,
|
||||||
status: ComponentStatus,
|
status: ComponentStatus,
|
||||||
|
|
@ -125,7 +122,7 @@ impl ComponentMetadata {
|
||||||
self.id.clone()
|
self.id.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn description(&self) -> Option<SharedString> {
|
pub fn description(&self) -> SharedString {
|
||||||
self.description.clone()
|
self.description.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -133,7 +130,7 @@ impl ComponentMetadata {
|
||||||
self.name.clone()
|
self.name.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn preview(&self) -> Option<fn(&mut Window, &mut App) -> Option<AnyElement>> {
|
pub fn preview(&self) -> fn(&mut Window, &mut App) -> AnyElement {
|
||||||
self.preview
|
self.preview
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -234,17 +231,15 @@ pub trait Component {
|
||||||
/// struct MyComponent;
|
/// struct MyComponent;
|
||||||
///
|
///
|
||||||
/// impl MyComponent {
|
/// impl MyComponent {
|
||||||
/// fn description() -> Option<&'static str> {
|
/// fn description() -> &'static str {
|
||||||
/// Some(Self::DOCS)
|
/// Self::DOCS
|
||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// This will result in "This is a doc comment." being passed
|
/// This will result in "This is a doc comment." being passed
|
||||||
/// to the component's description.
|
/// to the component's description.
|
||||||
fn description() -> Option<&'static str> {
|
fn description() -> &'static str;
|
||||||
None
|
|
||||||
}
|
|
||||||
/// The component's preview.
|
/// The component's preview.
|
||||||
///
|
///
|
||||||
/// An element returned here will be shown in the component's preview.
|
/// An element returned here will be shown in the component's preview.
|
||||||
|
|
@ -259,9 +254,7 @@ pub trait Component {
|
||||||
/// This is useful for displaying related UI to the component you are
|
/// This is useful for displaying related UI to the component you are
|
||||||
/// trying to preview, such as a button that opens a modal or shows a
|
/// trying to preview, such as a button that opens a modal or shows a
|
||||||
/// tooltip on hover, or a grid of icons showcasing all the icons available.
|
/// tooltip on hover, or a grid of icons showcasing all the icons available.
|
||||||
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
|
fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement;
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The ready status of this component.
|
/// The ready status of this component.
|
||||||
|
|
@ -286,14 +279,17 @@ impl ComponentStatus {
|
||||||
pub fn description(&self) -> &str {
|
pub fn description(&self) -> &str {
|
||||||
match self {
|
match self {
|
||||||
ComponentStatus::WorkInProgress => {
|
ComponentStatus::WorkInProgress => {
|
||||||
"These components are still being designed or refined. They shouldn't be used in the app yet."
|
"These components are still being designed or refined. \
|
||||||
|
They shouldn't be used in the app yet."
|
||||||
}
|
}
|
||||||
ComponentStatus::EngineeringReady => {
|
ComponentStatus::EngineeringReady => {
|
||||||
"These components are design complete or partially implemented, and are ready for an engineer to complete their implementation."
|
"These components are design complete or partially implemented, \
|
||||||
|
and are ready for an engineer to complete their implementation."
|
||||||
}
|
}
|
||||||
ComponentStatus::Live => "These components are ready for use in the app.",
|
ComponentStatus::Live => "These components are ready for use in the app.",
|
||||||
ComponentStatus::Deprecated => {
|
ComponentStatus::Deprecated => {
|
||||||
"These components are no longer recommended for use in the app, and may be removed in a future release."
|
"These components are no longer recommended for use in the app, \
|
||||||
|
and may be removed in a future release."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,10 @@ use notifications::status_toast::StatusToast;
|
||||||
use persistence::ComponentPreviewDb;
|
use persistence::ComponentPreviewDb;
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use std::{iter::Iterator, ops::Range, sync::Arc};
|
use std::{iter::Iterator, ops::Range, sync::Arc};
|
||||||
use ui::{ButtonLike, Divider, HighlightedLabel, ListItem, ListSubHeader, Tooltip, prelude::*};
|
use ui::{
|
||||||
|
ButtonLike, Divider, HighlightedLabel, ListItem, ListSubHeader, Scrollbars, Tooltip,
|
||||||
|
WithScrollbar, prelude::*,
|
||||||
|
};
|
||||||
use ui_input::InputField;
|
use ui_input::InputField;
|
||||||
use workspace::AppState;
|
use workspace::AppState;
|
||||||
use workspace::{
|
use workspace::{
|
||||||
|
|
@ -197,10 +200,7 @@ impl ComponentPreview {
|
||||||
.filter(|component| {
|
.filter(|component| {
|
||||||
let component_name = component.name().to_lowercase();
|
let component_name = component.name().to_lowercase();
|
||||||
let scope_name = component.scope().to_string().to_lowercase();
|
let scope_name = component.scope().to_string().to_lowercase();
|
||||||
let description = component
|
let description = component.description().to_lowercase();
|
||||||
.description()
|
|
||||||
.map(|d| d.to_lowercase())
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
component_name.contains(&filter)
|
component_name.contains(&filter)
|
||||||
|| scope_name.contains(&filter)
|
|| scope_name.contains(&filter)
|
||||||
|
|
@ -231,7 +231,7 @@ impl ComponentPreview {
|
||||||
// let full_component_name = component.name();
|
// let full_component_name = component.name();
|
||||||
let scopeless_name = component.scopeless_name();
|
let scopeless_name = component.scopeless_name();
|
||||||
let scope_name = component.scope().to_string();
|
let scope_name = component.scope().to_string();
|
||||||
let description = component.description().unwrap_or_default();
|
let description = component.description();
|
||||||
|
|
||||||
let lowercase_scopeless = scopeless_name.to_lowercase();
|
let lowercase_scopeless = scopeless_name.to_lowercase();
|
||||||
let lowercase_scope = scope_name.to_lowercase();
|
let lowercase_scope = scope_name.to_lowercase();
|
||||||
|
|
@ -445,7 +445,9 @@ impl ComponentPreview {
|
||||||
let description = component.description();
|
let description = component.description();
|
||||||
|
|
||||||
// Build the content container
|
// Build the content container
|
||||||
let mut preview_container = v_flex().py_2().child(
|
v_flex()
|
||||||
|
.py_2()
|
||||||
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.border_1()
|
.border_1()
|
||||||
.border_color(cx.theme().colors().border)
|
.border_color(cx.theme().colors().border)
|
||||||
|
|
@ -459,31 +461,24 @@ impl ComponentPreview {
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex().gap_1().text_xl().child(div().child(name)).when(
|
||||||
.gap_1()
|
scope != ComponentScope::None,
|
||||||
.text_xl()
|
|this| {
|
||||||
.child(div().child(name))
|
|
||||||
.when(!matches!(scope, ComponentScope::None), |this| {
|
|
||||||
this.child(div().opacity(0.5).child(format!("({})", scope)))
|
this.child(div().opacity(0.5).child(format!("({})", scope)))
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.when_some(description, |this, description| {
|
.child(
|
||||||
this.child(
|
|
||||||
div()
|
div()
|
||||||
.text_ui_sm(cx)
|
.text_ui_sm(cx)
|
||||||
.text_color(cx.theme().colors().text_muted)
|
.text_color(cx.theme().colors().text_muted)
|
||||||
.max_w(px(600.0))
|
.max_w(px(600.0))
|
||||||
.child(description),
|
.child(description),
|
||||||
)
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
|
)
|
||||||
if let Some(preview) = component.preview() {
|
.child((component.preview())(window, cx))
|
||||||
preview_container = preview_container.children(preview(window, cx));
|
.into_any_element()
|
||||||
}
|
|
||||||
|
|
||||||
preview_container.into_any_element()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_all_components(&self, cx: &Context<Self>) -> impl IntoElement {
|
fn render_all_components(&self, cx: &Context<Self>) -> impl IntoElement {
|
||||||
|
|
@ -593,6 +588,7 @@ impl Render for ComponentPreview {
|
||||||
}
|
}
|
||||||
let sidebar_entries = self.scope_ordered_entries();
|
let sidebar_entries = self.scope_ordered_entries();
|
||||||
let active_page = self.active_page.clone();
|
let active_page = self.active_page.clone();
|
||||||
|
let background_color = cx.theme().colors().editor_background;
|
||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.id("component-preview")
|
.id("component-preview")
|
||||||
|
|
@ -601,28 +597,28 @@ impl Render for ComponentPreview {
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.size_full()
|
.size_full()
|
||||||
.track_focus(&self.focus_handle)
|
.track_focus(&self.focus_handle)
|
||||||
.bg(cx.theme().colors().editor_background)
|
.bg(background_color)
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.h_full()
|
.h_full()
|
||||||
.border_r_1()
|
.border_r_1()
|
||||||
.border_color(cx.theme().colors().border)
|
.border_color(cx.theme().colors().border)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.size_full()
|
||||||
.child(
|
.child(
|
||||||
gpui::uniform_list(
|
gpui::uniform_list(
|
||||||
"component-nav",
|
"component-nav",
|
||||||
sidebar_entries.len(),
|
sidebar_entries.len(),
|
||||||
cx.processor(move |this, range: Range<usize>, _window, cx| {
|
cx.processor(move |this, range: Range<usize>, _window, cx| {
|
||||||
range
|
range
|
||||||
.filter_map(|ix| {
|
.filter(|ix| ix < &sidebar_entries.len())
|
||||||
if ix < sidebar_entries.len() {
|
.map(|ix| {
|
||||||
Some(this.render_sidebar_entry(
|
this.render_sidebar_entry(
|
||||||
ix,
|
ix,
|
||||||
&sidebar_entries[ix],
|
&sidebar_entries[ix],
|
||||||
cx,
|
cx,
|
||||||
))
|
)
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}),
|
}),
|
||||||
|
|
@ -633,6 +629,14 @@ impl Render for ComponentPreview {
|
||||||
.h_full()
|
.h_full()
|
||||||
.flex_1(),
|
.flex_1(),
|
||||||
)
|
)
|
||||||
|
.custom_scrollbars(
|
||||||
|
Scrollbars::new(ui::ScrollAxes::Vertical)
|
||||||
|
.with_track_along(ui::ScrollAxes::Vertical, background_color)
|
||||||
|
.tracked_scroll_handle(&self.nav_scroll_handle),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
),
|
||||||
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.w_full()
|
.w_full()
|
||||||
|
|
@ -961,23 +965,10 @@ impl ComponentPreviewPage {
|
||||||
.children(self.render_component_status(cx)),
|
.children(self.render_component_status(cx)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.when_some(self.component.description(), |this, description| {
|
.child(Label::new(self.component.description()).size(LabelSize::Small))
|
||||||
this.child(Label::new(description).size(LabelSize::Small))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_preview(&self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render_preview(&self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
let content = if let Some(preview) = self.component.preview() {
|
|
||||||
// Fall back to component preview
|
|
||||||
preview(window, cx).unwrap_or_else(|| {
|
|
||||||
div()
|
|
||||||
.child("Failed to load preview. This path should be unreachable")
|
|
||||||
.into_any_element()
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
div().child("No preview available").into_any_element()
|
|
||||||
};
|
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.id(("component-preview", self.reset_key))
|
.id(("component-preview", self.reset_key))
|
||||||
.size_full()
|
.size_full()
|
||||||
|
|
@ -985,7 +976,7 @@ impl ComponentPreviewPage {
|
||||||
.px_12()
|
.px_12()
|
||||||
.py_6()
|
.py_6()
|
||||||
.bg(cx.theme().colors().editor_background)
|
.bg(cx.theme().colors().editor_background)
|
||||||
.child(content)
|
.child((self.component.preview())(window, cx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -511,8 +511,14 @@ impl Copilot {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(oauth_token) = env::var(copilot_chat::COPILOT_OAUTH_ENV_VAR) {
|
for env_var in [
|
||||||
env.insert(copilot_chat::COPILOT_OAUTH_ENV_VAR.to_string(), oauth_token);
|
copilot_chat::COPILOT_OAUTH_ENV_VAR,
|
||||||
|
copilot_chat::GITHUB_COPILOT_OAUTH_ENV_VAR,
|
||||||
|
] {
|
||||||
|
if let Ok(oauth_token) = env::var(env_var) {
|
||||||
|
env.insert(env_var.to_string(), oauth_token);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if env.is_empty() { None } else { Some(env) }
|
if env.is_empty() { None } else { Some(env) }
|
||||||
|
|
@ -1259,6 +1265,7 @@ impl Copilot {
|
||||||
| request::SignInStatus::AlreadySignedIn { .. } => {
|
| request::SignInStatus::AlreadySignedIn { .. } => {
|
||||||
server.sign_in_status = SignInStatus::Authorized;
|
server.sign_in_status = SignInStatus::Authorized;
|
||||||
cx.emit(Event::CopilotAuthSignedIn);
|
cx.emit(Event::CopilotAuthSignedIn);
|
||||||
|
notify_copilot_chat_auth_changed(cx);
|
||||||
for buffer in self.buffers.iter().cloned().collect::<Vec<_>>() {
|
for buffer in self.buffers.iter().cloned().collect::<Vec<_>>() {
|
||||||
if let Some(buffer) = buffer.upgrade() {
|
if let Some(buffer) = buffer.upgrade() {
|
||||||
self.register_buffer(&buffer, cx);
|
self.register_buffer(&buffer, cx);
|
||||||
|
|
@ -1278,6 +1285,7 @@ impl Copilot {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
cx.emit(Event::CopilotAuthSignedOut);
|
cx.emit(Event::CopilotAuthSignedOut);
|
||||||
|
notify_copilot_chat_auth_changed(cx);
|
||||||
for buffer in self.buffers.iter().cloned().collect::<Vec<_>>() {
|
for buffer in self.buffers.iter().cloned().collect::<Vec<_>>() {
|
||||||
self.unregister_buffer(&buffer);
|
self.unregister_buffer(&buffer);
|
||||||
}
|
}
|
||||||
|
|
@ -1381,6 +1389,15 @@ fn notify_did_change_config_to_server(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Notify Copilot Chat after the Copilot LSP reports an auth state change.
|
||||||
|
/// This replaces watching the SDK's token files, which is unreliable for
|
||||||
|
/// SQLite backed auth because writes may go through WAL files.
|
||||||
|
fn notify_copilot_chat_auth_changed(cx: &mut Context<Copilot>) {
|
||||||
|
if let Some(copilot_chat) = copilot_chat::CopilotChat::global(cx) {
|
||||||
|
copilot_chat.update(cx, |chat, cx| chat.reload_auth(cx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn clear_copilot_dir() {
|
async fn clear_copilot_dir() {
|
||||||
remove_matching(paths::copilot_dir(), |_| true).await
|
remove_matching(paths::copilot_dir(), |_| true).await
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,9 @@ paths.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
settings.workspace = true
|
settings.workspace = true
|
||||||
|
sqlez.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
gpui = { workspace = true, features = ["test-support"] }
|
gpui = { workspace = true, features = ["test-support"] }
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
tempfile.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
pub mod responses;
|
pub mod responses;
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
|
|
@ -17,9 +17,10 @@ use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
|
||||||
use paths::home_dir;
|
use paths::home_dir;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use settings::watch_config_dir;
|
// The Copilot language server unofficially supports both token env vars:
|
||||||
|
// https://github.com/github/copilot-language-server-release/issues/3#issuecomment-2699433055
|
||||||
pub const COPILOT_OAUTH_ENV_VAR: &str = "GH_COPILOT_TOKEN";
|
pub const COPILOT_OAUTH_ENV_VAR: &str = "GH_COPILOT_TOKEN";
|
||||||
|
pub const GITHUB_COPILOT_OAUTH_ENV_VAR: &str = "GITHUB_COPILOT_TOKEN";
|
||||||
const DEFAULT_COPILOT_API_ENDPOINT: &str = "https://api.githubcopilot.com";
|
const DEFAULT_COPILOT_API_ENDPOINT: &str = "https://api.githubcopilot.com";
|
||||||
|
|
||||||
#[derive(Default, Clone, Debug, PartialEq)]
|
#[derive(Default, Clone, Debug, PartialEq)]
|
||||||
|
|
@ -501,6 +502,7 @@ pub struct CopilotChat {
|
||||||
configuration: CopilotChatConfiguration,
|
configuration: CopilotChatConfiguration,
|
||||||
models: Option<Vec<Model>>,
|
models: Option<Vec<Model>>,
|
||||||
client: Arc<dyn HttpClient>,
|
client: Arc<dyn HttpClient>,
|
||||||
|
fs: Arc<dyn Fs>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(
|
pub fn init(
|
||||||
|
|
@ -529,11 +531,19 @@ pub fn copilot_chat_config_dir() -> &'static PathBuf {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Legacy JSON token-storage paths used by older Copilot SDK builds.
|
||||||
|
/// TODO(copilot): once Copilot SDK supports `auth.db`, remove these paths.
|
||||||
fn copilot_chat_config_paths() -> [PathBuf; 2] {
|
fn copilot_chat_config_paths() -> [PathBuf; 2] {
|
||||||
let base_dir = copilot_chat_config_dir();
|
let base_dir = copilot_chat_config_dir();
|
||||||
[base_dir.join("hosts.json"), base_dir.join("apps.json")]
|
[base_dir.join("hosts.json"), base_dir.join("apps.json")]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn oauth_token_from_env() -> Option<String> {
|
||||||
|
std::env::var(COPILOT_OAUTH_ENV_VAR)
|
||||||
|
.ok()
|
||||||
|
.or_else(|| std::env::var(GITHUB_COPILOT_OAUTH_ENV_VAR).ok())
|
||||||
|
}
|
||||||
|
|
||||||
impl CopilotChat {
|
impl CopilotChat {
|
||||||
pub fn global(cx: &App) -> Option<gpui::Entity<Self>> {
|
pub fn global(cx: &App) -> Option<gpui::Entity<Self>> {
|
||||||
cx.try_global::<GlobalCopilotChat>()
|
cx.try_global::<GlobalCopilotChat>()
|
||||||
|
|
@ -546,40 +556,42 @@ impl CopilotChat {
|
||||||
configuration: CopilotChatConfiguration,
|
configuration: CopilotChatConfiguration,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let config_paths: HashSet<PathBuf> = copilot_chat_config_paths().into_iter().collect();
|
// Initial async scan of token sources. Live reload is driven by the
|
||||||
let dir_path = copilot_chat_config_dir();
|
// Copilot LSP's auth status notifications instead of watching files,
|
||||||
|
// because SQLite WAL writes can make directory watchers racy.
|
||||||
cx.spawn(async move |this, cx| {
|
cx.spawn({
|
||||||
let mut parent_watch_rx = watch_config_dir(
|
let fs = fs.clone();
|
||||||
cx.background_executor(),
|
async move |this, cx| {
|
||||||
fs.clone(),
|
|
||||||
dir_path.clone(),
|
|
||||||
config_paths,
|
|
||||||
);
|
|
||||||
while let Some(contents) = parent_watch_rx.next().await {
|
|
||||||
let oauth_domain =
|
let oauth_domain =
|
||||||
this.read_with(cx, |this, _| this.configuration.oauth_domain())?;
|
this.read_with(cx, |this, _| this.configuration.oauth_domain())?;
|
||||||
let oauth_token = extract_oauth_token(contents, &oauth_domain);
|
let config_paths: HashSet<PathBuf> =
|
||||||
|
copilot_chat_config_paths().into_iter().collect();
|
||||||
|
let auth_db_path = copilot_chat_config_dir().join("auth.db");
|
||||||
|
|
||||||
this.update(cx, |this, cx| {
|
let oauth_token =
|
||||||
this.oauth_token = oauth_token.clone();
|
read_oauth_token(&fs, &config_paths, &oauth_domain, &auth_db_path, cx).await;
|
||||||
cx.notify();
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if oauth_token.is_some() {
|
if oauth_token.is_some() {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.oauth_token = oauth_token;
|
||||||
|
cx.notify();
|
||||||
|
})?;
|
||||||
Self::update_models(&this, cx).await?;
|
Self::update_models(&this, cx).await?;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
anyhow::Ok(())
|
anyhow::Ok(())
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
|
|
||||||
|
// Initial state uses env var because it's cheap. The others do IO, so
|
||||||
|
// are on the background.
|
||||||
let this = Self {
|
let this = Self {
|
||||||
oauth_token: std::env::var(COPILOT_OAUTH_ENV_VAR).ok(),
|
oauth_token: oauth_token_from_env(),
|
||||||
api_endpoint: None,
|
api_endpoint: None,
|
||||||
models: None,
|
models: None,
|
||||||
configuration,
|
configuration,
|
||||||
client,
|
client,
|
||||||
|
fs,
|
||||||
};
|
};
|
||||||
|
|
||||||
if this.oauth_token.is_some() {
|
if this.oauth_token.is_some() {
|
||||||
|
|
@ -764,6 +776,39 @@ impl CopilotChat {
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn reload_auth(&mut self, cx: &mut Context<Self>) {
|
||||||
|
let fs = self.fs.clone();
|
||||||
|
let oauth_domain = self.configuration.oauth_domain();
|
||||||
|
cx.spawn(async move |this, cx| {
|
||||||
|
let config_paths: HashSet<PathBuf> = copilot_chat_config_paths().into_iter().collect();
|
||||||
|
let auth_db_path = copilot_chat_config_dir().join("auth.db");
|
||||||
|
|
||||||
|
let new_token =
|
||||||
|
read_oauth_token(&fs, &config_paths, &oauth_domain, &auth_db_path, cx).await;
|
||||||
|
|
||||||
|
let token_present = this.update(cx, |this, cx| {
|
||||||
|
let changed = this.oauth_token != new_token;
|
||||||
|
if changed {
|
||||||
|
this.oauth_token = new_token.clone();
|
||||||
|
if new_token.is_none() {
|
||||||
|
// Sign-out: drop derived state so a future sign-in
|
||||||
|
// re-discovers the endpoint and re-fetches models.
|
||||||
|
this.api_endpoint = None;
|
||||||
|
this.models = None;
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
new_token.is_some()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if token_present {
|
||||||
|
Self::update_models(&this, cx).await?;
|
||||||
|
}
|
||||||
|
anyhow::Ok(())
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_models(
|
async fn get_models(
|
||||||
|
|
@ -917,6 +962,40 @@ async fn request_models(
|
||||||
Ok(models)
|
Ok(models)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn read_oauth_token(
|
||||||
|
fs: &Arc<dyn Fs>,
|
||||||
|
config_paths: &HashSet<PathBuf>,
|
||||||
|
oauth_domain: &str,
|
||||||
|
auth_db_path: &std::path::Path,
|
||||||
|
cx: &AsyncApp,
|
||||||
|
) -> Option<String> {
|
||||||
|
if let Some(token) = oauth_token_from_env() {
|
||||||
|
return Some(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
let token_from_db = cx
|
||||||
|
.background_spawn({
|
||||||
|
let auth_db_path = auth_db_path.to_path_buf();
|
||||||
|
let oauth_domain = oauth_domain.to_string();
|
||||||
|
async move { extract_oauth_token_from_db(&auth_db_path, &oauth_domain) }
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Some(token) = token_from_db {
|
||||||
|
return Some(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
for file_path in config_paths {
|
||||||
|
if let Ok(contents) = fs.load(file_path).await {
|
||||||
|
if let Some(token) = extract_oauth_token(contents, oauth_domain) {
|
||||||
|
return Some(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn extract_oauth_token(contents: String, domain: &str) -> Option<String> {
|
fn extract_oauth_token(contents: String, domain: &str) -> Option<String> {
|
||||||
serde_json::from_str::<serde_json::Value>(&contents)
|
serde_json::from_str::<serde_json::Value>(&contents)
|
||||||
.map(|v| {
|
.map(|v| {
|
||||||
|
|
@ -934,6 +1013,36 @@ fn extract_oauth_token(contents: String, domain: &str) -> Option<String> {
|
||||||
.flatten()
|
.flatten()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn extract_oauth_token_from_db(db_path: &Path, auth_authority: &str) -> Option<String> {
|
||||||
|
if !db_path.exists() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let db = sqlez::connection::Connection::open_file(db_path.to_str()?);
|
||||||
|
|
||||||
|
let token_bytes: Option<Vec<u8>> = db
|
||||||
|
.select_row_bound::<&str, Vec<u8>>(
|
||||||
|
"SELECT token_ciphertext FROM oauth_tokens WHERE auth_authority = ? ORDER BY last_used_at DESC, token_id DESC LIMIT 1",
|
||||||
|
)
|
||||||
|
.ok()
|
||||||
|
.and_then(|mut select| select(auth_authority).ok().flatten());
|
||||||
|
|
||||||
|
let token = token_bytes.and_then(|bytes| String::from_utf8(bytes).ok())?;
|
||||||
|
|
||||||
|
if token.starts_with("ghu_")
|
||||||
|
&& token.len() >= 36
|
||||||
|
&& token.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
|
||||||
|
{
|
||||||
|
log::debug!("Copilot OAuth token loaded from auth.db");
|
||||||
|
Some(token)
|
||||||
|
} else {
|
||||||
|
log::warn!(
|
||||||
|
"Copilot auth.db: token does not match expected GitHub OAuth format (ghu_<alphanumeric>)"
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn stream_completion(
|
async fn stream_completion(
|
||||||
client: Arc<dyn HttpClient>,
|
client: Arc<dyn HttpClient>,
|
||||||
oauth_token: String,
|
oauth_token: String,
|
||||||
|
|
@ -1751,4 +1860,61 @@ mod tests {
|
||||||
"\"none\""
|
"\"none\""
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extract_oauth_token_from_db_matches_auth_authority_and_recency() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let db_path = dir.path().join("auth.db");
|
||||||
|
let older_github_token = "ghu_oldergithubtokenvalue000000000000";
|
||||||
|
let newer_github_token = "ghu_newergithubtokenvalue000000000000";
|
||||||
|
let enterprise_token = "ghu_enterprisetokenvalue0000000000000";
|
||||||
|
|
||||||
|
let connection = sqlez::connection::Connection::open_file(db_path.to_str().unwrap());
|
||||||
|
connection
|
||||||
|
.exec(
|
||||||
|
"CREATE TABLE oauth_tokens (
|
||||||
|
token_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
auth_authority TEXT NOT NULL,
|
||||||
|
token_ciphertext BLOB NOT NULL,
|
||||||
|
last_used_at INTEGER NOT NULL
|
||||||
|
);",
|
||||||
|
)
|
||||||
|
.unwrap()()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut insert_token = connection
|
||||||
|
.exec_bound::<(&str, Vec<u8>, i64)>(
|
||||||
|
"INSERT INTO oauth_tokens (auth_authority, token_ciphertext, last_used_at) VALUES (?, ?, ?);",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
insert_token(("github.com", older_github_token.as_bytes().to_vec(), 10)).unwrap();
|
||||||
|
insert_token((
|
||||||
|
"github.enterprise.test",
|
||||||
|
enterprise_token.as_bytes().to_vec(),
|
||||||
|
30,
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
insert_token(("github.com", newer_github_token.as_bytes().to_vec(), 20)).unwrap();
|
||||||
|
}
|
||||||
|
drop(connection);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
extract_oauth_token_from_db(&db_path, "github.com").as_deref(),
|
||||||
|
Some(newer_github_token)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
extract_oauth_token_from_db(&db_path, "github.enterprise.test").as_deref(),
|
||||||
|
Some(enterprise_token)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extract_oauth_token_from_db_missing_db_does_not_create_file() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let db_path = dir.path().join("auth.db");
|
||||||
|
|
||||||
|
assert_eq!(extract_oauth_token_from_db(&db_path, "github.com"), None);
|
||||||
|
assert!(!db_path.exists());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -211,7 +211,7 @@ pub(crate) struct DevContainer {
|
||||||
#[serde(rename = "updateRemoteUserUID")]
|
#[serde(rename = "updateRemoteUserUID")]
|
||||||
pub(crate) update_remote_user_uid: Option<bool>,
|
pub(crate) update_remote_user_uid: Option<bool>,
|
||||||
user_env_probe: Option<UserEnvProbe>,
|
user_env_probe: Option<UserEnvProbe>,
|
||||||
override_command: Option<bool>,
|
pub(crate) override_command: Option<bool>,
|
||||||
shutdown_action: Option<ShutdownAction>,
|
shutdown_action: Option<ShutdownAction>,
|
||||||
init: Option<bool>,
|
init: Option<bool>,
|
||||||
pub(crate) privileged: Option<bool>,
|
pub(crate) privileged: Option<bool>,
|
||||||
|
|
@ -232,7 +232,7 @@ pub(crate) struct DevContainer {
|
||||||
#[serde(default, deserialize_with = "deserialize_string_or_array")]
|
#[serde(default, deserialize_with = "deserialize_string_or_array")]
|
||||||
pub(crate) docker_compose_file: Option<Vec<String>>,
|
pub(crate) docker_compose_file: Option<Vec<String>>,
|
||||||
pub(crate) service: Option<String>,
|
pub(crate) service: Option<String>,
|
||||||
run_services: Option<Vec<String>>,
|
pub(crate) run_services: Option<Vec<String>>,
|
||||||
pub(crate) initialize_command: Option<LifecycleScript>,
|
pub(crate) initialize_command: Option<LifecycleScript>,
|
||||||
pub(crate) on_create_command: Option<LifecycleScript>,
|
pub(crate) on_create_command: Option<LifecycleScript>,
|
||||||
pub(crate) update_content_command: Option<LifecycleScript>,
|
pub(crate) update_content_command: Option<LifecycleScript>,
|
||||||
|
|
|
||||||
|
|
@ -794,6 +794,9 @@ RUN sed -i -E 's/((^|\s)PATH=)([^\$]*)$/\1\${{PATH:-\3}}/g' /etc/profile || true
|
||||||
let privileged = dev_container.privileged.unwrap_or(false)
|
let privileged = dev_container.privileged.unwrap_or(false)
|
||||||
|| self.features.iter().any(|f| f.privileged());
|
|| self.features.iter().any(|f| f.privileged());
|
||||||
|
|
||||||
|
let entrypoint_script = if dev_container.override_command == Some(false) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
let mut entrypoint_script_lines = vec![
|
let mut entrypoint_script_lines = vec![
|
||||||
"echo Container started".to_string(),
|
"echo Container started".to_string(),
|
||||||
"trap \"exit 0\" 15".to_string(),
|
"trap \"exit 0\" 15".to_string(),
|
||||||
|
|
@ -807,11 +810,14 @@ RUN sed -i -E 's/((^|\s)PATH=)([^\$]*)$/\1\${{PATH:-\3}}/g' /etc/profile || true
|
||||||
"while sleep 1 & wait $!; do :; done".to_string(),
|
"while sleep 1 & wait $!; do :; done".to_string(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
Some(entrypoint_script_lines.join("\n").trim().to_string())
|
||||||
|
};
|
||||||
|
|
||||||
Ok(DockerBuildResources {
|
Ok(DockerBuildResources {
|
||||||
image: base_image,
|
image: base_image,
|
||||||
additional_mounts: mounts,
|
additional_mounts: mounts,
|
||||||
privileged,
|
privileged,
|
||||||
entrypoint_script: entrypoint_script_lines.join("\n").trim().to_string(),
|
entrypoint_script,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1052,7 +1058,11 @@ RUN sed -i -E 's/((^|\s)PATH=)([^\$]*)$/\1\${{PATH:-\3}}/g' /etc/profile || true
|
||||||
|
|
||||||
let project_name = self.project_name().await?;
|
let project_name = self.project_name().await?;
|
||||||
self.docker_client
|
self.docker_client
|
||||||
.docker_compose_build(&docker_compose_resources.files, &project_name)
|
.docker_compose_build(
|
||||||
|
&docker_compose_resources.files,
|
||||||
|
&project_name,
|
||||||
|
dev_container.run_services.as_ref(),
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
(
|
(
|
||||||
self.docker_client
|
self.docker_client
|
||||||
|
|
@ -1145,7 +1155,11 @@ RUN sed -i -E 's/((^|\s)PATH=)([^\$]*)$/\1\${{PATH:-\3}}/g' /etc/profile || true
|
||||||
|
|
||||||
let project_name = self.project_name().await?;
|
let project_name = self.project_name().await?;
|
||||||
self.docker_client
|
self.docker_client
|
||||||
.docker_compose_build(&docker_compose_resources.files, &project_name)
|
.docker_compose_build(
|
||||||
|
&docker_compose_resources.files,
|
||||||
|
&project_name,
|
||||||
|
dev_container.run_services.as_ref(),
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
(
|
(
|
||||||
|
|
@ -1255,13 +1269,17 @@ RUN sed -i -E 's/((^|\s)PATH=)([^\$]*)$/\1\${{PATH:-\3}}/g' /etc/profile || true
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let mut main_service = DockerComposeService {
|
let entrypoint = resources.entrypoint_script.map(|script| {
|
||||||
entrypoint: Some(vec![
|
vec![
|
||||||
"/bin/sh".to_string(),
|
"/bin/sh".to_string(),
|
||||||
"-c".to_string(),
|
"-c".to_string(),
|
||||||
resources.entrypoint_script,
|
script,
|
||||||
"-".to_string(),
|
"-".to_string(),
|
||||||
]),
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut main_service = DockerComposeService {
|
||||||
|
entrypoint,
|
||||||
cap_add: Some(vec!["SYS_PTRACE".to_string()]),
|
cap_add: Some(vec!["SYS_PTRACE".to_string()]),
|
||||||
security_opt: Some(vec!["seccomp=unconfined".to_string()]),
|
security_opt: Some(vec!["seccomp=unconfined".to_string()]),
|
||||||
labels: Some(runtime_labels),
|
labels: Some(runtime_labels),
|
||||||
|
|
@ -1775,6 +1793,9 @@ RUN sed -i -E 's/((^|\s)PATH=)([^\$]*)$/\1\${PATH:-\3}/g' /etc/profile || true
|
||||||
command.args(&["-f", &docker_compose_file.display().to_string()]);
|
command.args(&["-f", &docker_compose_file.display().to_string()]);
|
||||||
}
|
}
|
||||||
command.args(&["up", "-d"]);
|
command.args(&["up", "-d"]);
|
||||||
|
if let Some(run_services) = self.dev_container().run_services.as_ref() {
|
||||||
|
command.args(run_services);
|
||||||
|
}
|
||||||
|
|
||||||
let output = self
|
let output = self
|
||||||
.command_runner
|
.command_runner
|
||||||
|
|
@ -1977,13 +1998,16 @@ RUN sed -i -E 's/((^|\s)PATH=)([^\$]*)$/\1\${PATH:-\3}/g' /etc/profile || true
|
||||||
command.arg(app_port);
|
command.arg(app_port);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(entrypoint_script) = build_resources.entrypoint_script {
|
||||||
command.arg("--entrypoint");
|
command.arg("--entrypoint");
|
||||||
command.arg("/bin/sh");
|
command.arg("/bin/sh");
|
||||||
command.arg(&build_resources.image.id);
|
command.arg(&build_resources.image.id);
|
||||||
command.arg("-c");
|
command.arg("-c");
|
||||||
|
command.arg(entrypoint_script);
|
||||||
command.arg(build_resources.entrypoint_script);
|
|
||||||
command.arg("-");
|
command.arg("-");
|
||||||
|
} else {
|
||||||
|
command.arg(&build_resources.image.id);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(command)
|
Ok(command)
|
||||||
}
|
}
|
||||||
|
|
@ -2409,7 +2433,7 @@ struct DockerBuildResources {
|
||||||
image: DockerInspect,
|
image: DockerInspect,
|
||||||
additional_mounts: Vec<MountDefinition>,
|
additional_mounts: Vec<MountDefinition>,
|
||||||
privileged: bool,
|
privileged: bool,
|
||||||
entrypoint_script: String,
|
entrypoint_script: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
@ -3166,7 +3190,7 @@ mod test {
|
||||||
},
|
},
|
||||||
additional_mounts: vec![],
|
additional_mounts: vec![],
|
||||||
privileged: false,
|
privileged: false,
|
||||||
entrypoint_script: "echo Container started\n trap \"exit 0\" 15\n exec \"$@\"\n while sleep 1 & wait $!; do :; done".to_string(),
|
entrypoint_script: Some("echo Container started\n trap \"exit 0\" 15\n exec \"$@\"\n while sleep 1 & wait $!; do :; done".to_string()),
|
||||||
};
|
};
|
||||||
let docker_run_command = devcontainer_manifest.create_docker_run_command(build_resources);
|
let docker_run_command = devcontainer_manifest.create_docker_run_command(build_resources);
|
||||||
|
|
||||||
|
|
@ -3212,6 +3236,56 @@ mod test {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn should_not_override_entrypoint_when_override_command_is_false(
|
||||||
|
cx: &mut TestAppContext,
|
||||||
|
) {
|
||||||
|
let (_, mut devcontainer_manifest) = init_default_devcontainer_manifest(
|
||||||
|
cx,
|
||||||
|
r#"{
|
||||||
|
"name": "test",
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
|
||||||
|
"overrideCommand": false
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
devcontainer_manifest.parse_nonremote_vars().unwrap();
|
||||||
|
|
||||||
|
let base_image = DockerInspect {
|
||||||
|
id: "mcr.microsoft.com/devcontainers/base:ubuntu".to_string(),
|
||||||
|
config: DockerInspectConfig {
|
||||||
|
labels: DockerConfigLabels { metadata: None },
|
||||||
|
image_user: None,
|
||||||
|
env: Vec::new(),
|
||||||
|
},
|
||||||
|
mounts: None,
|
||||||
|
state: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let resources = devcontainer_manifest
|
||||||
|
.build_merged_resources(base_image)
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
resources.entrypoint_script.is_none(),
|
||||||
|
"overrideCommand: false must not produce an entrypoint script"
|
||||||
|
);
|
||||||
|
|
||||||
|
let docker_run_command = devcontainer_manifest
|
||||||
|
.create_docker_run_command(resources)
|
||||||
|
.unwrap();
|
||||||
|
let args: Vec<&OsStr> = docker_run_command.get_args().collect();
|
||||||
|
assert!(
|
||||||
|
!args.contains(&OsStr::new("--entrypoint")),
|
||||||
|
"overrideCommand: false must not pass --entrypoint to docker run"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
args.contains(&OsStr::new("mcr.microsoft.com/devcontainers/base:ubuntu")),
|
||||||
|
"image id must still be present in docker run command"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn should_find_primary_service_in_docker_compose(cx: &mut TestAppContext) {
|
async fn should_find_primary_service_in_docker_compose(cx: &mut TestAppContext) {
|
||||||
// State where service not defined in dev container
|
// State where service not defined in dev container
|
||||||
|
|
@ -4720,6 +4794,111 @@ ENV DOCKER_BUILDKIT=1
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_spawns_only_requested_compose_services(cx: &mut TestAppContext) {
|
||||||
|
cx.executor().allow_parking();
|
||||||
|
env_logger::try_init().ok();
|
||||||
|
let given_devcontainer_contents = r#"
|
||||||
|
{
|
||||||
|
"name": "Devcontainer and PostgreSQL",
|
||||||
|
"dockerComposeFile": "docker-compose.yml",
|
||||||
|
"service": "devcontainer",
|
||||||
|
"runServices": ["devcontainer", "db"],
|
||||||
|
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
||||||
|
"updateRemoteUserUID": false
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
let (test_dependencies, mut devcontainer_manifest) =
|
||||||
|
init_default_devcontainer_manifest(cx, given_devcontainer_contents)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
test_dependencies
|
||||||
|
.fs
|
||||||
|
.atomic_write(
|
||||||
|
PathBuf::from(TEST_PROJECT_PATH).join(".devcontainer/docker-compose.yml"),
|
||||||
|
r#"
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
x-base: &base
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres-data:
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
<<: *base
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
|
||||||
|
devcontainer:
|
||||||
|
<<: *base
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
volumes:
|
||||||
|
- ../..:/workspaces:cached
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:14.1
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- postgres-data:/var/lib/postgresql/data
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
"#
|
||||||
|
.trim()
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
test_dependencies
|
||||||
|
.fs
|
||||||
|
.atomic_write(
|
||||||
|
PathBuf::from(TEST_PROJECT_PATH).join(".devcontainer/Dockerfile"),
|
||||||
|
r#"
|
||||||
|
FROM mcr.microsoft.com/devcontainers/rust:2-1-bookworm
|
||||||
|
|
||||||
|
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||||
|
&& apt-get -y install clang lld \
|
||||||
|
&& apt-get autoremove -y && apt-get clean -y
|
||||||
|
"#
|
||||||
|
.trim()
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
devcontainer_manifest.parse_nonremote_vars().unwrap();
|
||||||
|
let _devcontainer_up = devcontainer_manifest.build_and_run().await.unwrap();
|
||||||
|
|
||||||
|
let docker_commands = test_dependencies
|
||||||
|
.command_runner
|
||||||
|
.commands_by_program("docker");
|
||||||
|
let compose_up = docker_commands
|
||||||
|
.iter()
|
||||||
|
.find(|c| {
|
||||||
|
c.args.first().map(String::as_str) == Some("compose")
|
||||||
|
&& c.args.iter().any(|a| a == "up")
|
||||||
|
})
|
||||||
|
.expect("docker compose up command recorded");
|
||||||
|
assert!(
|
||||||
|
compose_up.args.ends_with(&[
|
||||||
|
"up".to_string(),
|
||||||
|
"-d".to_string(),
|
||||||
|
"devcontainer".to_string(),
|
||||||
|
"db".to_string(),
|
||||||
|
]),
|
||||||
|
"compose up should target only the requested service, got: {:?}",
|
||||||
|
compose_up.args
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_spawns_devcontainer_with_docker_compose_and_podman(cx: &mut TestAppContext) {
|
async fn test_spawns_devcontainer_with_docker_compose_and_podman(cx: &mut TestAppContext) {
|
||||||
|
|
@ -6004,6 +6183,19 @@ FROM docker.io/hexpm/elixir:1.21-erlang-28.4.1-debian-trixie-20260316-slim AS de
|
||||||
return Ok(Some(DockerComposeConfig {
|
return Ok(Some(DockerComposeConfig {
|
||||||
name: None,
|
name: None,
|
||||||
services: HashMap::from([
|
services: HashMap::from([
|
||||||
|
(
|
||||||
|
"devcontainer".to_string(),
|
||||||
|
DockerComposeService {
|
||||||
|
image: Some("test_image:latest".to_string()),
|
||||||
|
volumes: vec![MountDefinition {
|
||||||
|
source: Some("../..".to_string()),
|
||||||
|
target: "/workspaces".to_string(),
|
||||||
|
mount_type: Some("bind".to_string()),
|
||||||
|
}],
|
||||||
|
command: vec!["sleep".to_string(), "infinity".to_string()],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
),
|
||||||
(
|
(
|
||||||
"app".to_string(),
|
"app".to_string(),
|
||||||
DockerComposeService {
|
DockerComposeService {
|
||||||
|
|
@ -6130,6 +6322,7 @@ FROM docker.io/hexpm/elixir:1.21-erlang-28.4.1-debian-trixie-20260316-slim AS de
|
||||||
&self,
|
&self,
|
||||||
_config_files: &Vec<PathBuf>,
|
_config_files: &Vec<PathBuf>,
|
||||||
_project_name: &str,
|
_project_name: &str,
|
||||||
|
_services: Option<&Vec<String>>,
|
||||||
) -> Result<(), DevContainerError> {
|
) -> Result<(), DevContainerError> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -291,6 +291,7 @@ impl DockerClient for Docker {
|
||||||
&self,
|
&self,
|
||||||
config_files: &Vec<PathBuf>,
|
config_files: &Vec<PathBuf>,
|
||||||
project_name: &str,
|
project_name: &str,
|
||||||
|
services: Option<&Vec<String>>,
|
||||||
) -> Result<(), DevContainerError> {
|
) -> Result<(), DevContainerError> {
|
||||||
let mut command = Command::new(&self.docker_cli);
|
let mut command = Command::new(&self.docker_cli);
|
||||||
if !self.is_podman() {
|
if !self.is_podman() {
|
||||||
|
|
@ -301,6 +302,9 @@ impl DockerClient for Docker {
|
||||||
command.args(&["-f", &docker_compose_file.display().to_string()]);
|
command.args(&["-f", &docker_compose_file.display().to_string()]);
|
||||||
}
|
}
|
||||||
command.arg("build");
|
command.arg("build");
|
||||||
|
if let Some(services) = services {
|
||||||
|
command.args(services);
|
||||||
|
}
|
||||||
|
|
||||||
let output = command.output().await.map_err(|e| {
|
let output = command.output().await.map_err(|e| {
|
||||||
log::error!("Error running docker compose up: {e}");
|
log::error!("Error running docker compose up: {e}");
|
||||||
|
|
@ -457,6 +461,7 @@ pub(crate) trait DockerClient {
|
||||||
&self,
|
&self,
|
||||||
config_files: &Vec<PathBuf>,
|
config_files: &Vec<PathBuf>,
|
||||||
project_name: &str,
|
project_name: &str,
|
||||||
|
services: Option<&Vec<String>>,
|
||||||
) -> Result<(), DevContainerError>;
|
) -> Result<(), DevContainerError>;
|
||||||
async fn run_docker_exec(
|
async fn run_docker_exec(
|
||||||
&self,
|
&self,
|
||||||
|
|
|
||||||
|
|
@ -1550,6 +1550,8 @@ async fn go_to_diagnostic_with_severity(cx: &mut TestAppContext) {
|
||||||
|
|
||||||
// Default, should cycle through all diagnostics
|
// Default, should cycle through all diagnostics
|
||||||
go!(GoToDiagnosticSeverityFilter::default());
|
go!(GoToDiagnosticSeverityFilter::default());
|
||||||
|
cx.assert_editor_state(indoc! {"error warning info ˇhint"});
|
||||||
|
go!(GoToDiagnosticSeverityFilter::default());
|
||||||
cx.assert_editor_state(indoc! {"ˇerror warning info hint"});
|
cx.assert_editor_state(indoc! {"ˇerror warning info hint"});
|
||||||
go!(GoToDiagnosticSeverityFilter::default());
|
go!(GoToDiagnosticSeverityFilter::default());
|
||||||
cx.assert_editor_state(indoc! {"error ˇwarning info hint"});
|
cx.assert_editor_state(indoc! {"error ˇwarning info hint"});
|
||||||
|
|
|
||||||
|
|
@ -64,13 +64,19 @@ impl Render for DiagnosticIndicator {
|
||||||
.message
|
.message
|
||||||
.split_once('\n')
|
.split_once('\n')
|
||||||
.map_or(&*diagnostic.message, |(first, _)| first);
|
.map_or(&*diagnostic.message, |(first, _)| first);
|
||||||
|
let diagnostics_already_active = self.any_active_diagnostics(cx);
|
||||||
|
let tooltip = if !diagnostics_already_active {
|
||||||
|
"Expand Diagnostics"
|
||||||
|
} else {
|
||||||
|
"Next Diagnostic"
|
||||||
|
};
|
||||||
Some(
|
Some(
|
||||||
Button::new("diagnostic_message", SharedString::new(message))
|
Button::new("diagnostic_message", SharedString::new(message))
|
||||||
.label_size(LabelSize::Small)
|
.label_size(LabelSize::Small)
|
||||||
.truncate(true)
|
.truncate(true)
|
||||||
.tooltip(|_window, cx| {
|
.tooltip(move |_window, cx| {
|
||||||
Tooltip::for_action(
|
Tooltip::for_action(
|
||||||
"Next Diagnostic",
|
tooltip,
|
||||||
&editor::actions::GoToDiagnostic::default(),
|
&editor::actions::GoToDiagnostic::default(),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
|
@ -154,10 +160,18 @@ impl DiagnosticIndicator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn any_active_diagnostics(&self, cx: &mut Context<Self>) -> bool {
|
||||||
|
if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade()) {
|
||||||
|
editor.read(cx).any_active_diagnostics()
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn go_to_next_diagnostic(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn go_to_next_diagnostic(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade()) {
|
if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade()) {
|
||||||
editor.update(cx, |editor, cx| {
|
editor.update(cx, |editor, cx| {
|
||||||
editor.go_to_diagnostic_impl(
|
editor.go_to_diagnostic_at_cursor(
|
||||||
editor::Direction::Next,
|
editor::Direction::Next,
|
||||||
GoToDiagnosticSeverityFilter::default(),
|
GoToDiagnosticSeverityFilter::default(),
|
||||||
window,
|
window,
|
||||||
|
|
|
||||||
|
|
@ -38,9 +38,9 @@ use gpui::{
|
||||||
};
|
};
|
||||||
use heapless::Vec as ArrayVec;
|
use heapless::Vec as ArrayVec;
|
||||||
use language::{
|
use language::{
|
||||||
Anchor, Buffer, BufferSnapshot, EditPredictionPromptFormat, EditPredictionsMode, EditPreview,
|
Anchor, Buffer, BufferEditSource, BufferSnapshot, EditPredictionPromptFormat,
|
||||||
File, OffsetRangeExt, Point, TextBufferSnapshot, ToOffset, ToPoint,
|
EditPredictionsMode, EditPreview, File, OffsetRangeExt, Point, TextBufferSnapshot, ToOffset,
|
||||||
language_settings::all_language_settings,
|
ToPoint, language_settings::all_language_settings,
|
||||||
};
|
};
|
||||||
use project::{DisableAiSettings, Project, ProjectPath, WorktreeId};
|
use project::{DisableAiSettings, Project, ProjectPath, WorktreeId};
|
||||||
use release_channel::AppVersion;
|
use release_channel::AppVersion;
|
||||||
|
|
@ -324,6 +324,7 @@ struct ProjectState {
|
||||||
recent_paths: VecDeque<ProjectPath>,
|
recent_paths: VecDeque<ProjectPath>,
|
||||||
registered_buffers: HashMap<gpui::EntityId, RegisteredBuffer>,
|
registered_buffers: HashMap<gpui::EntityId, RegisteredBuffer>,
|
||||||
current_prediction: Option<CurrentEditPrediction>,
|
current_prediction: Option<CurrentEditPrediction>,
|
||||||
|
last_edit_source: Option<BufferEditSource>,
|
||||||
next_pending_prediction_id: usize,
|
next_pending_prediction_id: usize,
|
||||||
pending_predictions: ArrayVec<PendingPrediction, 2, u8>,
|
pending_predictions: ArrayVec<PendingPrediction, 2, u8>,
|
||||||
debug_tx: Option<mpsc::UnboundedSender<DebugEvent>>,
|
debug_tx: Option<mpsc::UnboundedSender<DebugEvent>>,
|
||||||
|
|
@ -1212,6 +1213,7 @@ impl EditPredictionStore {
|
||||||
debug_tx: None,
|
debug_tx: None,
|
||||||
registered_buffers: HashMap::default(),
|
registered_buffers: HashMap::default(),
|
||||||
current_prediction: None,
|
current_prediction: None,
|
||||||
|
last_edit_source: None,
|
||||||
cancelled_predictions: HashSet::default(),
|
cancelled_predictions: HashSet::default(),
|
||||||
pending_predictions: ArrayVec::new(),
|
pending_predictions: ArrayVec::new(),
|
||||||
next_pending_prediction_id: 0,
|
next_pending_prediction_id: 0,
|
||||||
|
|
@ -1315,6 +1317,9 @@ impl EditPredictionStore {
|
||||||
}
|
}
|
||||||
// TODO [zeta2] init with recent paths
|
// TODO [zeta2] init with recent paths
|
||||||
match event {
|
match event {
|
||||||
|
project::Event::BufferEdited { source } => {
|
||||||
|
self.get_or_init_project(&project, cx).last_edit_source = Some(*source);
|
||||||
|
}
|
||||||
project::Event::ActiveEntryChanged(Some(active_entry_id)) => {
|
project::Event::ActiveEntryChanged(Some(active_entry_id)) => {
|
||||||
let Some(project_state) = self.projects.get_mut(&project.entity_id()) else {
|
let Some(project_state) = self.projects.get_mut(&project.entity_id()) else {
|
||||||
return;
|
return;
|
||||||
|
|
@ -1332,6 +1337,15 @@ impl EditPredictionStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
project::Event::DiagnosticsUpdated { .. } => {
|
project::Event::DiagnosticsUpdated { .. } => {
|
||||||
|
if self
|
||||||
|
.projects
|
||||||
|
.get(&project.entity_id())
|
||||||
|
.and_then(|project_state| project_state.last_edit_source)
|
||||||
|
== Some(BufferEditSource::Agent)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if cx.has_flag::<EditPredictionJumpsFeatureFlag>() {
|
if cx.has_flag::<EditPredictionJumpsFeatureFlag>() {
|
||||||
self.refresh_prediction_from_diagnostics(
|
self.refresh_prediction_from_diagnostics(
|
||||||
project,
|
project,
|
||||||
|
|
@ -1391,11 +1405,17 @@ impl EditPredictionStore {
|
||||||
cx.subscribe(buffer, {
|
cx.subscribe(buffer, {
|
||||||
let project = project.downgrade();
|
let project = project.downgrade();
|
||||||
move |this, buffer, event, cx| {
|
move |this, buffer, event, cx| {
|
||||||
if let language::BufferEvent::Edited { is_local } = event
|
if let language::BufferEvent::Edited { source } = event
|
||||||
&& let Some(project) = project.upgrade()
|
&& let Some(project) = project.upgrade()
|
||||||
{
|
{
|
||||||
|
let project_state = this.get_or_init_project(&project, cx);
|
||||||
|
project_state.last_edit_source = Some(*source);
|
||||||
this.report_changes_for_buffer(
|
this.report_changes_for_buffer(
|
||||||
&buffer, &project, false, *is_local, cx,
|
&buffer,
|
||||||
|
&project,
|
||||||
|
false,
|
||||||
|
source.is_local(),
|
||||||
|
cx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2530,6 +2550,15 @@ impl EditPredictionStore {
|
||||||
allow_jump: bool,
|
allow_jump: bool,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Task<Result<Option<EditPredictionResult>>> {
|
) -> Task<Result<Option<EditPredictionResult>>> {
|
||||||
|
let is_cloud_zeta = matches!(self.edit_prediction_model, EditPredictionModel::Zeta)
|
||||||
|
&& !matches!(
|
||||||
|
all_language_settings(None, cx).edit_predictions.provider,
|
||||||
|
EditPredictionProvider::Ollama | EditPredictionProvider::OpenAiCompatibleApi
|
||||||
|
);
|
||||||
|
if is_cloud_zeta && !self.client.cloud_client().has_credentials() {
|
||||||
|
return Task::ready(Ok(None));
|
||||||
|
}
|
||||||
|
|
||||||
self.get_or_init_project(&project, cx);
|
self.get_or_init_project(&project, cx);
|
||||||
let project_state = self.projects.get(&project.entity_id()).unwrap();
|
let project_state = self.projects.get(&project.entity_id()).unwrap();
|
||||||
let stored_events = project_state.events(cx);
|
let stored_events = project_state.events(cx);
|
||||||
|
|
@ -2551,11 +2580,24 @@ impl EditPredictionStore {
|
||||||
EditPredictionsMode::Subtle => PredictEditsMode::Subtle,
|
EditPredictionsMode::Subtle => PredictEditsMode::Subtle,
|
||||||
};
|
};
|
||||||
|
|
||||||
let is_open_source = snapshot
|
let buffer_id = active_buffer.read(cx).remote_id();
|
||||||
|
let repo_url = project
|
||||||
|
.read(cx)
|
||||||
|
.git_store()
|
||||||
|
.read(cx)
|
||||||
|
.repository_and_path_for_buffer_id(buffer_id, cx)
|
||||||
|
.and_then(|(repo, _)| repo.read(cx).default_remote_url());
|
||||||
|
|
||||||
|
let is_staff_zed_repo = cx.is_staff()
|
||||||
|
&& repo_url
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|url| is_zed_industries_repo(url));
|
||||||
|
let is_open_source = is_staff_zed_repo
|
||||||
|
|| (snapshot
|
||||||
.file()
|
.file()
|
||||||
.map_or(false, |file| self.is_file_open_source(&project, file, cx))
|
.map_or(false, |file| self.is_file_open_source(&project, file, cx))
|
||||||
&& events.iter().all(|event| event.in_open_source_repo())
|
&& events.iter().all(|event| event.in_open_source_repo())
|
||||||
&& related_files.iter().all(|file| file.in_open_source_repo);
|
&& related_files.iter().all(|file| file.in_open_source_repo));
|
||||||
|
|
||||||
let can_collect_data = !cfg!(test)
|
let can_collect_data = !cfg!(test)
|
||||||
&& is_open_source
|
&& is_open_source
|
||||||
|
|
@ -2594,7 +2636,7 @@ impl EditPredictionStore {
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
zeta::request_prediction_with_zeta(self, inputs, capture_events, cx)
|
zeta::request_prediction_with_zeta(self, inputs, capture_events, repo_url, cx)
|
||||||
}
|
}
|
||||||
EditPredictionModel::Fim { format } => fim::request_prediction(inputs, format, cx),
|
EditPredictionModel::Fim { format } => fim::request_prediction(inputs, format, cx),
|
||||||
EditPredictionModel::Mercury => {
|
EditPredictionModel::Mercury => {
|
||||||
|
|
@ -3286,3 +3328,11 @@ pub fn init(cx: &mut App) {
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_zed_industries_repo(url: &str) -> bool {
|
||||||
|
url.strip_prefix("https://github.com/zed-industries/")
|
||||||
|
.or_else(|| url.strip_prefix("http://github.com/zed-industries/"))
|
||||||
|
.or_else(|| url.strip_prefix("git@github.com:zed-industries/"))
|
||||||
|
.or_else(|| url.strip_prefix("ssh://git@github.com/zed-industries/"))
|
||||||
|
.is_some_and(|repo| !repo.is_empty())
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,8 @@ use gpui::{
|
||||||
};
|
};
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
use language::{
|
use language::{
|
||||||
Anchor, Buffer, Capability, CursorShape, Diagnostic, DiagnosticEntry, DiagnosticSet,
|
Anchor, Buffer, BufferEditSource, Capability, CursorShape, Diagnostic, DiagnosticEntry,
|
||||||
DiagnosticSeverity, Operation, Point, Selection, SelectionGoal,
|
DiagnosticSet, DiagnosticSeverity, Operation, Point, Selection, SelectionGoal,
|
||||||
};
|
};
|
||||||
|
|
||||||
use lsp::LanguageServerId;
|
use lsp::LanguageServerId;
|
||||||
|
|
@ -352,6 +352,70 @@ async fn test_diagnostics_refresh_suppressed_while_following(cx: &mut TestAppCon
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_diagnostics_refresh_suppressed_after_agent_edit(cx: &mut TestAppContext) {
|
||||||
|
let (ep_store, mut requests) = init_test_with_fake_client(cx);
|
||||||
|
|
||||||
|
cx.update(|cx| {
|
||||||
|
cx.update_flags(
|
||||||
|
false,
|
||||||
|
vec![EditPredictionJumpsFeatureFlag::NAME.to_string()],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
fs.insert_tree(
|
||||||
|
"/root",
|
||||||
|
json!({
|
||||||
|
"1.txt": "Hello!\nHow\nBye\n",
|
||||||
|
"2.txt": "Hola!\nComo\nAdios\n"
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let project = Project::test(fs, vec![path!("/root").as_ref()], cx).await;
|
||||||
|
|
||||||
|
let buffer = project
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
let path = project.find_project_path(path!("root/1.txt"), cx).unwrap();
|
||||||
|
project.set_active_path(Some(path.clone()), cx);
|
||||||
|
project.open_buffer(path, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
ep_store.update(cx, |ep_store, cx| {
|
||||||
|
ep_store.register_project(&project, cx);
|
||||||
|
ep_store.register_buffer(&buffer, &project, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
buffer.update(cx, |buffer, cx| {
|
||||||
|
buffer.start_transaction();
|
||||||
|
buffer.edit([(Point::new(1, 3)..Point::new(1, 3), "!")], None, cx);
|
||||||
|
buffer.end_transaction_with_source(BufferEditSource::Agent, cx);
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
update_test_diagnostics(&project, path!("/root/2.txt"), "Sentence is incomplete", cx);
|
||||||
|
cx.run_until_parked();
|
||||||
|
assert_no_predict_request_ready(&mut requests.predict);
|
||||||
|
|
||||||
|
buffer.update(cx, |buffer, cx| {
|
||||||
|
buffer.edit([(Point::new(1, 4)..Point::new(1, 4), "?")], None, cx);
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
update_test_diagnostics(
|
||||||
|
&project,
|
||||||
|
path!("/root/2.txt"),
|
||||||
|
"Sentence is still incomplete",
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
let (_request, respond_tx) = requests.predict.next().await.unwrap();
|
||||||
|
respond_tx.send(empty_response()).unwrap();
|
||||||
|
cx.run_until_parked();
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_simple_request(cx: &mut TestAppContext) {
|
async fn test_simple_request(cx: &mut TestAppContext) {
|
||||||
let (ep_store, mut requests) = init_test_with_fake_client(cx);
|
let (ep_store, mut requests) = init_test_with_fake_client(cx);
|
||||||
|
|
@ -2498,6 +2562,39 @@ fn assert_no_predict_request_ready(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update_test_diagnostics(
|
||||||
|
project: &Entity<Project>,
|
||||||
|
path: &str,
|
||||||
|
message: &str,
|
||||||
|
cx: &mut TestAppContext,
|
||||||
|
) {
|
||||||
|
let diagnostic = lsp::Diagnostic {
|
||||||
|
range: lsp::Range::new(lsp::Position::new(1, 1), lsp::Position::new(1, 5)),
|
||||||
|
severity: Some(lsp::DiagnosticSeverity::ERROR),
|
||||||
|
message: message.to_string(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
project.update(cx, |project, cx| {
|
||||||
|
project.lsp_store().update(cx, |lsp_store, cx| {
|
||||||
|
lsp_store
|
||||||
|
.update_diagnostics(
|
||||||
|
LanguageServerId(0),
|
||||||
|
lsp::PublishDiagnosticsParams {
|
||||||
|
uri: lsp::Uri::from_file_path(path).unwrap(),
|
||||||
|
diagnostics: vec![diagnostic],
|
||||||
|
version: None,
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
language::DiagnosticSourceKind::Pushed,
|
||||||
|
&[],
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
struct RequestChannels {
|
struct RequestChannels {
|
||||||
predict: mpsc::UnboundedReceiver<(
|
predict: mpsc::UnboundedReceiver<(
|
||||||
PredictEditsV3Request,
|
PredictEditsV3Request,
|
||||||
|
|
@ -3107,11 +3204,18 @@ async fn test_unauthenticated_without_custom_url_blocks_prediction_impl(cx: &mut
|
||||||
|
|
||||||
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
||||||
|
|
||||||
let http_client = FakeHttpClient::create(|_req| async move {
|
let request_count = Arc::new(std::sync::atomic::AtomicUsize::default());
|
||||||
|
let http_client = FakeHttpClient::create({
|
||||||
|
let request_count = request_count.clone();
|
||||||
|
move |_req| {
|
||||||
|
request_count.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||||
|
async move {
|
||||||
Ok(gpui::http_client::Response::builder()
|
Ok(gpui::http_client::Response::builder()
|
||||||
.status(401)
|
.status(401)
|
||||||
.body("Unauthorized".into())
|
.body("Unauthorized".into())
|
||||||
.unwrap())
|
.unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let client =
|
let client =
|
||||||
|
|
@ -3144,11 +3248,8 @@ async fn test_unauthenticated_without_custom_url_blocks_prediction_impl(cx: &mut
|
||||||
ep_store.request_prediction(&project, &buffer, cursor, Default::default(), cx)
|
ep_store.request_prediction(&project, &buffer, cursor, Default::default(), cx)
|
||||||
});
|
});
|
||||||
|
|
||||||
let result = completion_task.await;
|
assert!(completion_task.await.unwrap().is_none());
|
||||||
assert!(
|
assert_eq!(request_count.load(std::sync::atomic::Ordering::SeqCst), 0);
|
||||||
result.is_err(),
|
|
||||||
"Without authentication and without custom URL, prediction should fail"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ pub fn request_prediction_with_zeta(
|
||||||
Vec<crate::StoredEvent>,
|
Vec<crate::StoredEvent>,
|
||||||
Task<Result<collections::HashMap<Arc<Path>, Entity<BufferDiff>>>>,
|
Task<Result<collections::HashMap<Arc<Path>, Entity<BufferDiff>>>>,
|
||||||
)>,
|
)>,
|
||||||
|
repo_url: Option<String>,
|
||||||
cx: &mut Context<EditPredictionStore>,
|
cx: &mut Context<EditPredictionStore>,
|
||||||
) -> Task<Result<Option<EditPredictionResult>>> {
|
) -> Task<Result<Option<EditPredictionResult>>> {
|
||||||
let settings = &all_language_settings(None, cx).edit_predictions;
|
let settings = &all_language_settings(None, cx).edit_predictions;
|
||||||
|
|
@ -73,17 +74,7 @@ pub fn request_prediction_with_zeta(
|
||||||
|
|
||||||
let excerpt_path = buffer_path_with_id_fallback(snapshot.file(), &snapshot.text, cx);
|
let excerpt_path = buffer_path_with_id_fallback(snapshot.file(), &snapshot.text, cx);
|
||||||
|
|
||||||
let repo_url = if can_collect_data {
|
let repo_url = repo_url.filter(|_| can_collect_data);
|
||||||
let buffer_id = buffer.read(cx).remote_id();
|
|
||||||
project
|
|
||||||
.read(cx)
|
|
||||||
.git_store()
|
|
||||||
.read(cx)
|
|
||||||
.repository_and_path_for_buffer_id(buffer_id, cx)
|
|
||||||
.and_then(|(repo, _)| repo.read(cx).default_remote_url())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
let client = store.client.clone();
|
let client = store.client.clone();
|
||||||
let llm_token = store.llm_token.clone();
|
let llm_token = store.llm_token.clone();
|
||||||
let organization_id = store
|
let organization_id = store
|
||||||
|
|
|
||||||
|
|
@ -323,7 +323,8 @@ pub struct SplitSelectionIntoLines {
|
||||||
pub keep_selections: bool,
|
pub keep_selections: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Goes to the next diagnostic in the file.
|
/// Expands the diagnostic under the cursor, if any, in case diagnostics are not
|
||||||
|
/// yet active. Otherwise, goes to the next diagnostic in the file.
|
||||||
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
|
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
|
||||||
#[action(namespace = editor)]
|
#[action(namespace = editor)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
|
|
@ -332,7 +333,8 @@ pub struct GoToDiagnostic {
|
||||||
pub severity: GoToDiagnosticSeverityFilter,
|
pub severity: GoToDiagnosticSeverityFilter,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Goes to the previous diagnostic in the file.
|
/// Expands the diagnostic under the cursor, if any, in case diagnostics are not
|
||||||
|
/// yet active. Otherwise, goes to the previous diagnostic in the file.
|
||||||
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
|
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
|
||||||
#[action(namespace = editor)]
|
#[action(namespace = editor)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,8 @@ impl Editor {
|
||||||
if !self.diagnostics_enabled() {
|
if !self.diagnostics_enabled() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.go_to_diagnostic_impl(Direction::Next, action.severity, window, cx)
|
|
||||||
|
self.go_to_diagnostic_at_cursor(Direction::Next, action.severity, window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn go_to_prev_diagnostic(
|
pub fn go_to_prev_diagnostic(
|
||||||
|
|
@ -89,10 +90,43 @@ impl Editor {
|
||||||
if !self.diagnostics_enabled() {
|
if !self.diagnostics_enabled() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.go_to_diagnostic_impl(Direction::Prev, action.severity, window, cx)
|
|
||||||
|
self.go_to_diagnostic_at_cursor(Direction::Prev, action.severity, window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn go_to_diagnostic_impl(
|
fn diagnostics_before_cursor<'a>(
|
||||||
|
buffer: &'a MultiBufferSnapshot,
|
||||||
|
cursor: MultiBufferOffset,
|
||||||
|
severity: GoToDiagnosticSeverityFilter,
|
||||||
|
) -> impl Iterator<Item = DiagnosticEntryRef<'a, MultiBufferOffset>> {
|
||||||
|
buffer
|
||||||
|
.diagnostics_in_range(MultiBufferOffset(0)..cursor)
|
||||||
|
.filter(move |entry| entry.range.start <= cursor)
|
||||||
|
.filter(move |entry| severity.matches(entry.diagnostic.severity))
|
||||||
|
.filter(|entry| entry.range.start != entry.range.end)
|
||||||
|
.filter(|entry| !entry.diagnostic.is_unnecessary)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diagnostics_after_cursor<'a>(
|
||||||
|
buffer: &'a MultiBufferSnapshot,
|
||||||
|
cursor: MultiBufferOffset,
|
||||||
|
severity: GoToDiagnosticSeverityFilter,
|
||||||
|
) -> impl Iterator<Item = DiagnosticEntryRef<'a, MultiBufferOffset>> {
|
||||||
|
buffer
|
||||||
|
.diagnostics_in_range(cursor..buffer.len())
|
||||||
|
.filter(move |entry| entry.range.start >= cursor)
|
||||||
|
.filter(move |entry| severity.matches(entry.diagnostic.severity))
|
||||||
|
.filter(|entry| entry.range.start != entry.range.end)
|
||||||
|
.filter(|entry| !entry.diagnostic.is_unnecessary)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempts to expand the diagnostic at the current cursor position,
|
||||||
|
/// updating the cursor position to the diagnostic's start point.
|
||||||
|
///
|
||||||
|
/// In case there's no diagnostic at the current cursor position, this will
|
||||||
|
/// fallback to finding the next or previous diagnostic instead, depending
|
||||||
|
/// on the provided `direction`.
|
||||||
|
pub fn go_to_diagnostic_at_cursor(
|
||||||
&mut self,
|
&mut self,
|
||||||
direction: Direction,
|
direction: Direction,
|
||||||
severity: GoToDiagnosticSeverityFilter,
|
severity: GoToDiagnosticSeverityFilter,
|
||||||
|
|
@ -104,6 +138,71 @@ impl Editor {
|
||||||
.selections
|
.selections
|
||||||
.newest::<MultiBufferOffset>(&self.display_snapshot(cx));
|
.newest::<MultiBufferOffset>(&self.display_snapshot(cx));
|
||||||
|
|
||||||
|
let before = Self::diagnostics_before_cursor(&buffer, selection.start, severity);
|
||||||
|
let after = Self::diagnostics_after_cursor(&buffer, selection.start, severity);
|
||||||
|
let active_group_id = match &self.active_diagnostics {
|
||||||
|
ActiveDiagnostic::Group(group) => Some(group.group_id),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut cursor_on_active = false;
|
||||||
|
let mut target = None;
|
||||||
|
|
||||||
|
for diagnostic in after.chain(before) {
|
||||||
|
let contains_cursor = diagnostic.range.contains(&selection.start)
|
||||||
|
|| diagnostic.range.end == selection.head();
|
||||||
|
|
||||||
|
if !contains_cursor {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if active_group_id == Some(diagnostic.diagnostic.group_id) {
|
||||||
|
cursor_on_active = true;
|
||||||
|
} else if target.is_none() {
|
||||||
|
target = Some(diagnostic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match (target, cursor_on_active) {
|
||||||
|
(Some(diagnostic), false) => self.activate_diagnostic(&buffer, diagnostic, window, cx),
|
||||||
|
_ => self.go_to_diagnostic_in_direction(
|
||||||
|
&buffer, &selection, direction, severity, window, cx,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn activate_diagnostic(
|
||||||
|
&mut self,
|
||||||
|
buffer: &MultiBufferSnapshot,
|
||||||
|
diagnostic: DiagnosticEntryRef<MultiBufferOffset>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let diagnostic_start = buffer.anchor_after(diagnostic.range.start);
|
||||||
|
let Some((buffer_anchor, _)) = buffer.anchor_to_buffer_anchor(diagnostic_start) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let buffer_id = buffer_anchor.buffer_id;
|
||||||
|
let snapshot = self.snapshot(window, cx);
|
||||||
|
if snapshot.intersects_fold(diagnostic.range.start) {
|
||||||
|
self.unfold_ranges(std::slice::from_ref(&diagnostic.range), true, false, cx);
|
||||||
|
}
|
||||||
|
self.change_selections(Default::default(), window, cx, |s| {
|
||||||
|
s.select_ranges(vec![diagnostic.range.start..diagnostic.range.start])
|
||||||
|
});
|
||||||
|
self.activate_diagnostics(buffer_id, diagnostic, window, cx);
|
||||||
|
self.refresh_edit_prediction(false, true, window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn go_to_diagnostic_in_direction(
|
||||||
|
&mut self,
|
||||||
|
buffer: &MultiBufferSnapshot,
|
||||||
|
selection: &Selection<MultiBufferOffset>,
|
||||||
|
direction: Direction,
|
||||||
|
severity: GoToDiagnosticSeverityFilter,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
let mut active_group_id = None;
|
let mut active_group_id = None;
|
||||||
if let ActiveDiagnostic::Group(active_group) = &self.active_diagnostics
|
if let ActiveDiagnostic::Group(active_group) = &self.active_diagnostics
|
||||||
&& active_group.active_range.start.to_offset(&buffer) == selection.start
|
&& active_group.active_range.start.to_offset(&buffer) == selection.start
|
||||||
|
|
@ -111,28 +210,8 @@ impl Editor {
|
||||||
active_group_id = Some(active_group.group_id);
|
active_group_id = Some(active_group.group_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn filtered<'a>(
|
let before = Self::diagnostics_before_cursor(&buffer, selection.start, severity);
|
||||||
severity: GoToDiagnosticSeverityFilter,
|
let after = Self::diagnostics_after_cursor(&buffer, selection.start, severity);
|
||||||
diagnostics: impl Iterator<Item = DiagnosticEntryRef<'a, MultiBufferOffset>>,
|
|
||||||
) -> impl Iterator<Item = DiagnosticEntryRef<'a, MultiBufferOffset>> {
|
|
||||||
diagnostics
|
|
||||||
.filter(move |entry| severity.matches(entry.diagnostic.severity))
|
|
||||||
.filter(|entry| entry.range.start != entry.range.end)
|
|
||||||
.filter(|entry| !entry.diagnostic.is_unnecessary)
|
|
||||||
}
|
|
||||||
|
|
||||||
let before = filtered(
|
|
||||||
severity,
|
|
||||||
buffer
|
|
||||||
.diagnostics_in_range(MultiBufferOffset(0)..selection.start)
|
|
||||||
.filter(|entry| entry.range.start <= selection.start),
|
|
||||||
);
|
|
||||||
let after = filtered(
|
|
||||||
severity,
|
|
||||||
buffer
|
|
||||||
.diagnostics_in_range(selection.start..buffer.len())
|
|
||||||
.filter(|entry| entry.range.start >= selection.start),
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut found: Option<DiagnosticEntryRef<MultiBufferOffset>> = None;
|
let mut found: Option<DiagnosticEntryRef<MultiBufferOffset>> = None;
|
||||||
if direction == Direction::Prev {
|
if direction == Direction::Prev {
|
||||||
|
|
@ -158,31 +237,12 @@ impl Editor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(next_diagnostic) = found else {
|
let Some(next_diagnostic) = found else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let next_diagnostic_start = buffer.anchor_after(next_diagnostic.range.start);
|
self.activate_diagnostic(&buffer, next_diagnostic, window, cx);
|
||||||
let Some((buffer_anchor, _)) = buffer.anchor_to_buffer_anchor(next_diagnostic_start) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let buffer_id = buffer_anchor.buffer_id;
|
|
||||||
let snapshot = self.snapshot(window, cx);
|
|
||||||
if snapshot.intersects_fold(next_diagnostic.range.start) {
|
|
||||||
self.unfold_ranges(
|
|
||||||
std::slice::from_ref(&next_diagnostic.range),
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
self.change_selections(Default::default(), window, cx, |s| {
|
|
||||||
s.select_ranges(vec![
|
|
||||||
next_diagnostic.range.start..next_diagnostic.range.start,
|
|
||||||
])
|
|
||||||
});
|
|
||||||
self.activate_diagnostics(buffer_id, next_diagnostic, window, cx);
|
|
||||||
self.refresh_edit_prediction(false, true, window, cx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
|
@ -324,12 +384,10 @@ impl Editor {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.dismiss_diagnostics(cx);
|
self.dismiss_diagnostics(cx);
|
||||||
let snapshot = self.snapshot(window, cx);
|
|
||||||
let buffer = self.buffer.read(cx).snapshot(cx);
|
let buffer = self.buffer.read(cx).snapshot(cx);
|
||||||
let Some(renderer) = GlobalDiagnosticRenderer::global(cx) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
let blocks = if let Some(renderer) = GlobalDiagnosticRenderer::global(cx) {
|
||||||
|
let snapshot = self.snapshot(window, cx);
|
||||||
let diagnostic_group = buffer
|
let diagnostic_group = buffer
|
||||||
.diagnostic_group(buffer_id, diagnostic.diagnostic.group_id)
|
.diagnostic_group(buffer_id, diagnostic.diagnostic.group_id)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
@ -347,9 +405,16 @@ impl Editor {
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
|
||||||
let blocks = self.display_map.update(cx, |display_map, cx| {
|
self.display_map.update(cx, |display_map, cx| {
|
||||||
display_map.insert_blocks(blocks, cx).into_iter().collect()
|
display_map.insert_blocks(blocks, cx).into_iter().collect()
|
||||||
});
|
})
|
||||||
|
} else {
|
||||||
|
// Ensure that, even if there's no global renderer set, we still use
|
||||||
|
// an empty set of blocks, such that we can record the active group
|
||||||
|
// below instead of bailing out.
|
||||||
|
HashSet::default()
|
||||||
|
};
|
||||||
|
|
||||||
self.active_diagnostics = ActiveDiagnostic::Group(ActiveDiagnosticGroup {
|
self.active_diagnostics = ActiveDiagnostic::Group(ActiveDiagnosticGroup {
|
||||||
active_range: buffer.anchor_before(diagnostic.range.start)
|
active_range: buffer.anchor_before(diagnostic.range.start)
|
||||||
..buffer.anchor_after(diagnostic.range.end),
|
..buffer.anchor_after(diagnostic.range.end),
|
||||||
|
|
@ -516,4 +581,12 @@ impl Editor {
|
||||||
self.scrollbar_marker_state.dirty = true;
|
self.scrollbar_marker_state.dirty = true;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn any_active_diagnostics(&self) -> bool {
|
||||||
|
match &self.active_diagnostics {
|
||||||
|
ActiveDiagnostic::None => false,
|
||||||
|
ActiveDiagnostic::All => true,
|
||||||
|
ActiveDiagnostic::Group(_) => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9231,7 +9231,7 @@ impl Editor {
|
||||||
match event {
|
match event {
|
||||||
multi_buffer::Event::Edited {
|
multi_buffer::Event::Edited {
|
||||||
edited_buffer,
|
edited_buffer,
|
||||||
is_local,
|
source,
|
||||||
} => {
|
} => {
|
||||||
self.scrollbar_marker_state.dirty = true;
|
self.scrollbar_marker_state.dirty = true;
|
||||||
self.active_indent_guides_state.dirty = true;
|
self.active_indent_guides_state.dirty = true;
|
||||||
|
|
@ -9242,7 +9242,7 @@ impl Editor {
|
||||||
self.refresh_matching_bracket_highlights(&snapshot, cx);
|
self.refresh_matching_bracket_highlights(&snapshot, cx);
|
||||||
self.refresh_outline_symbols_at_cursor(cx);
|
self.refresh_outline_symbols_at_cursor(cx);
|
||||||
self.refresh_sticky_headers(&snapshot, cx);
|
self.refresh_sticky_headers(&snapshot, cx);
|
||||||
if *is_local && self.has_active_edit_prediction() {
|
if source.is_local() && self.has_active_edit_prediction() {
|
||||||
self.update_visible_edit_prediction(window, cx);
|
self.update_visible_edit_prediction(window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20506,6 +20506,130 @@ async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mu
|
||||||
"});
|
"});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn go_to_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
|
||||||
|
init_test(cx, |_| {});
|
||||||
|
|
||||||
|
let mut cx = EditorTestContext::new(cx).await;
|
||||||
|
let lsp_store =
|
||||||
|
cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
|
||||||
|
|
||||||
|
// Place the cursor inside the `def` diagnostic (`[12, 15)`) before any
|
||||||
|
// diagnostic is active so we can later confirm that running `editor: go to
|
||||||
|
// diagnostic` will activate this diagnostic instead of advancing to the
|
||||||
|
// next one.
|
||||||
|
cx.set_state(indoc! {"
|
||||||
|
fn func(abc dˇef: i32) -> u32 {
|
||||||
|
}
|
||||||
|
"});
|
||||||
|
|
||||||
|
// Set up the diagnostics:
|
||||||
|
//
|
||||||
|
// * `[11, 12)` (the space before `def`),
|
||||||
|
// * `[12, 15)` (`def`),
|
||||||
|
// * `[25, 28)` (`u32`).
|
||||||
|
cx.update(|_, cx| {
|
||||||
|
lsp_store.update(cx, |lsp_store, cx| {
|
||||||
|
lsp_store
|
||||||
|
.update_diagnostics(
|
||||||
|
LanguageServerId(0),
|
||||||
|
lsp::PublishDiagnosticsParams {
|
||||||
|
uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
|
||||||
|
version: None,
|
||||||
|
diagnostics: vec![
|
||||||
|
lsp::Diagnostic {
|
||||||
|
range: lsp::Range::new(
|
||||||
|
lsp::Position::new(0, 11),
|
||||||
|
lsp::Position::new(0, 12),
|
||||||
|
),
|
||||||
|
severity: Some(lsp::DiagnosticSeverity::ERROR),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
lsp::Diagnostic {
|
||||||
|
range: lsp::Range::new(
|
||||||
|
lsp::Position::new(0, 12),
|
||||||
|
lsp::Position::new(0, 15),
|
||||||
|
),
|
||||||
|
severity: Some(lsp::DiagnosticSeverity::ERROR),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
lsp::Diagnostic {
|
||||||
|
range: lsp::Range::new(
|
||||||
|
lsp::Position::new(0, 25),
|
||||||
|
lsp::Position::new(0, 28),
|
||||||
|
),
|
||||||
|
severity: Some(lsp::DiagnosticSeverity::ERROR),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
DiagnosticSourceKind::Pushed,
|
||||||
|
&[],
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
executor.run_until_parked();
|
||||||
|
|
||||||
|
// When the cursor is at an inactive diagnostic, cursor should be moved to
|
||||||
|
// the start of that same diagnostic and activate it.
|
||||||
|
cx.update_editor(|editor, window, cx| {
|
||||||
|
editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
|
||||||
|
});
|
||||||
|
cx.assert_editor_state(indoc! {"
|
||||||
|
fn func(abc ˇdef: i32) -> u32 {
|
||||||
|
}
|
||||||
|
"});
|
||||||
|
|
||||||
|
cx.update_editor(|editor, window, cx| {
|
||||||
|
editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
|
||||||
|
});
|
||||||
|
cx.assert_editor_state(indoc! {"
|
||||||
|
fn func(abc def: i32) -> ˇu32 {
|
||||||
|
}
|
||||||
|
"});
|
||||||
|
|
||||||
|
cx.update_editor(|editor, window, cx| {
|
||||||
|
editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
|
||||||
|
});
|
||||||
|
cx.assert_editor_state(indoc! {"
|
||||||
|
fn func(abcˇ def: i32) -> u32 {
|
||||||
|
}
|
||||||
|
"});
|
||||||
|
|
||||||
|
// Manually move the cursor to a different, not yet active diagnostic to
|
||||||
|
// confirm that using `editor: go to diagnostic` will now activate this one.
|
||||||
|
cx.update_editor(|editor, window, cx| {
|
||||||
|
editor.change_selections(Default::default(), window, cx, |s| {
|
||||||
|
s.select_ranges([Point::new(0, 26)..Point::new(0, 26)])
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.update_editor(|editor, window, cx| {
|
||||||
|
editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
|
||||||
|
});
|
||||||
|
cx.assert_editor_state(indoc! {"
|
||||||
|
fn func(abc def: i32) -> ˇu32 {
|
||||||
|
}
|
||||||
|
"});
|
||||||
|
|
||||||
|
cx.update_editor(|editor, window, cx| {
|
||||||
|
editor.change_selections(Default::default(), window, cx, |s| {
|
||||||
|
s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
|
||||||
|
});
|
||||||
|
});
|
||||||
|
cx.update_editor(|editor, window, cx| {
|
||||||
|
editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
|
||||||
|
});
|
||||||
|
cx.assert_editor_state(indoc! {"
|
||||||
|
fn func(abcˇ def: i32) -> u32 {
|
||||||
|
}
|
||||||
|
"});
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
|
async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
|
||||||
init_test(cx, |_| {});
|
init_test(cx, |_| {});
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
1055
crates/editor/src/element/header.rs
Normal file
1055
crates/editor/src/element/header.rs
Normal file
File diff suppressed because it is too large
Load diff
1187
crates/editor/src/element/mouse.rs
Normal file
1187
crates/editor/src/element/mouse.rs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1088,6 +1088,7 @@ impl InfoPopover {
|
||||||
.track_scroll(&self.scroll_handle)
|
.track_scroll(&self.scroll_handle)
|
||||||
.child(
|
.child(
|
||||||
MarkdownElement::new(markdown, hover_markdown_style(window, cx))
|
MarkdownElement::new(markdown, hover_markdown_style(window, cx))
|
||||||
|
.scroll_handle(self.scroll_handle.clone())
|
||||||
.code_block_renderer(markdown::CodeBlockRenderer::Default {
|
.code_block_renderer(markdown::CodeBlockRenderer::Default {
|
||||||
copy_button_visibility: CopyButtonVisibility::Hidden,
|
copy_button_visibility: CopyButtonVisibility::Hidden,
|
||||||
wrap_button_visibility: markdown::WrapButtonVisibility::Hidden,
|
wrap_button_visibility: markdown::WrapButtonVisibility::Hidden,
|
||||||
|
|
|
||||||
|
|
@ -164,12 +164,14 @@ pub fn lsp_tasks(
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
if !new_lsp_tasks.is_empty() {
|
||||||
lsp_tasks
|
lsp_tasks
|
||||||
.entry(source_kind)
|
.entry(source_kind)
|
||||||
.or_insert_with(Vec::new)
|
.or_insert_with(Vec::new)
|
||||||
.append(&mut new_lsp_tasks);
|
.append(&mut new_lsp_tasks);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
lsp_tasks.into_iter().collect()
|
lsp_tasks.into_iter().collect()
|
||||||
})
|
})
|
||||||
.with_timeout(Duration::from_millis(200), &cx.background_executor())
|
.with_timeout(Duration::from_millis(200), &cx.background_executor())
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,18 @@ impl FeatureFlag for AgentSharingFeatureFlag {
|
||||||
}
|
}
|
||||||
register_feature_flag!(AgentSharingFeatureFlag);
|
register_feature_flag!(AgentSharingFeatureFlag);
|
||||||
|
|
||||||
|
pub struct HandoffFeatureFlag;
|
||||||
|
|
||||||
|
impl FeatureFlag for HandoffFeatureFlag {
|
||||||
|
const NAME: &'static str = "handoff";
|
||||||
|
type Value = PresenceFlag;
|
||||||
|
|
||||||
|
fn enabled_for_staff() -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
register_feature_flag!(HandoffFeatureFlag);
|
||||||
|
|
||||||
pub struct DiffReviewFeatureFlag;
|
pub struct DiffReviewFeatureFlag;
|
||||||
|
|
||||||
impl FeatureFlag for DiffReviewFeatureFlag {
|
impl FeatureFlag for DiffReviewFeatureFlag {
|
||||||
|
|
|
||||||
|
|
@ -238,7 +238,11 @@ impl BlameRenderer for GitBlameRenderer {
|
||||||
|
|
||||||
let message = details
|
let message = details
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|_| MarkdownElement::new(markdown.clone(), markdown_style).into_any())
|
.map(|_| {
|
||||||
|
MarkdownElement::new(markdown.clone(), markdown_style)
|
||||||
|
.scroll_handle(scroll_handle.clone())
|
||||||
|
.into_any()
|
||||||
|
})
|
||||||
.unwrap_or("<no commit message>".into_any());
|
.unwrap_or("<no commit message>".into_any());
|
||||||
|
|
||||||
let pull_request = details
|
let pull_request = details
|
||||||
|
|
|
||||||
|
|
@ -258,7 +258,11 @@ impl Render for CommitTooltip {
|
||||||
.commit
|
.commit
|
||||||
.message
|
.message
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|_| MarkdownElement::new(self.markdown.clone(), markdown_style).into_any())
|
.map(|_| {
|
||||||
|
MarkdownElement::new(self.markdown.clone(), markdown_style)
|
||||||
|
.scroll_handle(self.scroll_handle.clone())
|
||||||
|
.into_any()
|
||||||
|
})
|
||||||
.unwrap_or("<no commit message>".into_any());
|
.unwrap_or("<no commit message>".into_any());
|
||||||
|
|
||||||
let pull_request = self
|
let pull_request = self
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ use crate::commit_view::CommitView;
|
||||||
use crate::git_panel_settings::GitPanelScrollbarAccessor;
|
use crate::git_panel_settings::GitPanelScrollbarAccessor;
|
||||||
use crate::project_diff::{self, BranchDiff, Diff, ProjectDiff};
|
use crate::project_diff::{self, BranchDiff, Diff, ProjectDiff};
|
||||||
use crate::remote_output::{self, RemoteAction, SuccessMessage};
|
use crate::remote_output::{self, RemoteAction, SuccessMessage};
|
||||||
|
use crate::solo_diff_view::SoloDiffView;
|
||||||
use crate::{branch_picker, picker_prompt, render_remote_button};
|
use crate::{branch_picker, picker_prompt, render_remote_button};
|
||||||
use crate::{
|
use crate::{
|
||||||
git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector,
|
git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector,
|
||||||
|
|
@ -15,10 +16,7 @@ use anyhow::Context as _;
|
||||||
use askpass::AskPassDelegate;
|
use askpass::AskPassDelegate;
|
||||||
use collections::{BTreeMap, HashMap, HashSet};
|
use collections::{BTreeMap, HashMap, HashSet};
|
||||||
use db::kvp::KeyValueStore;
|
use db::kvp::KeyValueStore;
|
||||||
use editor::{
|
use editor::{Editor, EditorElement, EditorMode, MultiBuffer, MultiBufferOffset, SizingBehavior};
|
||||||
Direction, Editor, EditorElement, EditorMode, MultiBuffer, MultiBufferOffset, SizingBehavior,
|
|
||||||
actions::ExpandAllDiffHunks,
|
|
||||||
};
|
|
||||||
use editor::{EditorStyle, RewrapOptions};
|
use editor::{EditorStyle, RewrapOptions};
|
||||||
use file_icons::FileIcons;
|
use file_icons::FileIcons;
|
||||||
use futures::StreamExt as _;
|
use futures::StreamExt as _;
|
||||||
|
|
@ -62,7 +60,7 @@ use project::{
|
||||||
},
|
},
|
||||||
project_settings::{GitPathStyle, ProjectSettings},
|
project_settings::{GitPathStyle, ProjectSettings},
|
||||||
};
|
};
|
||||||
use prompt_store::{BuiltInPrompt, PromptId, PromptStore, RULES_FILE_NAMES};
|
use prompt_store::RULES_FILE_NAMES;
|
||||||
use proto::RpcError;
|
use proto::RpcError;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::{Settings, SettingsStore, StatusStyle, update_settings_file};
|
use settings::{Settings, SettingsStore, StatusStyle, update_settings_file};
|
||||||
|
|
@ -85,7 +83,7 @@ use workspace::SERIALIZATION_THROTTLE_TIME;
|
||||||
use workspace::{
|
use workspace::{
|
||||||
Item, Workspace,
|
Item, Workspace,
|
||||||
dock::{DockPosition, Panel, PanelEvent},
|
dock::{DockPosition, Panel, PanelEvent},
|
||||||
notifications::{DetachAndPromptErr, ErrorMessagePrompt, NotificationId, NotifyResultExt},
|
notifications::{DetachAndPromptErr, ErrorMessagePrompt, NotificationId, NotifyTaskExt},
|
||||||
};
|
};
|
||||||
use zed_actions::{DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize};
|
use zed_actions::{DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize};
|
||||||
|
|
||||||
|
|
@ -1385,63 +1383,22 @@ impl GitPanel {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open_file(
|
fn open_solo_diff(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: &menu::SecondaryConfirm,
|
_: &menu::SecondaryConfirm,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
maybe!({
|
maybe!({
|
||||||
let entry = self.entries.get(self.selected_entry?)?.status_entry()?;
|
let entry = self
|
||||||
let active_repo = self.active_repository.as_ref()?;
|
.entries
|
||||||
let path = active_repo
|
.get(self.selected_entry?)?
|
||||||
.read(cx)
|
.status_entry()?
|
||||||
.repo_path_to_project_path(&entry.repo_path, cx)?;
|
.clone();
|
||||||
if entry.status.is_deleted() {
|
let repository = self.active_repository.clone()?;
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let open_task = self
|
SoloDiffView::open_or_focus(entry, repository, self.workspace.clone(), window, cx)
|
||||||
.workspace
|
.detach_and_notify_err(self.workspace.clone(), window, cx);
|
||||||
.update(cx, |workspace, cx| {
|
|
||||||
workspace.open_path_preview(path, None, false, false, true, window, cx)
|
|
||||||
})
|
|
||||||
.ok()?;
|
|
||||||
|
|
||||||
let workspace = self.workspace.clone();
|
|
||||||
cx.spawn_in(window, async move |_, mut cx| {
|
|
||||||
let item = open_task
|
|
||||||
.await
|
|
||||||
.notify_workspace_async_err(workspace, &mut cx)
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("Failed to open file"))?;
|
|
||||||
if let Some(active_editor) = item.downcast::<Editor>() {
|
|
||||||
if let Some(diff_task) =
|
|
||||||
active_editor.update(cx, |editor, _cx| editor.wait_for_diff_to_load())
|
|
||||||
{
|
|
||||||
diff_task.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.update(|window, cx| {
|
|
||||||
active_editor.update(cx, |editor, cx| {
|
|
||||||
editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
|
|
||||||
|
|
||||||
let snapshot = editor.snapshot(window, cx);
|
|
||||||
editor.go_to_hunk_before_or_after_position(
|
|
||||||
&snapshot,
|
|
||||||
language::Point::new(0, 0),
|
|
||||||
Direction::Next,
|
|
||||||
true,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.log_err();
|
|
||||||
}
|
|
||||||
|
|
||||||
anyhow::Ok(())
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
Some(())
|
Some(())
|
||||||
});
|
});
|
||||||
|
|
@ -2685,20 +2642,6 @@ impl GitPanel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn load_commit_message_prompt(cx: &mut AsyncApp) -> String {
|
|
||||||
let load = async {
|
|
||||||
let store = cx.update(|cx| PromptStore::global(cx)).await.ok()?;
|
|
||||||
store
|
|
||||||
.update(cx, |s, cx| {
|
|
||||||
s.load(PromptId::BuiltIn(BuiltInPrompt::CommitMessage), cx)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.ok()
|
|
||||||
};
|
|
||||||
load.await
|
|
||||||
.unwrap_or_else(|| BuiltInPrompt::CommitMessage.default_content().to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_commit_message_prompt(
|
fn build_commit_message_prompt(
|
||||||
prompt: &str,
|
prompt: &str,
|
||||||
user_agents_md: Option<&str>,
|
user_agents_md: Option<&str>,
|
||||||
|
|
@ -2803,7 +2746,7 @@ impl GitPanel {
|
||||||
.and_then(|user_agents_md| user_agents_md.content().cloned())
|
.and_then(|user_agents_md| user_agents_md.content().cloned())
|
||||||
});
|
});
|
||||||
|
|
||||||
let prompt = Self::load_commit_message_prompt(&mut cx).await;
|
let prompt = include_str!("../src/commit_message_prompt.txt");
|
||||||
|
|
||||||
let subject = this.update(cx, |this, cx| {
|
let subject = this.update(cx, |this, cx| {
|
||||||
this.commit_editor
|
this.commit_editor
|
||||||
|
|
@ -5984,7 +5927,7 @@ impl GitPanel {
|
||||||
)
|
)
|
||||||
.separator()
|
.separator()
|
||||||
.action("Open Diff", menu::Confirm.boxed_clone())
|
.action("Open Diff", menu::Confirm.boxed_clone())
|
||||||
.action("Open File", menu::SecondaryConfirm.boxed_clone())
|
.action("Open Diff (File)", menu::SecondaryConfirm.boxed_clone())
|
||||||
.when(!is_created, |context_menu| {
|
.when(!is_created, |context_menu| {
|
||||||
context_menu
|
context_menu
|
||||||
.separator()
|
.separator()
|
||||||
|
|
@ -6263,7 +6206,7 @@ impl GitPanel {
|
||||||
this.selected_entry = Some(ix);
|
this.selected_entry = Some(ix);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
if event.click_count() > 1 || event.modifiers().secondary() {
|
if event.click_count() > 1 || event.modifiers().secondary() {
|
||||||
this.open_file(&Default::default(), window, cx)
|
this.open_solo_diff(&Default::default(), window, cx)
|
||||||
} else {
|
} else {
|
||||||
this.open_diff(&Default::default(), window, cx);
|
this.open_diff(&Default::default(), window, cx);
|
||||||
this.focus_handle.focus(window, cx);
|
this.focus_handle.focus(window, cx);
|
||||||
|
|
@ -6713,7 +6656,7 @@ impl Render for GitPanel {
|
||||||
.on_action(cx.listener(Self::last_entry))
|
.on_action(cx.listener(Self::last_entry))
|
||||||
.on_action(cx.listener(Self::close_panel))
|
.on_action(cx.listener(Self::close_panel))
|
||||||
.on_action(cx.listener(Self::open_diff))
|
.on_action(cx.listener(Self::open_diff))
|
||||||
.on_action(cx.listener(Self::open_file))
|
.on_action(cx.listener(Self::open_solo_diff))
|
||||||
.on_action(cx.listener(Self::focus_changes_list))
|
.on_action(cx.listener(Self::focus_changes_list))
|
||||||
.on_action(cx.listener(Self::focus_editor))
|
.on_action(cx.listener(Self::focus_editor))
|
||||||
.on_action(cx.listener(Self::expand_commit_editor))
|
.on_action(cx.listener(Self::expand_commit_editor))
|
||||||
|
|
@ -7147,7 +7090,11 @@ impl Component for PanelRepoFooter {
|
||||||
ComponentScope::VersionControl
|
ComponentScope::VersionControl
|
||||||
}
|
}
|
||||||
|
|
||||||
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
|
fn description() -> &'static str {
|
||||||
|
"The footer shown at the bottom of the git panel."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
|
||||||
let unknown_upstream = None;
|
let unknown_upstream = None;
|
||||||
let no_remote_upstream = Some(UpstreamTracking::Gone);
|
let no_remote_upstream = Some(UpstreamTracking::Gone);
|
||||||
let ahead_of_upstream = Some(
|
let ahead_of_upstream = Some(
|
||||||
|
|
@ -7221,7 +7168,7 @@ impl Component for PanelRepoFooter {
|
||||||
}
|
}
|
||||||
|
|
||||||
let example_width = px(340.);
|
let example_width = px(340.);
|
||||||
Some(
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_6()
|
.gap_6()
|
||||||
.w_full()
|
.w_full()
|
||||||
|
|
@ -7390,8 +7337,7 @@ impl Component for PanelRepoFooter {
|
||||||
.grow()
|
.grow()
|
||||||
.vertical(),
|
.vertical(),
|
||||||
])
|
])
|
||||||
.into_any_element(),
|
.into_any_element()
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ pub mod picker_prompt;
|
||||||
pub mod project_diff;
|
pub mod project_diff;
|
||||||
pub(crate) mod remote_output;
|
pub(crate) mod remote_output;
|
||||||
pub mod repository_selector;
|
pub mod repository_selector;
|
||||||
|
pub mod solo_diff_view;
|
||||||
pub mod stash_picker;
|
pub mod stash_picker;
|
||||||
pub mod text_diff_view;
|
pub mod text_diff_view;
|
||||||
pub mod worktree_names;
|
pub mod worktree_names;
|
||||||
|
|
@ -1038,7 +1039,12 @@ impl Component for GitStatusIcon {
|
||||||
ComponentScope::VersionControl
|
ComponentScope::VersionControl
|
||||||
}
|
}
|
||||||
|
|
||||||
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
|
fn description() -> &'static str {
|
||||||
|
"An icon that visually represents the git status of a file, \
|
||||||
|
using a distinct glyph and color for modified, added, deleted, and conflicted states."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
|
||||||
fn tracked_file_status(code: StatusCode) -> FileStatus {
|
fn tracked_file_status(code: StatusCode) -> FileStatus {
|
||||||
FileStatus::Tracked(git::status::TrackedStatus {
|
FileStatus::Tracked(git::status::TrackedStatus {
|
||||||
index_status: code,
|
index_status: code,
|
||||||
|
|
@ -1055,7 +1061,6 @@ impl Component for GitStatusIcon {
|
||||||
}
|
}
|
||||||
.into();
|
.into();
|
||||||
|
|
||||||
Some(
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_6()
|
.gap_6()
|
||||||
.children(vec![example_group(vec![
|
.children(vec![example_group(vec![
|
||||||
|
|
@ -1067,8 +1072,7 @@ impl Component for GitStatusIcon {
|
||||||
GitStatusIcon::new(conflict).into_any_element(),
|
GitStatusIcon::new(conflict).into_any_element(),
|
||||||
),
|
),
|
||||||
])])
|
])])
|
||||||
.into_any_element(),
|
.into_any_element()
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
787
crates/git_ui/src/solo_diff_view.rs
Normal file
787
crates/git_ui/src/solo_diff_view.rs
Normal file
|
|
@ -0,0 +1,787 @@
|
||||||
|
use crate::{git_panel::GitStatusEntry, git_status_icon};
|
||||||
|
use anyhow::{Context as _, Result};
|
||||||
|
use buffer_diff::DiffHunkSecondaryStatus;
|
||||||
|
use editor::{
|
||||||
|
Direction, Editor, EditorEvent, EditorSettings, SplittableEditor, ToggleSplitDiff,
|
||||||
|
actions::{GoToHunk, GoToPreviousHunk},
|
||||||
|
};
|
||||||
|
use fs::Fs;
|
||||||
|
use git::{
|
||||||
|
Commit, Restore, StageAndNext, StageFile, ToggleStaged, UnstageAndNext, UnstageFile,
|
||||||
|
repository::RepoPath, status::StageStatus,
|
||||||
|
};
|
||||||
|
use gpui::{
|
||||||
|
Action, AnyElement, App, AppContext as _, Context, Entity, EventEmitter, FocusHandle,
|
||||||
|
Focusable, IntoElement, Render, Subscription, Task, WeakEntity, Window,
|
||||||
|
};
|
||||||
|
use language::{Buffer, HighlightedText};
|
||||||
|
use multi_buffer::MultiBuffer;
|
||||||
|
use project::{
|
||||||
|
Project,
|
||||||
|
git_store::{Repository, RepositoryId},
|
||||||
|
};
|
||||||
|
use settings::{DiffViewStyle, Settings, SettingsStore, update_settings_file};
|
||||||
|
use std::{
|
||||||
|
any::{Any, TypeId},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
use ui::{
|
||||||
|
Color, DiffStat, Divider, Icon, IconButton, IconButtonShape, IconName, Label, LabelCommon as _,
|
||||||
|
SharedString, Tooltip, prelude::*, vertical_divider,
|
||||||
|
};
|
||||||
|
use util::paths::{PathExt as _, PathStyle};
|
||||||
|
use workspace::{
|
||||||
|
Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
|
||||||
|
Workspace,
|
||||||
|
item::{ItemEvent, SaveOptions, TabContentParams},
|
||||||
|
notifications::NotifyTaskExt,
|
||||||
|
searchable::SearchableItemHandle,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct SoloDiffView {
|
||||||
|
repository: Entity<Repository>,
|
||||||
|
repository_id: RepositoryId,
|
||||||
|
repo_path: RepoPath,
|
||||||
|
buffer: Entity<Buffer>,
|
||||||
|
editor: Entity<SplittableEditor>,
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
|
_settings_subscription: Subscription,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SoloDiffView {
|
||||||
|
pub fn open_or_focus(
|
||||||
|
entry: GitStatusEntry,
|
||||||
|
repository: Entity<Repository>,
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Task<Result<Entity<Self>>> {
|
||||||
|
let Some(workspace_entity) = workspace.upgrade() else {
|
||||||
|
return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
|
||||||
|
};
|
||||||
|
|
||||||
|
let existing = workspace_entity
|
||||||
|
.read(cx)
|
||||||
|
.items_of_type::<SoloDiffView>(cx)
|
||||||
|
.find(|item| item.read(cx).matches(&repository, &entry.repo_path, cx));
|
||||||
|
if let Some(existing) = existing {
|
||||||
|
workspace_entity.update(cx, |workspace, cx| {
|
||||||
|
workspace.activate_item(&existing, true, true, window, cx);
|
||||||
|
});
|
||||||
|
existing.focus_handle(cx).focus(window, cx);
|
||||||
|
return Task::ready(Ok(existing));
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(project_path) = repository
|
||||||
|
.read(cx)
|
||||||
|
.repo_path_to_project_path(&entry.repo_path, cx)
|
||||||
|
else {
|
||||||
|
return Task::ready(Err(anyhow::anyhow!(
|
||||||
|
"could not resolve repository path {:?}",
|
||||||
|
entry.repo_path
|
||||||
|
)));
|
||||||
|
};
|
||||||
|
|
||||||
|
let project = workspace_entity.read(cx).project().clone();
|
||||||
|
let repo_path = entry.repo_path;
|
||||||
|
window.spawn(cx, async move |cx| {
|
||||||
|
let buffer = project
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
project.open_buffer(project_path.clone(), cx)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
let diff = project
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
project.open_uncommitted_diff(buffer.clone(), cx)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
workspace_entity.update_in(cx, |workspace, window, cx| {
|
||||||
|
let workspace_handle = cx.entity();
|
||||||
|
let view = cx.new(|cx| {
|
||||||
|
Self::new(
|
||||||
|
project,
|
||||||
|
repository,
|
||||||
|
repo_path,
|
||||||
|
buffer,
|
||||||
|
diff,
|
||||||
|
workspace_handle,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
workspace.add_item_to_active_pane(Box::new(view.clone()), None, true, window, cx);
|
||||||
|
view
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new(
|
||||||
|
project: Entity<Project>,
|
||||||
|
repository: Entity<Repository>,
|
||||||
|
repo_path: RepoPath,
|
||||||
|
buffer: Entity<Buffer>,
|
||||||
|
diff: Entity<buffer_diff::BufferDiff>,
|
||||||
|
workspace: Entity<Workspace>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Self {
|
||||||
|
let repository_id = repository.read(cx).id;
|
||||||
|
let multibuffer = cx.new(|cx| {
|
||||||
|
let mut multibuffer = MultiBuffer::singleton(buffer.clone(), cx);
|
||||||
|
multibuffer.add_diff(diff, cx);
|
||||||
|
multibuffer.set_all_diff_hunks_expanded(cx);
|
||||||
|
multibuffer
|
||||||
|
});
|
||||||
|
let editor = cx.new(|cx| {
|
||||||
|
let editor = SplittableEditor::new(
|
||||||
|
EditorSettings::get_global(cx).diff_view_style,
|
||||||
|
multibuffer,
|
||||||
|
project.clone(),
|
||||||
|
workspace.clone(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
editor.rhs_editor().update(cx, |editor, cx| {
|
||||||
|
editor.set_should_serialize(false, cx);
|
||||||
|
let snapshot = editor.snapshot(window, cx);
|
||||||
|
editor.go_to_hunk_before_or_after_position(
|
||||||
|
&snapshot,
|
||||||
|
language::Point::new(0, 0),
|
||||||
|
Direction::Next,
|
||||||
|
true,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
editor
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut previous_diff_view_style = EditorSettings::get_global(cx).diff_view_style;
|
||||||
|
let settings_subscription =
|
||||||
|
cx.observe_global_in::<SettingsStore>(window, move |this, window, cx| {
|
||||||
|
let diff_view_style = EditorSettings::get_global(cx).diff_view_style;
|
||||||
|
if diff_view_style != previous_diff_view_style {
|
||||||
|
this.editor.update(cx, |editor, cx| {
|
||||||
|
if editor.diff_view_style() != diff_view_style {
|
||||||
|
editor.toggle_split(&ToggleSplitDiff, window, cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
previous_diff_view_style = diff_view_style;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
repository,
|
||||||
|
repository_id,
|
||||||
|
repo_path,
|
||||||
|
buffer,
|
||||||
|
editor,
|
||||||
|
workspace: workspace.downgrade(),
|
||||||
|
_settings_subscription: settings_subscription,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn matches(&self, repository: &Entity<Repository>, repo_path: &RepoPath, cx: &App) -> bool {
|
||||||
|
self.repository_id == repository.read(cx).id && &self.repo_path == repo_path
|
||||||
|
}
|
||||||
|
|
||||||
|
fn button_states(&self, cx: &App) -> SoloDiffButtonStates {
|
||||||
|
let editor = self.editor.read(cx).rhs_editor().read(cx);
|
||||||
|
let multibuffer = editor.buffer().read(cx);
|
||||||
|
let snapshot = multibuffer.snapshot(cx);
|
||||||
|
let prev_next = snapshot.diff_hunks().nth(1).is_some();
|
||||||
|
let mut selection = true;
|
||||||
|
|
||||||
|
let mut ranges = editor
|
||||||
|
.selections
|
||||||
|
.disjoint_anchor_ranges()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if !ranges.iter().any(|range| range.start != range.end) {
|
||||||
|
selection = false;
|
||||||
|
let anchor = editor.selections.newest_anchor().head();
|
||||||
|
if let Some((_, excerpt_range)) = snapshot.excerpt_containing(anchor..anchor)
|
||||||
|
&& let Some(range) = snapshot
|
||||||
|
.anchor_in_buffer(excerpt_range.context.start)
|
||||||
|
.zip(snapshot.anchor_in_buffer(excerpt_range.context.end))
|
||||||
|
.map(|(start, end)| start..end)
|
||||||
|
{
|
||||||
|
ranges = vec![range];
|
||||||
|
} else {
|
||||||
|
ranges = Vec::new();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut stage = false;
|
||||||
|
let mut unstage = false;
|
||||||
|
for hunk in editor.diff_hunks_in_ranges(&ranges, &snapshot) {
|
||||||
|
match hunk.status.secondary {
|
||||||
|
DiffHunkSecondaryStatus::HasSecondaryHunk
|
||||||
|
| DiffHunkSecondaryStatus::SecondaryHunkAdditionPending => {
|
||||||
|
stage = true;
|
||||||
|
}
|
||||||
|
DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk => {
|
||||||
|
stage = true;
|
||||||
|
unstage = true;
|
||||||
|
}
|
||||||
|
DiffHunkSecondaryStatus::NoSecondaryHunk
|
||||||
|
| DiffHunkSecondaryStatus::SecondaryHunkRemovalPending => {
|
||||||
|
unstage = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let stage_status = self
|
||||||
|
.repository
|
||||||
|
.read(cx)
|
||||||
|
.status_for_path(&self.repo_path)
|
||||||
|
.map(|entry| entry.status.staging())
|
||||||
|
.unwrap_or(StageStatus::Unstaged);
|
||||||
|
|
||||||
|
SoloDiffButtonStates {
|
||||||
|
stage,
|
||||||
|
unstage,
|
||||||
|
restore: stage || unstage,
|
||||||
|
prev_next,
|
||||||
|
selection,
|
||||||
|
stage_file: stage_status.has_unstaged(),
|
||||||
|
unstage_file: stage_status.has_staged(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dispatch_action(&self, action: &dyn Action, window: &mut Window, cx: &mut App) {
|
||||||
|
self.focus_handle(cx).focus(window, cx);
|
||||||
|
let action = action.boxed_clone();
|
||||||
|
cx.defer(move |cx| {
|
||||||
|
cx.dispatch_action(action.as_ref());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn change_file_stage(&self, stage: bool, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let repository = self.repository.clone();
|
||||||
|
let repo_path = self.repo_path.clone();
|
||||||
|
let workspace = self.workspace.clone();
|
||||||
|
let task = cx.spawn(async move |_, cx| {
|
||||||
|
repository
|
||||||
|
.update(cx, |repository, cx| {
|
||||||
|
if stage {
|
||||||
|
repository.stage_entries(vec![repo_path], cx)
|
||||||
|
} else {
|
||||||
|
repository.unstage_entries(vec![repo_path], cx)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.with_context(|| {
|
||||||
|
if stage {
|
||||||
|
"failed to stage file"
|
||||||
|
} else {
|
||||||
|
"failed to unstage file"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
task.detach_and_notify_err(workspace, window, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<EditorEvent> for SoloDiffView {}
|
||||||
|
|
||||||
|
impl Focusable for SoloDiffView {
|
||||||
|
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||||
|
self.editor.focus_handle(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Item for SoloDiffView {
|
||||||
|
type Event = EditorEvent;
|
||||||
|
|
||||||
|
fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
|
||||||
|
Some(Icon::new(IconName::Diff).color(Color::Muted))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
|
||||||
|
Label::new(self.tab_content_text(params.detail.unwrap_or_default(), cx))
|
||||||
|
.color(if params.selected {
|
||||||
|
Color::Default
|
||||||
|
} else {
|
||||||
|
Color::Muted
|
||||||
|
})
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString {
|
||||||
|
self.buffer
|
||||||
|
.read(cx)
|
||||||
|
.file()
|
||||||
|
.and_then(|file| {
|
||||||
|
Some(
|
||||||
|
file.full_path(cx)
|
||||||
|
.file_name()?
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
self.repo_path
|
||||||
|
.as_ref()
|
||||||
|
.display(PathStyle::local())
|
||||||
|
.into_owned()
|
||||||
|
})
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tab_tooltip_text(&self, cx: &App) -> Option<SharedString> {
|
||||||
|
Some(
|
||||||
|
self.buffer
|
||||||
|
.read(cx)
|
||||||
|
.file()
|
||||||
|
.map(|file| file.full_path(cx).compact().to_string_lossy().into_owned())
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
self.repo_path
|
||||||
|
.as_ref()
|
||||||
|
.display(PathStyle::local())
|
||||||
|
.into_owned()
|
||||||
|
})
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_item_events(event: &EditorEvent, f: &mut dyn FnMut(ItemEvent)) {
|
||||||
|
Editor::to_item_events(event, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn telemetry_event_text(&self) -> Option<&'static str> {
|
||||||
|
Some("Solo Diff View Opened")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.editor.deactivated(window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn act_as_type<'a>(
|
||||||
|
&'a self,
|
||||||
|
type_id: TypeId,
|
||||||
|
self_handle: &'a Entity<Self>,
|
||||||
|
cx: &'a App,
|
||||||
|
) -> Option<gpui::AnyEntity> {
|
||||||
|
if type_id == TypeId::of::<Self>() {
|
||||||
|
Some(self_handle.clone().into())
|
||||||
|
} else {
|
||||||
|
self.editor.act_as_type(type_id, cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_searchable(&self, _: &Entity<Self>, _: &App) -> Option<Box<dyn SearchableItemHandle>> {
|
||||||
|
Some(Box::new(self.editor.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn for_each_project_item(
|
||||||
|
&self,
|
||||||
|
cx: &App,
|
||||||
|
f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
|
||||||
|
) {
|
||||||
|
self.editor.for_each_project_item(cx, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_nav_history(
|
||||||
|
&mut self,
|
||||||
|
nav_history: ItemNavHistory,
|
||||||
|
_: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.editor.update(cx, |editor, cx| {
|
||||||
|
editor.rhs_editor().update(cx, |editor, _| {
|
||||||
|
editor.set_nav_history(Some(nav_history));
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn navigate(
|
||||||
|
&mut self,
|
||||||
|
data: Arc<dyn Any + Send>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> bool {
|
||||||
|
self.editor.update(cx, |editor, cx| {
|
||||||
|
editor
|
||||||
|
.rhs_editor()
|
||||||
|
.update(cx, |editor, cx| editor.navigate(data, window, cx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
|
||||||
|
ToolbarItemLocation::PrimaryLeft
|
||||||
|
}
|
||||||
|
|
||||||
|
fn breadcrumbs(&self, cx: &App) -> Option<(Vec<HighlightedText>, Option<gpui::Font>)> {
|
||||||
|
self.editor.breadcrumbs(cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn added_to_workspace(
|
||||||
|
&mut self,
|
||||||
|
workspace: &mut Workspace,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.editor.update(cx, |editor, cx| {
|
||||||
|
editor.rhs_editor().update(cx, |editor, cx| {
|
||||||
|
editor.added_to_workspace(workspace, window, cx)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn can_save(&self, cx: &App) -> bool {
|
||||||
|
self.editor.read(cx).rhs_editor().read(cx).can_save(cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save(
|
||||||
|
&mut self,
|
||||||
|
options: SaveOptions,
|
||||||
|
project: Entity<Project>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Task<Result<()>> {
|
||||||
|
self.editor.save(options, project, window, cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for SoloDiffView {
|
||||||
|
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
self.editor.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SoloDiffStyleToolbar {
|
||||||
|
solo_diff: Option<WeakEntity<SoloDiffView>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SoloDiffGitToolbar {
|
||||||
|
solo_diff: Option<WeakEntity<SoloDiffView>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SoloDiffStyleToolbar {
|
||||||
|
pub fn new(_: &mut Context<Self>) -> Self {
|
||||||
|
Self { solo_diff: None }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn solo_diff(&self) -> Option<Entity<SoloDiffView>> {
|
||||||
|
self.solo_diff.as_ref()?.upgrade()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_diff_view_style(
|
||||||
|
&mut self,
|
||||||
|
diff_view_style: DiffViewStyle,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let Some(solo_diff) = self.solo_diff() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let workspace = solo_diff.read(cx).workspace.clone();
|
||||||
|
|
||||||
|
update_settings_file(<dyn Fs>::global(cx), cx, move |settings, _| {
|
||||||
|
settings.editor.diff_view_style = Some(diff_view_style);
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(workspace) = workspace.upgrade() {
|
||||||
|
let splittable_editors = {
|
||||||
|
workspace
|
||||||
|
.read(cx)
|
||||||
|
.items(cx)
|
||||||
|
.filter_map(|item| item.act_as_type(TypeId::of::<SplittableEditor>(), cx))
|
||||||
|
.filter_map(|item| item.downcast::<SplittableEditor>().ok())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
};
|
||||||
|
|
||||||
|
for editor in splittable_editors {
|
||||||
|
editor.update(cx, |editor, cx| {
|
||||||
|
if editor.diff_view_style() != diff_view_style {
|
||||||
|
editor.toggle_split(&ToggleSplitDiff, window, cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<ToolbarItemEvent> for SoloDiffStyleToolbar {}
|
||||||
|
|
||||||
|
impl ToolbarItemView for SoloDiffStyleToolbar {
|
||||||
|
fn set_active_pane_item(
|
||||||
|
&mut self,
|
||||||
|
active_pane_item: Option<&dyn ItemHandle>,
|
||||||
|
_: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> ToolbarItemLocation {
|
||||||
|
self.solo_diff = active_pane_item
|
||||||
|
.and_then(|item| item.act_as::<SoloDiffView>(cx))
|
||||||
|
.map(|entity| entity.downgrade());
|
||||||
|
if self.solo_diff.is_some() {
|
||||||
|
ToolbarItemLocation::PrimaryLeft
|
||||||
|
} else {
|
||||||
|
ToolbarItemLocation::Hidden
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for SoloDiffStyleToolbar {
|
||||||
|
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let Some(solo_diff) = self.solo_diff() else {
|
||||||
|
return div();
|
||||||
|
};
|
||||||
|
let editor_entity = solo_diff.read(cx).editor.clone();
|
||||||
|
let editor = editor_entity.read(cx);
|
||||||
|
let diff_view_style = editor.diff_view_style();
|
||||||
|
let is_split_set = diff_view_style == DiffViewStyle::Split;
|
||||||
|
let split_icon = if is_split_set && !editor.is_split() {
|
||||||
|
IconName::DiffSplitAuto
|
||||||
|
} else {
|
||||||
|
IconName::DiffSplit
|
||||||
|
};
|
||||||
|
|
||||||
|
h_flex()
|
||||||
|
.h_8()
|
||||||
|
.items_center()
|
||||||
|
.gap_1()
|
||||||
|
.child(
|
||||||
|
IconButton::new("solo-diff-unified", IconName::DiffUnified)
|
||||||
|
.icon_size(IconSize::Small)
|
||||||
|
.toggle_state(diff_view_style == DiffViewStyle::Unified)
|
||||||
|
.tooltip(Tooltip::text("Unified"))
|
||||||
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
|
this.set_diff_view_style(DiffViewStyle::Unified, window, cx);
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
IconButton::new("solo-diff-split", split_icon)
|
||||||
|
.icon_size(IconSize::Small)
|
||||||
|
.toggle_state(diff_view_style == DiffViewStyle::Split)
|
||||||
|
.tooltip(Tooltip::text("Split"))
|
||||||
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
|
this.set_diff_view_style(DiffViewStyle::Split, window, cx);
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.child(vertical_divider())
|
||||||
|
.child(div().w_1())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SoloDiffGitToolbar {
|
||||||
|
pub fn new(_: &mut Context<Self>) -> Self {
|
||||||
|
Self { solo_diff: None }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn solo_diff(&self) -> Option<Entity<SoloDiffView>> {
|
||||||
|
self.solo_diff.as_ref()?.upgrade()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dispatch_action(&self, action: &dyn Action, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if let Some(solo_diff) = self.solo_diff() {
|
||||||
|
solo_diff.update(cx, |solo_diff, cx| {
|
||||||
|
solo_diff.dispatch_action(action, window, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stage_file(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if let Some(solo_diff) = self.solo_diff() {
|
||||||
|
solo_diff.update(cx, |solo_diff, cx| {
|
||||||
|
solo_diff.change_file_stage(true, window, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unstage_file(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if let Some(solo_diff) = self.solo_diff() {
|
||||||
|
solo_diff.update(cx, |solo_diff, cx| {
|
||||||
|
solo_diff.change_file_stage(false, window, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<ToolbarItemEvent> for SoloDiffGitToolbar {}
|
||||||
|
|
||||||
|
impl ToolbarItemView for SoloDiffGitToolbar {
|
||||||
|
fn set_active_pane_item(
|
||||||
|
&mut self,
|
||||||
|
active_pane_item: Option<&dyn ItemHandle>,
|
||||||
|
_: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> ToolbarItemLocation {
|
||||||
|
self.solo_diff = active_pane_item
|
||||||
|
.and_then(|item| item.act_as::<SoloDiffView>(cx))
|
||||||
|
.map(|entity| entity.downgrade());
|
||||||
|
if self.solo_diff.is_some() {
|
||||||
|
ToolbarItemLocation::PrimaryRight
|
||||||
|
} else {
|
||||||
|
ToolbarItemLocation::Hidden
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SoloDiffButtonStates {
|
||||||
|
stage: bool,
|
||||||
|
unstage: bool,
|
||||||
|
restore: bool,
|
||||||
|
prev_next: bool,
|
||||||
|
selection: bool,
|
||||||
|
stage_file: bool,
|
||||||
|
unstage_file: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for SoloDiffGitToolbar {
|
||||||
|
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let Some(solo_diff) = self.solo_diff() else {
|
||||||
|
return div();
|
||||||
|
};
|
||||||
|
let focus_handle = solo_diff.focus_handle(cx);
|
||||||
|
let solo_diff = solo_diff.read(cx);
|
||||||
|
let button_states = solo_diff.button_states(cx);
|
||||||
|
let status_entry = solo_diff
|
||||||
|
.repository
|
||||||
|
.read(cx)
|
||||||
|
.status_for_path(&solo_diff.repo_path);
|
||||||
|
let status = status_entry.as_ref().map(|entry| entry.status);
|
||||||
|
let diff_stat = status_entry.and_then(|entry| entry.diff_stat);
|
||||||
|
|
||||||
|
h_group_xl()
|
||||||
|
.my_neg_1()
|
||||||
|
.py_1()
|
||||||
|
.items_center()
|
||||||
|
.flex_wrap()
|
||||||
|
.justify_between()
|
||||||
|
.children(status.map(|status| git_status_icon(status).into_any_element()))
|
||||||
|
.children(diff_stat.map(|stat| {
|
||||||
|
DiffStat::new("solo-diff-stat", stat.added as usize, stat.deleted as usize)
|
||||||
|
.into_any_element()
|
||||||
|
}))
|
||||||
|
.child(
|
||||||
|
h_group_sm()
|
||||||
|
.when(button_states.selection, |el| {
|
||||||
|
el.child(
|
||||||
|
Button::new("stage", "Toggle Staged")
|
||||||
|
.tooltip(Tooltip::for_action_title_in(
|
||||||
|
"Toggle Staged",
|
||||||
|
&ToggleStaged,
|
||||||
|
&focus_handle,
|
||||||
|
))
|
||||||
|
.disabled(!button_states.stage && !button_states.unstage)
|
||||||
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
|
this.dispatch_action(&ToggleStaged, window, cx)
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.when(!button_states.selection, |el| {
|
||||||
|
el.child(
|
||||||
|
Button::new("stage", "Stage")
|
||||||
|
.tooltip(Tooltip::for_action_title_in(
|
||||||
|
"Stage and go to next hunk",
|
||||||
|
&StageAndNext,
|
||||||
|
&focus_handle,
|
||||||
|
))
|
||||||
|
.disabled(!button_states.stage)
|
||||||
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
|
this.dispatch_action(&StageAndNext, window, cx)
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new("unstage", "Unstage")
|
||||||
|
.tooltip(Tooltip::for_action_title_in(
|
||||||
|
"Unstage and go to next hunk",
|
||||||
|
&UnstageAndNext,
|
||||||
|
&focus_handle,
|
||||||
|
))
|
||||||
|
.disabled(!button_states.unstage)
|
||||||
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
|
this.dispatch_action(&UnstageAndNext, window, cx)
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
Button::new("restore", "Restore")
|
||||||
|
.tooltip(Tooltip::for_action_title_in(
|
||||||
|
"Restore selected hunk",
|
||||||
|
&Restore,
|
||||||
|
&focus_handle,
|
||||||
|
))
|
||||||
|
.disabled(!button_states.restore)
|
||||||
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
|
this.dispatch_action(&Restore, window, cx)
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_group_sm()
|
||||||
|
.child(
|
||||||
|
IconButton::new("up", IconName::ArrowUp)
|
||||||
|
.shape(IconButtonShape::Square)
|
||||||
|
.tooltip(Tooltip::for_action_title_in(
|
||||||
|
"Go to previous hunk",
|
||||||
|
&GoToPreviousHunk,
|
||||||
|
&focus_handle,
|
||||||
|
))
|
||||||
|
.disabled(!button_states.prev_next)
|
||||||
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
|
this.dispatch_action(&GoToPreviousHunk, window, cx)
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
IconButton::new("down", IconName::ArrowDown)
|
||||||
|
.shape(IconButtonShape::Square)
|
||||||
|
.tooltip(Tooltip::for_action_title_in(
|
||||||
|
"Go to next hunk",
|
||||||
|
&GoToHunk,
|
||||||
|
&focus_handle,
|
||||||
|
))
|
||||||
|
.disabled(!button_states.prev_next)
|
||||||
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
|
this.dispatch_action(&GoToHunk, window, cx)
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(vertical_divider())
|
||||||
|
.child(
|
||||||
|
h_group_sm()
|
||||||
|
.child(
|
||||||
|
Button::new("stage-file", "Stage File")
|
||||||
|
.tooltip(Tooltip::for_action_title_in(
|
||||||
|
"Stage file",
|
||||||
|
&StageFile,
|
||||||
|
&focus_handle,
|
||||||
|
))
|
||||||
|
.disabled(!button_states.stage_file)
|
||||||
|
.on_click(
|
||||||
|
cx.listener(|this, _, window, cx| this.stage_file(window, cx)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new("unstage-file", "Unstage File")
|
||||||
|
.tooltip(Tooltip::for_action_title_in(
|
||||||
|
"Unstage file",
|
||||||
|
&UnstageFile,
|
||||||
|
&focus_handle,
|
||||||
|
))
|
||||||
|
.disabled(!button_states.unstage_file)
|
||||||
|
.on_click(
|
||||||
|
cx.listener(|this, _, window, cx| this.unstage_file(window, cx)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(Divider::vertical())
|
||||||
|
.child(
|
||||||
|
Button::new("commit", "Commit")
|
||||||
|
.tooltip(Tooltip::for_action_title_in(
|
||||||
|
"Commit",
|
||||||
|
&Commit,
|
||||||
|
&focus_handle,
|
||||||
|
))
|
||||||
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
|
this.dispatch_action(&Commit, window, cx);
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,8 +14,8 @@ use picker::{Picker, PickerDelegate, PickerEditorPosition};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use project::git_store::RepositoryEvent;
|
use project::git_store::RepositoryEvent;
|
||||||
use ui::{
|
use ui::{
|
||||||
Button, Divider, HighlightedLabel, IconButton, KeyBinding, ListItem, ListItemSpacing, Tooltip,
|
Button, CommonAnimationExt as _, Divider, HighlightedLabel, IconButton, KeyBinding, ListItem,
|
||||||
prelude::*,
|
ListItemSpacing, Tooltip, prelude::*,
|
||||||
};
|
};
|
||||||
use util::ResultExt as _;
|
use util::ResultExt as _;
|
||||||
use util::paths::PathExt;
|
use util::paths::PathExt;
|
||||||
|
|
@ -116,6 +116,7 @@ impl WorktreePicker {
|
||||||
show_footer,
|
show_footer,
|
||||||
modifiers: Modifiers::default(),
|
modifiers: Modifiers::default(),
|
||||||
hovered_delete_index: None,
|
hovered_delete_index: None,
|
||||||
|
deleting_worktree_paths: HashSet::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let picker = cx.new(|cx| {
|
let picker = cx.new(|cx| {
|
||||||
|
|
@ -313,6 +314,7 @@ struct WorktreePickerDelegate {
|
||||||
show_footer: bool,
|
show_footer: bool,
|
||||||
modifiers: Modifiers,
|
modifiers: Modifiers,
|
||||||
hovered_delete_index: Option<usize>,
|
hovered_delete_index: Option<usize>,
|
||||||
|
deleting_worktree_paths: HashSet<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_worktree_command(path: &Path, force: bool) -> String {
|
fn remove_worktree_command(path: &Path, force: bool) -> String {
|
||||||
|
|
@ -420,8 +422,9 @@ impl WorktreePickerDelegate {
|
||||||
fn build_fixed_entries(&self) -> Vec<WorktreeEntry> {
|
fn build_fixed_entries(&self) -> Vec<WorktreeEntry> {
|
||||||
let mut entries = Vec::new();
|
let mut entries = Vec::new();
|
||||||
|
|
||||||
if !self.has_multiple_repositories {
|
if self.has_multiple_repositories {
|
||||||
if let Some(ref default_branch) = self.default_branch {
|
entries.push(WorktreeEntry::CreateFromCurrentBranch);
|
||||||
|
} else if let Some(ref default_branch) = self.default_branch {
|
||||||
let is_different = self
|
let is_different = self
|
||||||
.current_branch_name
|
.current_branch_name
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|
@ -432,7 +435,6 @@ impl WorktreePickerDelegate {
|
||||||
if is_different {
|
if is_different {
|
||||||
entries.push(WorktreeEntry::CreateFromCurrentBranch);
|
entries.push(WorktreeEntry::CreateFromCurrentBranch);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
entries.push(WorktreeEntry::CreateFromCurrentBranch);
|
entries.push(WorktreeEntry::CreateFromCurrentBranch);
|
||||||
}
|
}
|
||||||
|
|
@ -464,7 +466,7 @@ impl WorktreePickerDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn delete_worktree(
|
fn delete_worktree(
|
||||||
&self,
|
&mut self,
|
||||||
ix: usize,
|
ix: usize,
|
||||||
force: bool,
|
force: bool,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
|
|
@ -476,7 +478,9 @@ impl WorktreePickerDelegate {
|
||||||
let WorktreeEntry::Worktree { worktree, .. } = entry else {
|
let WorktreeEntry::Worktree { worktree, .. } = entry else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if !self.can_delete_worktree(worktree) {
|
if !self.can_delete_worktree(worktree)
|
||||||
|
|| self.deleting_worktree_paths.contains(&worktree.path)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -493,10 +497,27 @@ impl WorktreePickerDelegate {
|
||||||
);
|
);
|
||||||
let workspace = self.workspace.clone();
|
let workspace = self.workspace.clone();
|
||||||
|
|
||||||
|
self.deleting_worktree_paths.insert(path.clone());
|
||||||
|
if self.hovered_delete_index == Some(ix) {
|
||||||
|
self.hovered_delete_index = None;
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
|
||||||
cx.spawn_in(window, async move |picker, cx| {
|
cx.spawn_in(window, async move |picker, cx| {
|
||||||
let initial_result = repo
|
let initial_result = match repo
|
||||||
.update(cx, |repo, _| repo.remove_worktree(path.clone(), force))
|
.update(cx, |repo, _| repo.remove_worktree(path.clone(), force))
|
||||||
.await?;
|
.await
|
||||||
|
{
|
||||||
|
Ok(result) => result,
|
||||||
|
Err(error) => {
|
||||||
|
picker.update_in(cx, |picker, _window, cx| {
|
||||||
|
if picker.delegate.deleting_worktree_paths.remove(&path) {
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
return Err(error.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let (result, attempted_force) = match initial_result {
|
let (result, attempted_force) = match initial_result {
|
||||||
Ok(()) => (Ok(()), force),
|
Ok(()) => (Ok(()), force),
|
||||||
|
|
@ -510,6 +531,12 @@ impl WorktreePickerDelegate {
|
||||||
.flatten();
|
.flatten();
|
||||||
|
|
||||||
if let Some(prompt_message) = force_delete_prompt {
|
if let Some(prompt_message) = force_delete_prompt {
|
||||||
|
picker.update_in(cx, |picker, _window, cx| {
|
||||||
|
if picker.delegate.deleting_worktree_paths.remove(&path) {
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
let answer = cx.update(|window, cx| {
|
let answer = cx.update(|window, cx| {
|
||||||
window.prompt(
|
window.prompt(
|
||||||
PromptLevel::Warning,
|
PromptLevel::Warning,
|
||||||
|
|
@ -524,9 +551,39 @@ impl WorktreePickerDelegate {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let retry = repo
|
let should_retry = picker.update_in(cx, |picker, _window, cx| {
|
||||||
|
let worktree_still_exists = picker
|
||||||
|
.delegate
|
||||||
|
.all_worktrees
|
||||||
|
.iter()
|
||||||
|
.any(|worktree| worktree.path == path);
|
||||||
|
if !worktree_still_exists
|
||||||
|
|| !picker.delegate.deleting_worktree_paths.insert(path.clone())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
true
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if !should_retry {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let retry = match repo
|
||||||
.update(cx, |repo, _| repo.remove_worktree(path.clone(), true))
|
.update(cx, |repo, _| repo.remove_worktree(path.clone(), true))
|
||||||
.await?;
|
.await
|
||||||
|
{
|
||||||
|
Ok(result) => result,
|
||||||
|
Err(error) => {
|
||||||
|
picker.update_in(cx, |picker, _window, cx| {
|
||||||
|
if picker.delegate.deleting_worktree_paths.remove(&path) {
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
return Err(error.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if let Err(error) = &retry {
|
if let Err(error) = &retry {
|
||||||
log::error!("Failed to force remove worktree: {error}");
|
log::error!("Failed to force remove worktree: {error}");
|
||||||
|
|
@ -540,6 +597,12 @@ impl WorktreePickerDelegate {
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(error) = result {
|
if let Err(error) = result {
|
||||||
|
picker.update_in(cx, |picker, _window, cx| {
|
||||||
|
if picker.delegate.deleting_worktree_paths.remove(&path) {
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
if let Some(workspace) = workspace.upgrade() {
|
if let Some(workspace) = workspace.upgrade() {
|
||||||
cx.update(|_window, cx| {
|
cx.update(|_window, cx| {
|
||||||
show_error_toast(
|
show_error_toast(
|
||||||
|
|
@ -555,6 +618,7 @@ impl WorktreePickerDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
picker.update_in(cx, |picker, _window, cx| {
|
picker.update_in(cx, |picker, _window, cx| {
|
||||||
|
picker.delegate.deleting_worktree_paths.remove(&path);
|
||||||
picker.delegate.matches.retain(|e| {
|
picker.delegate.matches.retain(|e| {
|
||||||
!matches!(e, WorktreeEntry::Worktree { worktree, .. } if worktree.path == path)
|
!matches!(e, WorktreeEntry::Worktree { worktree, .. } if worktree.path == path)
|
||||||
});
|
});
|
||||||
|
|
@ -814,6 +878,10 @@ impl PickerDelegate for WorktreePickerDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WorktreeEntry::Worktree { worktree, .. } => {
|
WorktreeEntry::Worktree { worktree, .. } => {
|
||||||
|
if self.deleting_worktree_paths.contains(&worktree.path) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let is_current = self.project_worktree_paths.contains(&worktree.path);
|
let is_current = self.project_worktree_paths.contains(&worktree.path);
|
||||||
|
|
||||||
if !is_current {
|
if !is_current {
|
||||||
|
|
@ -956,6 +1024,7 @@ impl PickerDelegate for WorktreePickerDelegate {
|
||||||
let sha = worktree.sha.chars().take(7).collect::<String>();
|
let sha = worktree.sha.chars().take(7).collect::<String>();
|
||||||
|
|
||||||
let is_current = self.project_worktree_paths.contains(&worktree.path);
|
let is_current = self.project_worktree_paths.contains(&worktree.path);
|
||||||
|
let is_deleting = self.deleting_worktree_paths.contains(&worktree.path);
|
||||||
let can_delete = self.can_delete_worktree(worktree);
|
let can_delete = self.can_delete_worktree(worktree);
|
||||||
|
|
||||||
let entry_icon = if is_current {
|
let entry_icon = if is_current {
|
||||||
|
|
@ -1035,7 +1104,24 @@ impl PickerDelegate for WorktreePickerDelegate {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.when(!is_current, |this| {
|
.when(is_deleting, |this| {
|
||||||
|
this.end_slot(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.child(
|
||||||
|
Icon::new(IconName::LoadCircle)
|
||||||
|
.size(IconSize::Small)
|
||||||
|
.color(Color::Muted)
|
||||||
|
.with_rotate_animation(2),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Label::new("Deleting…")
|
||||||
|
.size(LabelSize::Small)
|
||||||
|
.color(Color::Muted),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.when(!is_deleting && !is_current, |this| {
|
||||||
let open_in_new_window_button =
|
let open_in_new_window_button =
|
||||||
IconButton::new(("open-new-window", ix), IconName::ArrowUpRight)
|
IconButton::new(("open-new-window", ix), IconName::ArrowUpRight)
|
||||||
.icon_size(IconSize::Small)
|
.icon_size(IconSize::Small)
|
||||||
|
|
@ -1045,6 +1131,13 @@ impl PickerDelegate for WorktreePickerDelegate {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if let WorktreeEntry::Worktree { worktree, .. } = entry {
|
if let WorktreeEntry::Worktree { worktree, .. } = entry {
|
||||||
|
if picker
|
||||||
|
.delegate
|
||||||
|
.deleting_worktree_paths
|
||||||
|
.contains(&worktree.path)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
window.dispatch_action(
|
window.dispatch_action(
|
||||||
Box::new(OpenWorktreeInNewWindow {
|
Box::new(OpenWorktreeInNewWindow {
|
||||||
path: worktree.path.clone(),
|
path: worktree.path.clone(),
|
||||||
|
|
@ -1083,12 +1176,8 @@ impl PickerDelegate for WorktreePickerDelegate {
|
||||||
.into()
|
.into()
|
||||||
})
|
})
|
||||||
.on_click(cx.listener(move |picker, _, window, cx| {
|
.on_click(cx.listener(move |picker, _, window, cx| {
|
||||||
picker.delegate.delete_worktree(
|
let force = picker.delegate.modifiers.alt;
|
||||||
ix,
|
picker.delegate.delete_worktree(ix, force, window, cx);
|
||||||
picker.delegate.modifiers.alt,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -1162,6 +1251,10 @@ impl PickerDelegate for WorktreePickerDelegate {
|
||||||
matches!(e, WorktreeEntry::Worktree { worktree, .. } if self.project_worktree_paths.contains(&worktree.path))
|
matches!(e, WorktreeEntry::Worktree { worktree, .. } if self.project_worktree_paths.contains(&worktree.path))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let is_deleting = selected_entry.is_some_and(|e| {
|
||||||
|
matches!(e, WorktreeEntry::Worktree { worktree, .. } if self.deleting_worktree_paths.contains(&worktree.path))
|
||||||
|
});
|
||||||
|
|
||||||
let footer = h_flex()
|
let footer = h_flex()
|
||||||
.w_full()
|
.w_full()
|
||||||
.p_1p5()
|
.p_1p5()
|
||||||
|
|
@ -1188,7 +1281,14 @@ impl PickerDelegate for WorktreePickerDelegate {
|
||||||
} else if is_existing_worktree {
|
} else if is_existing_worktree {
|
||||||
Some(
|
Some(
|
||||||
footer
|
footer
|
||||||
.when(can_delete, |this| {
|
.when(is_deleting, |this| {
|
||||||
|
this.child(
|
||||||
|
Button::new("delete-worktree", "Deleting…")
|
||||||
|
.loading(true)
|
||||||
|
.disabled(true),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.when(!is_deleting && can_delete, |this| {
|
||||||
let focus_handle = focus_handle.clone();
|
let focus_handle = focus_handle.clone();
|
||||||
this.child(
|
this.child(
|
||||||
Button::new("delete-worktree", "Delete")
|
Button::new("delete-worktree", "Delete")
|
||||||
|
|
@ -1201,7 +1301,7 @@ impl PickerDelegate for WorktreePickerDelegate {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.when(!is_current, |this| {
|
.when(!is_deleting && !is_current, |this| {
|
||||||
let focus_handle = focus_handle.clone();
|
let focus_handle = focus_handle.clone();
|
||||||
this.child(
|
this.child(
|
||||||
Button::new("open-in-new-window", "Open in New Window")
|
Button::new("open-in-new-window", "Open in New Window")
|
||||||
|
|
@ -1218,7 +1318,8 @@ impl PickerDelegate for WorktreePickerDelegate {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.child(
|
.when(!is_deleting, |this| {
|
||||||
|
this.child(
|
||||||
Button::new("open-worktree", "Open")
|
Button::new("open-worktree", "Open")
|
||||||
.key_binding(
|
.key_binding(
|
||||||
KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx)
|
KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx)
|
||||||
|
|
@ -1228,6 +1329,7 @@ impl PickerDelegate for WorktreePickerDelegate {
|
||||||
window.dispatch_action(menu::Confirm.boxed_clone(), cx)
|
window.dispatch_action(menu::Confirm.boxed_clone(), cx)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
})
|
||||||
.into_any(),
|
.into_any(),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1482,6 +1584,33 @@ mod tests {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn picker_contains_worktree(
|
||||||
|
worktree_picker: &Entity<WorktreePicker>,
|
||||||
|
worktree_path: &Path,
|
||||||
|
cx: &mut VisualTestContext,
|
||||||
|
) -> bool {
|
||||||
|
worktree_picker.update(cx, |worktree_picker, cx| {
|
||||||
|
worktree_picker.picker.update(cx, |picker, _| {
|
||||||
|
picker.delegate.all_worktrees.iter().any(|worktree| {
|
||||||
|
worktree.path == *worktree_path
|
||||||
|
}) && picker.delegate.matches.iter().any(|entry| {
|
||||||
|
matches!(entry, WorktreeEntry::Worktree { worktree, .. } if worktree.path == *worktree_path)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deleting_worktree_paths(
|
||||||
|
worktree_picker: &Entity<WorktreePicker>,
|
||||||
|
cx: &mut VisualTestContext,
|
||||||
|
) -> HashSet<PathBuf> {
|
||||||
|
worktree_picker.update(cx, |worktree_picker, cx| {
|
||||||
|
worktree_picker.picker.update(cx, |picker, _| {
|
||||||
|
picker.delegate.deleting_worktree_paths.clone()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async fn repo_contains_worktree(
|
async fn repo_contains_worktree(
|
||||||
repository: &Entity<project::git_store::Repository>,
|
repository: &Entity<project::git_store::Repository>,
|
||||||
worktree_path: &Path,
|
worktree_path: &Path,
|
||||||
|
|
@ -1497,6 +1626,54 @@ mod tests {
|
||||||
.any(|worktree| worktree.path == *worktree_path)
|
.any(|worktree| worktree.path == *worktree_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_delete_worktree_marks_row_pending_immediately(cx: &mut TestAppContext) {
|
||||||
|
let (_, worktree_picker, _repository, worktree_path, mut cx) =
|
||||||
|
init_worktree_picker_test(cx).await;
|
||||||
|
|
||||||
|
let index = worktree_index(&worktree_picker, &worktree_path, &mut cx);
|
||||||
|
worktree_picker.update_in(&mut cx, |worktree_picker, window, cx| {
|
||||||
|
worktree_picker.picker.update(cx, |picker, cx| {
|
||||||
|
picker.delegate.delete_worktree(index, false, window, cx);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let pending_paths = deleting_worktree_paths(&worktree_picker, &mut cx);
|
||||||
|
assert_eq!(pending_paths.len(), 1);
|
||||||
|
assert!(pending_paths.contains(&worktree_path));
|
||||||
|
|
||||||
|
cx.run_until_parked();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_delete_worktree_clears_pending_and_removes_row_on_success(
|
||||||
|
cx: &mut TestAppContext,
|
||||||
|
) {
|
||||||
|
let (_, worktree_picker, repository, worktree_path, mut cx) =
|
||||||
|
init_worktree_picker_test(cx).await;
|
||||||
|
|
||||||
|
let index = worktree_index(&worktree_picker, &worktree_path, &mut cx);
|
||||||
|
worktree_picker.update_in(&mut cx, |worktree_picker, window, cx| {
|
||||||
|
worktree_picker.picker.update(cx, |picker, cx| {
|
||||||
|
picker.delegate.delete_worktree(index, false, window, cx);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
assert!(deleting_worktree_paths(&worktree_picker, &mut cx).contains(&worktree_path));
|
||||||
|
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
assert!(deleting_worktree_paths(&worktree_picker, &mut cx).is_empty());
|
||||||
|
assert!(!picker_contains_worktree(
|
||||||
|
&worktree_picker,
|
||||||
|
&worktree_path,
|
||||||
|
&mut cx
|
||||||
|
));
|
||||||
|
assert!(
|
||||||
|
!repo_contains_worktree(&repository, &worktree_path, &mut cx).await,
|
||||||
|
"worktree should be removed after successful delete"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_remote_default_branch_is_preferred_create_target(cx: &mut TestAppContext) {
|
async fn test_remote_default_branch_is_preferred_create_target(cx: &mut TestAppContext) {
|
||||||
let (_fs, worktree_picker, _repository, _worktree_path, mut cx) =
|
let (_fs, worktree_picker, _repository, _worktree_path, mut cx) =
|
||||||
|
|
@ -1539,6 +1716,37 @@ mod tests {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_current_branch_create_target_is_shown_without_default_branch(
|
||||||
|
cx: &mut TestAppContext,
|
||||||
|
) {
|
||||||
|
let (_fs, worktree_picker, _repository, _worktree_path, mut cx) =
|
||||||
|
init_worktree_picker_test(cx).await;
|
||||||
|
|
||||||
|
worktree_picker.update_in(&mut cx, |worktree_picker, window, cx| {
|
||||||
|
worktree_picker.picker.update(cx, |picker, cx| {
|
||||||
|
picker.delegate.default_branch = None;
|
||||||
|
picker.refresh(window, cx);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
worktree_picker.update(&mut cx, |worktree_picker, cx| {
|
||||||
|
worktree_picker.picker.update(cx, |picker, _| {
|
||||||
|
assert!(matches!(
|
||||||
|
picker.delegate.matches.first(),
|
||||||
|
Some(WorktreeEntry::CreateFromCurrentBranch)
|
||||||
|
));
|
||||||
|
assert!(
|
||||||
|
!picker.delegate.matches.iter().any(|entry| matches!(
|
||||||
|
entry,
|
||||||
|
WorktreeEntry::CreateFromDefaultBranch { .. }
|
||||||
|
))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_delete_dirty_worktree_prompts_for_force_delete(cx: &mut TestAppContext) {
|
async fn test_delete_dirty_worktree_prompts_for_force_delete(cx: &mut TestAppContext) {
|
||||||
let (fs, worktree_picker, repository, worktree_path, mut cx) =
|
let (fs, worktree_picker, repository, worktree_path, mut cx) =
|
||||||
|
|
@ -1557,19 +1765,96 @@ mod tests {
|
||||||
picker.delegate.delete_worktree(index, false, window, cx);
|
picker.delegate.delete_worktree(index, false, window, cx);
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
assert!(deleting_worktree_paths(&worktree_picker, &mut cx).contains(&worktree_path));
|
||||||
|
|
||||||
cx.run_until_parked();
|
cx.run_until_parked();
|
||||||
assert!(cx.has_pending_prompt());
|
assert!(cx.has_pending_prompt());
|
||||||
|
assert!(
|
||||||
|
!deleting_worktree_paths(&worktree_picker, &mut cx).contains(&worktree_path),
|
||||||
|
"pending delete state should clear while waiting for force-delete confirmation"
|
||||||
|
);
|
||||||
|
|
||||||
cx.simulate_prompt_answer("Force Delete");
|
cx.simulate_prompt_answer("Force Delete");
|
||||||
cx.run_until_parked();
|
cx.run_until_parked();
|
||||||
|
|
||||||
assert!(!cx.has_pending_prompt());
|
assert!(!cx.has_pending_prompt());
|
||||||
|
assert!(deleting_worktree_paths(&worktree_picker, &mut cx).is_empty());
|
||||||
|
assert!(!picker_contains_worktree(
|
||||||
|
&worktree_picker,
|
||||||
|
&worktree_path,
|
||||||
|
&mut cx
|
||||||
|
));
|
||||||
assert!(
|
assert!(
|
||||||
!repo_contains_worktree(&repository, &worktree_path, &mut cx).await,
|
!repo_contains_worktree(&repository, &worktree_path, &mut cx).await,
|
||||||
"worktree should be removed after confirming force delete"
|
"worktree should be removed after confirming force delete"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_duplicate_delete_worktree_is_ignored_while_pending(cx: &mut TestAppContext) {
|
||||||
|
let (fs, worktree_picker, _repository, worktree_path, mut cx) =
|
||||||
|
init_worktree_picker_test(cx).await;
|
||||||
|
|
||||||
|
fs.with_git_state(path!("/root/project/.git").as_ref(), true, |state| {
|
||||||
|
state
|
||||||
|
.worktrees_requiring_force_delete
|
||||||
|
.insert(worktree_path.clone());
|
||||||
|
})
|
||||||
|
.expect("failed to mark test worktree as requiring force delete");
|
||||||
|
|
||||||
|
let index = worktree_index(&worktree_picker, &worktree_path, &mut cx);
|
||||||
|
worktree_picker.update_in(&mut cx, |worktree_picker, window, cx| {
|
||||||
|
worktree_picker.picker.update(cx, |picker, cx| {
|
||||||
|
picker.delegate.delete_worktree(index, false, window, cx);
|
||||||
|
picker.delegate.delete_worktree(index, false, window, cx);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let pending_paths = deleting_worktree_paths(&worktree_picker, &mut cx);
|
||||||
|
assert_eq!(pending_paths.len(), 1);
|
||||||
|
assert!(pending_paths.contains(&worktree_path));
|
||||||
|
|
||||||
|
cx.run_until_parked();
|
||||||
|
assert!(cx.has_pending_prompt());
|
||||||
|
assert!(deleting_worktree_paths(&worktree_picker, &mut cx).is_empty());
|
||||||
|
|
||||||
|
cx.simulate_prompt_answer("Cancel");
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
assert!(!cx.has_pending_prompt());
|
||||||
|
assert!(picker_contains_worktree(
|
||||||
|
&worktree_picker,
|
||||||
|
&worktree_path,
|
||||||
|
&mut cx
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_selected_deleting_worktree_cannot_be_opened(cx: &mut TestAppContext) {
|
||||||
|
let (_, worktree_picker, _repository, worktree_path, mut cx) =
|
||||||
|
init_worktree_picker_test(cx).await;
|
||||||
|
|
||||||
|
let subscription = cx.update(|_, cx| {
|
||||||
|
cx.subscribe(&worktree_picker, |_, _: &DismissEvent, _| {
|
||||||
|
panic!("DismissEvent should not be emitted for a deleting worktree");
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let index = worktree_index(&worktree_picker, &worktree_path, &mut cx);
|
||||||
|
worktree_picker.update_in(&mut cx, |worktree_picker, window, cx| {
|
||||||
|
worktree_picker.picker.update(cx, |picker, cx| {
|
||||||
|
picker.delegate.selected_index = index;
|
||||||
|
picker.delegate.delete_worktree(index, false, window, cx);
|
||||||
|
picker.delegate.confirm(false, window, cx);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(deleting_worktree_paths(&worktree_picker, &mut cx).contains(&worktree_path));
|
||||||
|
|
||||||
|
drop(subscription);
|
||||||
|
cx.run_until_parked();
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_force_delete_worktree_deletes_without_prompt(cx: &mut TestAppContext) {
|
async fn test_force_delete_worktree_deletes_without_prompt(cx: &mut TestAppContext) {
|
||||||
let (fs, worktree_picker, repository, worktree_path, mut cx) =
|
let (fs, worktree_picker, repository, worktree_path, mut cx) =
|
||||||
|
|
@ -1589,9 +1874,17 @@ mod tests {
|
||||||
picker.delegate.delete_worktree(index, true, window, cx);
|
picker.delegate.delete_worktree(index, true, window, cx);
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
assert!(deleting_worktree_paths(&worktree_picker, &mut cx).contains(&worktree_path));
|
||||||
|
|
||||||
cx.run_until_parked();
|
cx.run_until_parked();
|
||||||
|
|
||||||
assert!(!cx.has_pending_prompt());
|
assert!(!cx.has_pending_prompt());
|
||||||
|
assert!(deleting_worktree_paths(&worktree_picker, &mut cx).is_empty());
|
||||||
|
assert!(!picker_contains_worktree(
|
||||||
|
&worktree_picker,
|
||||||
|
&worktree_path,
|
||||||
|
&mut cx
|
||||||
|
));
|
||||||
assert!(
|
assert!(
|
||||||
!repo_contains_worktree(&repository, &worktree_path, &mut cx).await,
|
!repo_contains_worktree(&repository, &worktree_path, &mut cx).await,
|
||||||
"worktree should be removed by explicit force delete"
|
"worktree should be removed by explicit force delete"
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,21 @@ impl Application {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Builds an app with accessibility (AccessKit) integration forcibly
|
||||||
|
/// disabled.
|
||||||
|
///
|
||||||
|
/// In this mode, accessibility APIs (e.g.
|
||||||
|
/// [`div().role()`][crate::StatefulInteractiveElement::role]) silently
|
||||||
|
/// no-op.
|
||||||
|
///
|
||||||
|
/// See the [accessibility guide](crate::_accessibility) for an overview of
|
||||||
|
/// the features this disables.
|
||||||
|
pub fn new_inaccessible(platform: Rc<dyn Platform>) -> Self {
|
||||||
|
let this = Self::with_platform(platform);
|
||||||
|
this.0.borrow_mut().accessibility_force_disabled = true;
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
/// Assigns the source of assets for the application.
|
/// Assigns the source of assets for the application.
|
||||||
pub fn with_assets(self, asset_source: impl AssetSource) -> Self {
|
pub fn with_assets(self, asset_source: impl AssetSource) -> Self {
|
||||||
let mut context_lock = self.0.borrow_mut();
|
let mut context_lock = self.0.borrow_mut();
|
||||||
|
|
@ -666,6 +681,9 @@ pub struct App {
|
||||||
pub(crate) window_update_stack: Vec<WindowId>,
|
pub(crate) window_update_stack: Vec<WindowId>,
|
||||||
pub(crate) mode: GpuiMode,
|
pub(crate) mode: GpuiMode,
|
||||||
pub(crate) cursor_hide_mode: CursorHideMode,
|
pub(crate) cursor_hide_mode: CursorHideMode,
|
||||||
|
/// Whether the app was created by [`Application::new_inaccessible`]. No
|
||||||
|
/// accesskit APIs will be called when this flag is set.
|
||||||
|
pub(crate) accessibility_force_disabled: bool,
|
||||||
flushing_effects: bool,
|
flushing_effects: bool,
|
||||||
pending_updates: usize,
|
pending_updates: usize,
|
||||||
quit_mode: QuitMode,
|
quit_mode: QuitMode,
|
||||||
|
|
@ -755,6 +773,7 @@ impl App {
|
||||||
quit_mode: QuitMode::default(),
|
quit_mode: QuitMode::default(),
|
||||||
quitting: false,
|
quitting: false,
|
||||||
cursor_hide_mode: CursorHideMode::default(),
|
cursor_hide_mode: CursorHideMode::default(),
|
||||||
|
accessibility_force_disabled: false,
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support", debug_assertions))]
|
#[cfg(any(test, feature = "test-support", debug_assertions))]
|
||||||
name: None,
|
name: None,
|
||||||
|
|
|
||||||
|
|
@ -336,6 +336,20 @@ impl TestAppContext {
|
||||||
self.test_platform.simulate_new_path_selection(select_path);
|
self.test_platform.simulate_new_path_selection(select_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Simulates responding to a `prompt_for_paths` ("Open") dialog.
|
||||||
|
pub fn simulate_path_prompt_response(
|
||||||
|
&self,
|
||||||
|
select_paths: impl FnOnce(&crate::PathPromptOptions) -> Option<Vec<std::path::PathBuf>>,
|
||||||
|
) {
|
||||||
|
self.test_platform
|
||||||
|
.simulate_path_prompt_response(select_paths);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if there's a path selection dialog pending.
|
||||||
|
pub fn did_prompt_for_paths(&self) -> bool {
|
||||||
|
self.test_platform.did_prompt_for_paths()
|
||||||
|
}
|
||||||
|
|
||||||
/// Simulates clicking a button in an platform-level alert dialog.
|
/// Simulates clicking a button in an platform-level alert dialog.
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
pub fn simulate_prompt_answer(&self, button: &str) {
|
pub fn simulate_prompt_answer(&self, button: &str) {
|
||||||
|
|
@ -1098,3 +1112,54 @@ impl AnyWindowHandle {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::{PathPromptOptions, TestAppContext};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_simulate_path_prompt_response(cx: &mut TestAppContext) {
|
||||||
|
assert!(!cx.did_prompt_for_paths());
|
||||||
|
|
||||||
|
let receiver = cx.update(|cx| {
|
||||||
|
cx.prompt_for_paths(PathPromptOptions {
|
||||||
|
files: false,
|
||||||
|
directories: true,
|
||||||
|
multiple: true,
|
||||||
|
prompt: None,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
assert!(cx.did_prompt_for_paths());
|
||||||
|
|
||||||
|
let selected = vec![PathBuf::from("/a"), PathBuf::from("/b")];
|
||||||
|
cx.simulate_path_prompt_response({
|
||||||
|
let selected = selected.clone();
|
||||||
|
move |options| {
|
||||||
|
assert!(options.multiple);
|
||||||
|
Some(selected)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
assert!(!cx.did_prompt_for_paths());
|
||||||
|
|
||||||
|
let response = receiver.await.unwrap().unwrap();
|
||||||
|
assert_eq!(response, Some(selected));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_simulate_path_prompt_cancellation(cx: &mut TestAppContext) {
|
||||||
|
let receiver = cx.update(|cx| {
|
||||||
|
cx.prompt_for_paths(PathPromptOptions {
|
||||||
|
files: true,
|
||||||
|
directories: false,
|
||||||
|
multiple: false,
|
||||||
|
prompt: None,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.simulate_path_prompt_response(|_options| None);
|
||||||
|
|
||||||
|
let response = receiver.await.unwrap().unwrap();
|
||||||
|
assert_eq!(response, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -741,6 +741,44 @@ impl ListState {
|
||||||
pub fn viewport_bounds(&self) -> Bounds<Pixels> {
|
pub fn viewport_bounds(&self) -> Bounds<Pixels> {
|
||||||
self.0.borrow().last_layout_bounds.unwrap_or_default()
|
self.0.borrow().last_layout_bounds.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns whether the item is entirely above the viewport, or `None` if
|
||||||
|
/// the list has not measured enough layout to know.
|
||||||
|
pub fn item_is_above_viewport(&self, ix: usize) -> Option<bool> {
|
||||||
|
let viewport_bounds = self.viewport_bounds();
|
||||||
|
if viewport_bounds.size.height == px(0.0) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let scroll_top = self.logical_scroll_top();
|
||||||
|
if ix < scroll_top.item_ix {
|
||||||
|
// Rows before the logical scroll top have no item bounds, but
|
||||||
|
// their position relative to the viewport is known from scroll state.
|
||||||
|
return Some(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
let item_bounds = self.bounds_for_item(ix)?;
|
||||||
|
Some(item_bounds.bottom() <= viewport_bounds.top())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether the item is entirely below the viewport, or `None` if
|
||||||
|
/// the list has not measured enough layout to know.
|
||||||
|
pub fn item_is_below_viewport(&self, ix: usize) -> Option<bool> {
|
||||||
|
let viewport_bounds = self.viewport_bounds();
|
||||||
|
if viewport_bounds.size.height == px(0.0) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let scroll_top = self.logical_scroll_top();
|
||||||
|
if ix < scroll_top.item_ix {
|
||||||
|
// Rows before the logical scroll top have no item bounds, but
|
||||||
|
// their position relative to the viewport is known from scroll state.
|
||||||
|
return Some(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
let item_bounds = self.bounds_for_item(ix)?;
|
||||||
|
Some(item_bounds.top() >= viewport_bounds.bottom())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StateInner {
|
impl StateInner {
|
||||||
|
|
@ -1644,6 +1682,114 @@ mod test {
|
||||||
assert_eq!(offset.offset_in_item, px(0.));
|
assert_eq!(offset.offset_in_item, px(0.));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct TestListView(ListState);
|
||||||
|
impl Render for TestListView {
|
||||||
|
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
list(self.0.clone(), |_, _, _| {
|
||||||
|
div().h(px(20.)).w_full().into_any()
|
||||||
|
})
|
||||||
|
.w_full()
|
||||||
|
.h_full()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn test_item_viewport_queries_return_none_before_layout(_cx: &mut TestAppContext) {
|
||||||
|
let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
|
||||||
|
|
||||||
|
assert_eq!(state.item_is_above_viewport(0), None);
|
||||||
|
assert_eq!(state.item_is_below_viewport(0), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn test_item_viewport_queries_before_logical_scroll_top(cx: &mut TestAppContext) {
|
||||||
|
let cx = cx.add_empty_window();
|
||||||
|
|
||||||
|
let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
|
||||||
|
|
||||||
|
state.scroll_to(gpui::ListOffset {
|
||||||
|
item_ix: 2,
|
||||||
|
offset_in_item: px(0.),
|
||||||
|
});
|
||||||
|
cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
|
||||||
|
cx.new(|_| TestListView(state.clone())).into_any_element()
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(state.item_is_above_viewport(1), Some(true));
|
||||||
|
assert_eq!(state.item_is_below_viewport(1), Some(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn test_item_viewport_queries_measured_item_inside_viewport(cx: &mut TestAppContext) {
|
||||||
|
let cx = cx.add_empty_window();
|
||||||
|
|
||||||
|
let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
|
||||||
|
|
||||||
|
state.scroll_to(gpui::ListOffset {
|
||||||
|
item_ix: 2,
|
||||||
|
offset_in_item: px(0.),
|
||||||
|
});
|
||||||
|
cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
|
||||||
|
cx.new(|_| TestListView(state.clone())).into_any_element()
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(state.item_is_above_viewport(2), Some(false));
|
||||||
|
assert_eq!(state.item_is_below_viewport(2), Some(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn test_item_viewport_queries_measured_item_above_viewport(cx: &mut TestAppContext) {
|
||||||
|
let cx = cx.add_empty_window();
|
||||||
|
|
||||||
|
let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
|
||||||
|
|
||||||
|
state.scroll_to(gpui::ListOffset {
|
||||||
|
item_ix: 2,
|
||||||
|
offset_in_item: px(20.),
|
||||||
|
});
|
||||||
|
cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
|
||||||
|
cx.new(|_| TestListView(state.clone())).into_any_element()
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(state.item_is_above_viewport(2), Some(true));
|
||||||
|
assert_eq!(state.item_is_below_viewport(2), Some(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn test_item_viewport_queries_measured_item_below_viewport(cx: &mut TestAppContext) {
|
||||||
|
let cx = cx.add_empty_window();
|
||||||
|
|
||||||
|
let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
|
||||||
|
|
||||||
|
state.scroll_to(gpui::ListOffset {
|
||||||
|
item_ix: 2,
|
||||||
|
offset_in_item: px(0.),
|
||||||
|
});
|
||||||
|
cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
|
||||||
|
cx.new(|_| TestListView(state.clone())).into_any_element()
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(state.item_is_above_viewport(3), Some(false));
|
||||||
|
assert_eq!(state.item_is_below_viewport(3), Some(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn test_item_viewport_queries_after_scroll_to_end_before_layout(cx: &mut TestAppContext) {
|
||||||
|
let cx = cx.add_empty_window();
|
||||||
|
|
||||||
|
let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all();
|
||||||
|
|
||||||
|
cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
|
||||||
|
cx.new(|_| TestListView(state.clone())).into_any_element()
|
||||||
|
});
|
||||||
|
|
||||||
|
state.scroll_to_end();
|
||||||
|
|
||||||
|
assert_eq!(state.logical_scroll_top().item_ix, state.item_count());
|
||||||
|
assert_eq!(state.item_is_above_viewport(0), Some(true));
|
||||||
|
assert_eq!(state.item_is_below_viewport(0), Some(false));
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_measure_all_after_width_change(cx: &mut TestAppContext) {
|
fn test_measure_all_after_width_change(cx: &mut TestAppContext) {
|
||||||
let cx = cx.add_empty_window();
|
let cx = cx.add_empty_window();
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,7 @@ use scheduler::Instant;
|
||||||
use scheduler::Scheduler;
|
use scheduler::Scheduler;
|
||||||
use std::{future::Future, marker::PhantomData, mem, pin::Pin, rc::Rc, sync::Arc, time::Duration};
|
use std::{future::Future, marker::PhantomData, mem, pin::Pin, rc::Rc, sync::Arc, time::Duration};
|
||||||
|
|
||||||
pub use scheduler::{
|
pub use scheduler::{FallibleTask, LocalExecutor as SchedulerLocalExecutor, Priority, Task};
|
||||||
FallibleTask, ForegroundExecutor as SchedulerForegroundExecutor, Priority, Task,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// A pointer to the executor that is currently running,
|
/// A pointer to the executor that is currently running,
|
||||||
/// for spawning background tasks.
|
/// for spawning background tasks.
|
||||||
|
|
@ -22,7 +20,7 @@ pub struct BackgroundExecutor {
|
||||||
/// for spawning tasks on the main thread.
|
/// for spawning tasks on the main thread.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ForegroundExecutor {
|
pub struct ForegroundExecutor {
|
||||||
inner: scheduler::ForegroundExecutor,
|
inner: scheduler::LocalExecutor,
|
||||||
dispatcher: Arc<dyn PlatformDispatcher>,
|
dispatcher: Arc<dyn PlatformDispatcher>,
|
||||||
not_send: PhantomData<Rc<()>>,
|
not_send: PhantomData<Rc<()>>,
|
||||||
}
|
}
|
||||||
|
|
@ -280,18 +278,29 @@ impl ForegroundExecutor {
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
let platform_scheduler = Arc::new(PlatformScheduler::new(dispatcher.clone()));
|
let platform_scheduler = Arc::new(PlatformScheduler::new(dispatcher.clone()));
|
||||||
let session_id = platform_scheduler.allocate_session_id();
|
let inner = platform_scheduler.foreground_executor();
|
||||||
(platform_scheduler, session_id)
|
return Self {
|
||||||
|
inner,
|
||||||
|
dispatcher,
|
||||||
|
not_send: PhantomData,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(not(any(test, feature = "test-support")))]
|
#[cfg(not(any(test, feature = "test-support")))]
|
||||||
let (scheduler, session_id): (Arc<dyn Scheduler>, _) = {
|
let inner = {
|
||||||
let platform_scheduler = Arc::new(PlatformScheduler::new(dispatcher.clone()));
|
let platform_scheduler = Arc::new(PlatformScheduler::new(dispatcher.clone()));
|
||||||
let session_id = platform_scheduler.allocate_session_id();
|
platform_scheduler.foreground_executor()
|
||||||
(platform_scheduler, session_id)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let inner = scheduler::ForegroundExecutor::new(session_id, scheduler);
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
let inner = {
|
||||||
|
let scheduler_for_dispatch = Arc::downgrade(&scheduler);
|
||||||
|
scheduler::LocalExecutor::new(session_id, scheduler, move |runnable| {
|
||||||
|
if let Some(scheduler) = scheduler_for_dispatch.upgrade() {
|
||||||
|
scheduler.schedule_local(session_id, runnable);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
inner,
|
inner,
|
||||||
|
|
@ -366,7 +375,7 @@ impl ForegroundExecutor {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub fn scheduler_executor(&self) -> SchedulerForegroundExecutor {
|
pub fn scheduler_executor(&self) -> SchedulerLocalExecutor {
|
||||||
self.inner.clone()
|
self.inner.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -139,8 +139,7 @@ impl PlatformDispatcher for TestDispatcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_on_main_thread(&self, runnable: RunnableVariant, _priority: Priority) {
|
fn dispatch_on_main_thread(&self, runnable: RunnableVariant, _priority: Priority) {
|
||||||
self.scheduler
|
self.scheduler.schedule_local(self.session_id, runnable);
|
||||||
.schedule_foreground(self.session_id, runnable);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_after(&self, _duration: Duration, _runnable: RunnableVariant) {
|
fn dispatch_after(&self, _duration: Duration, _runnable: RunnableVariant) {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
|
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
|
||||||
DummyKeyboardMapper, ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay,
|
DummyKeyboardMapper, ForegroundExecutor, Keymap, NoopTextSystem, PathPromptOptions, Platform,
|
||||||
PlatformHeadlessRenderer, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem,
|
PlatformDisplay, PlatformHeadlessRenderer, PlatformKeyboardLayout, PlatformKeyboardMapper,
|
||||||
PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, SourceMetadata,
|
PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream,
|
||||||
Task, TestDisplay, TestWindow, ThermalState, WindowAppearance, WindowParams, size,
|
SourceMetadata, Task, TestDisplay, TestWindow, ThermalState, WindowAppearance, WindowParams,
|
||||||
|
size,
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use collections::VecDeque;
|
use collections::VecDeque;
|
||||||
|
|
@ -85,6 +86,10 @@ struct TestPrompt {
|
||||||
pub(crate) struct TestPrompts {
|
pub(crate) struct TestPrompts {
|
||||||
multiple_choice: VecDeque<TestPrompt>,
|
multiple_choice: VecDeque<TestPrompt>,
|
||||||
new_path: VecDeque<(PathBuf, oneshot::Sender<Result<Option<PathBuf>>>)>,
|
new_path: VecDeque<(PathBuf, oneshot::Sender<Result<Option<PathBuf>>>)>,
|
||||||
|
paths: VecDeque<(
|
||||||
|
PathPromptOptions,
|
||||||
|
oneshot::Sender<Result<Option<Vec<PathBuf>>>>,
|
||||||
|
)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TestPlatform {
|
impl TestPlatform {
|
||||||
|
|
@ -147,6 +152,33 @@ impl TestPlatform {
|
||||||
tx.send(Ok(select_path(&path))).ok();
|
tx.send(Ok(select_path(&path))).ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn simulate_path_prompt_response(
|
||||||
|
&self,
|
||||||
|
select_paths: impl FnOnce(&PathPromptOptions) -> Option<Vec<std::path::PathBuf>>,
|
||||||
|
) {
|
||||||
|
let (options, tx) = self
|
||||||
|
.prompts
|
||||||
|
.borrow_mut()
|
||||||
|
.paths
|
||||||
|
.pop_front()
|
||||||
|
.expect("no pending paths prompt");
|
||||||
|
let selection = select_paths(&options);
|
||||||
|
if let Some(paths) = &selection
|
||||||
|
&& !options.multiple
|
||||||
|
&& paths.len() > 1
|
||||||
|
{
|
||||||
|
panic!(
|
||||||
|
"selected {} paths for a prompt that does not allow multiple selection",
|
||||||
|
paths.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
tx.send(Ok(selection)).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn did_prompt_for_paths(&self) -> bool {
|
||||||
|
!self.prompts.borrow().paths.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
pub(crate) fn simulate_prompt_answer(&self, response: &str) {
|
pub(crate) fn simulate_prompt_answer(&self, response: &str) {
|
||||||
let prompt = self
|
let prompt = self
|
||||||
|
|
@ -348,9 +380,11 @@ impl Platform for TestPlatform {
|
||||||
|
|
||||||
fn prompt_for_paths(
|
fn prompt_for_paths(
|
||||||
&self,
|
&self,
|
||||||
_options: crate::PathPromptOptions,
|
options: crate::PathPromptOptions,
|
||||||
) -> oneshot::Receiver<Result<Option<Vec<std::path::PathBuf>>>> {
|
) -> oneshot::Receiver<Result<Option<Vec<std::path::PathBuf>>>> {
|
||||||
unimplemented!()
|
let (tx, rx) = oneshot::channel();
|
||||||
|
self.prompts.borrow_mut().paths.push_back((options, tx));
|
||||||
|
rx
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt_for_new_path(
|
fn prompt_for_new_path(
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,14 @@ use async_task::Runnable;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use futures::channel::oneshot;
|
use futures::channel::oneshot;
|
||||||
use scheduler::Instant;
|
use scheduler::Instant;
|
||||||
use scheduler::{Clock, Priority, Scheduler, SessionId, TestScheduler, Timer};
|
use scheduler::{
|
||||||
|
Clock, LocalExecutor, Priority, Scheduler, SessionId, Task, TestScheduler, Timer,
|
||||||
|
spawn_dedicated_thread,
|
||||||
|
};
|
||||||
#[cfg(not(target_family = "wasm"))]
|
#[cfg(not(target_family = "wasm"))]
|
||||||
use std::task::{Context, Poll};
|
use std::task::{Context, Poll};
|
||||||
use std::{
|
use std::{
|
||||||
|
any::Any,
|
||||||
future::Future,
|
future::Future,
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
sync::{
|
sync::{
|
||||||
|
|
@ -35,7 +39,17 @@ impl PlatformScheduler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn allocate_session_id(&self) -> SessionId {
|
pub fn foreground_executor(self: &Arc<Self>) -> LocalExecutor {
|
||||||
|
let session_id = self.next_session_id();
|
||||||
|
let scheduler = Arc::downgrade(self);
|
||||||
|
LocalExecutor::new(session_id, self.clone(), move |runnable| {
|
||||||
|
if let Some(scheduler) = scheduler.upgrade() {
|
||||||
|
scheduler.schedule_local(session_id, runnable);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_session_id(&self) -> SessionId {
|
||||||
SessionId::new(self.next_session_id.fetch_add(1, Ordering::SeqCst))
|
SessionId::new(self.next_session_id.fetch_add(1, Ordering::SeqCst))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -90,7 +104,7 @@ impl Scheduler for PlatformScheduler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn schedule_foreground(&self, _session_id: SessionId, runnable: Runnable<RunnableMeta>) {
|
fn schedule_local(&self, _session_id: SessionId, runnable: Runnable<RunnableMeta>) {
|
||||||
self.dispatcher
|
self.dispatcher
|
||||||
.dispatch_on_main_thread(runnable, Priority::default());
|
.dispatch_on_main_thread(runnable, Priority::default());
|
||||||
}
|
}
|
||||||
|
|
@ -133,6 +147,21 @@ impl Scheduler for PlatformScheduler {
|
||||||
self.clock.clone()
|
self.clock.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn spawn_dedicated(
|
||||||
|
self: Arc<Self>,
|
||||||
|
f: Box<
|
||||||
|
dyn FnOnce(
|
||||||
|
LocalExecutor,
|
||||||
|
)
|
||||||
|
-> Pin<Box<dyn Future<Output = Box<dyn Any + Send + Sync>> + 'static>>
|
||||||
|
+ Send
|
||||||
|
+ 'static,
|
||||||
|
>,
|
||||||
|
) -> Task<Box<dyn Any + Send + Sync>> {
|
||||||
|
let session_id = self.next_session_id();
|
||||||
|
spawn_dedicated_thread(session_id, self, move |executor| f(executor))
|
||||||
|
}
|
||||||
|
|
||||||
fn as_test(&self) -> Option<&TestScheduler> {
|
fn as_test(&self) -> Option<&TestScheduler> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
@ -152,3 +181,261 @@ impl Clock for PlatformClock {
|
||||||
self.dispatcher.now()
|
self.dispatcher.now()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, not(target_family = "wasm")))]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::{RunnableVariant, ThreadTaskTimings};
|
||||||
|
use scheduler::BackgroundExecutor;
|
||||||
|
use std::time::Instant as StdInstant;
|
||||||
|
|
||||||
|
// `spawn_dedicated` shouldn't touch the platform dispatcher at all;
|
||||||
|
// panicking on every method ensures the test catches it if it does.
|
||||||
|
struct SmokeDispatcher;
|
||||||
|
|
||||||
|
impl PlatformDispatcher for SmokeDispatcher {
|
||||||
|
fn get_all_timings(&self) -> Vec<ThreadTaskTimings> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
fn get_current_thread_timings(&self) -> ThreadTaskTimings {
|
||||||
|
ThreadTaskTimings {
|
||||||
|
thread_name: None,
|
||||||
|
thread_id: std::thread::current().id(),
|
||||||
|
timings: Vec::new(),
|
||||||
|
total_pushed: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn is_main_thread(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
fn dispatch(&self, _runnable: RunnableVariant, _priority: Priority) {
|
||||||
|
panic!("SmokeDispatcher should not be asked to dispatch in this test");
|
||||||
|
}
|
||||||
|
fn dispatch_on_main_thread(&self, _runnable: RunnableVariant, _priority: Priority) {
|
||||||
|
panic!("SmokeDispatcher does not implement a main thread");
|
||||||
|
}
|
||||||
|
fn dispatch_after(&self, _duration: Duration, _runnable: RunnableVariant) {
|
||||||
|
panic!("SmokeDispatcher does not implement timers");
|
||||||
|
}
|
||||||
|
fn spawn_realtime(&self, _f: Box<dyn FnOnce() + Send>) {
|
||||||
|
panic!("SmokeDispatcher does not implement realtime");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spawn_dedicated_runs_on_a_real_separate_thread() {
|
||||||
|
let background =
|
||||||
|
BackgroundExecutor::new(Arc::new(PlatformScheduler::new(Arc::new(SmokeDispatcher))));
|
||||||
|
let started = StdInstant::now();
|
||||||
|
let task = background.spawn_dedicated(|_executor| async move {
|
||||||
|
// A genuine blocking syscall on the dedicated thread. If
|
||||||
|
// `spawn_dedicated` were running the future on any shared
|
||||||
|
// executor, this would stall that executor.
|
||||||
|
let thread_id_before = std::thread::current().id();
|
||||||
|
std::thread::sleep(Duration::from_millis(50));
|
||||||
|
let thread_id_after = std::thread::current().id();
|
||||||
|
assert_eq!(thread_id_before, thread_id_after);
|
||||||
|
(thread_id_before, "slept")
|
||||||
|
});
|
||||||
|
let (dedicated_thread_id, message) = futures::executor::block_on(task);
|
||||||
|
let elapsed = started.elapsed();
|
||||||
|
assert_eq!(message, "slept");
|
||||||
|
assert_ne!(
|
||||||
|
dedicated_thread_id,
|
||||||
|
std::thread::current().id(),
|
||||||
|
"dedicated future ran on the test thread"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
elapsed >= Duration::from_millis(40),
|
||||||
|
"expected the dedicated thread to genuinely sleep, elapsed = {:?}",
|
||||||
|
elapsed
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spawn_dedicated_returns_not_send_future_output() {
|
||||||
|
// The whole point of `spawn_dedicated` is that the future can be
|
||||||
|
// `!Send`. Constructing one with `Rc<RefCell<_>>` ensures the
|
||||||
|
// signature actually permits it.
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
let background =
|
||||||
|
BackgroundExecutor::new(Arc::new(PlatformScheduler::new(Arc::new(SmokeDispatcher))));
|
||||||
|
let task = background.spawn_dedicated(|_executor| async move {
|
||||||
|
let state = Rc::new(RefCell::new(0_i32));
|
||||||
|
for _ in 0..3 {
|
||||||
|
*state.borrow_mut() += 1;
|
||||||
|
}
|
||||||
|
*state.borrow()
|
||||||
|
});
|
||||||
|
let output = futures::executor::block_on(task);
|
||||||
|
assert_eq!(output, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spawn_dedicated_dropping_task_cancels_future() {
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use std::sync::mpsc;
|
||||||
|
|
||||||
|
let background =
|
||||||
|
BackgroundExecutor::new(Arc::new(PlatformScheduler::new(Arc::new(SmokeDispatcher))));
|
||||||
|
|
||||||
|
let (started_tx, started_rx) = mpsc::channel::<()>();
|
||||||
|
let (after_park_tx, after_park_rx) = mpsc::channel::<()>();
|
||||||
|
let observed_post_await_write = Arc::new(Mutex::new(false));
|
||||||
|
|
||||||
|
let task = {
|
||||||
|
let observed_post_await_write = observed_post_await_write.clone();
|
||||||
|
background.spawn_dedicated(move |_executor| async move {
|
||||||
|
// Announce that the future is live on the dedicated thread.
|
||||||
|
started_tx
|
||||||
|
.send(())
|
||||||
|
.expect("started signal must be received");
|
||||||
|
// Park forever. Dropping the `Task` must cancel us here so
|
||||||
|
// the code below this `await` never runs.
|
||||||
|
futures::future::pending::<()>().await;
|
||||||
|
*observed_post_await_write.lock() = true;
|
||||||
|
after_park_tx
|
||||||
|
.send(())
|
||||||
|
.expect("after-park signal must be received");
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
// Wait until the dedicated future is actually parked at the await.
|
||||||
|
started_rx
|
||||||
|
.recv_timeout(Duration::from_secs(2))
|
||||||
|
.expect("dedicated future failed to start");
|
||||||
|
|
||||||
|
// Drop the root Task: this must cancel the future.
|
||||||
|
drop(task);
|
||||||
|
|
||||||
|
// If cancellation works, the future never advances past `pending`,
|
||||||
|
// so this recv must time out.
|
||||||
|
assert!(
|
||||||
|
after_park_rx
|
||||||
|
.recv_timeout(Duration::from_millis(100))
|
||||||
|
.is_err(),
|
||||||
|
"dedicated future advanced past the await after its Task was dropped"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!*observed_post_await_write.lock(),
|
||||||
|
"dedicated future ran code past the cancellation point"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spawn_dedicated_thread_tears_down_after_work_completes() {
|
||||||
|
use std::sync::mpsc;
|
||||||
|
|
||||||
|
// Fires from `Drop` so we observe teardown of the dedicated future's
|
||||||
|
// captured state on whichever thread runs its destructor.
|
||||||
|
struct DropSignal {
|
||||||
|
tx: Option<mpsc::Sender<std::thread::ThreadId>>,
|
||||||
|
}
|
||||||
|
impl Drop for DropSignal {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Some(tx) = self.tx.take() {
|
||||||
|
let _ = tx.send(std::thread::current().id());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let background =
|
||||||
|
BackgroundExecutor::new(Arc::new(PlatformScheduler::new(Arc::new(SmokeDispatcher))));
|
||||||
|
let (started_tx, started_rx) = mpsc::channel::<std::thread::ThreadId>();
|
||||||
|
let (drop_tx, drop_rx) = mpsc::channel::<std::thread::ThreadId>();
|
||||||
|
|
||||||
|
let task = background.spawn_dedicated(move |_executor| async move {
|
||||||
|
// Captured by the future's state. When the future completes and
|
||||||
|
// its state is dropped on the dedicated thread, this guard's
|
||||||
|
// `Drop` fires and reports the thread id it ran on.
|
||||||
|
let _guard = DropSignal { tx: Some(drop_tx) };
|
||||||
|
started_tx
|
||||||
|
.send(std::thread::current().id())
|
||||||
|
.expect("started signal must be received");
|
||||||
|
// Future returns immediately. The dedicated thread should then
|
||||||
|
// drop the future (firing _guard), exit the recv loop, and exit.
|
||||||
|
});
|
||||||
|
|
||||||
|
let dedicated_thread_id = started_rx
|
||||||
|
.recv_timeout(Duration::from_secs(2))
|
||||||
|
.expect("dedicated future failed to start");
|
||||||
|
assert_ne!(
|
||||||
|
dedicated_thread_id,
|
||||||
|
std::thread::current().id(),
|
||||||
|
"dedicated future ran on the test thread"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Drive the root task to completion so its body finishes.
|
||||||
|
futures::executor::block_on(task);
|
||||||
|
|
||||||
|
// The guard's drop runs from the dedicated thread as it tears down
|
||||||
|
// the future's captured state. If the executor/recv-loop were
|
||||||
|
// keeping the future alive past task completion, this would hang.
|
||||||
|
let drop_thread_id = drop_rx
|
||||||
|
.recv_timeout(Duration::from_secs(2))
|
||||||
|
.expect("dedicated future's captured state was not dropped after task completion");
|
||||||
|
assert_eq!(
|
||||||
|
drop_thread_id, dedicated_thread_id,
|
||||||
|
"dedicated future's captured state must be dropped on the dedicated thread, not elsewhere"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spawn_dedicated_detached_child_outlives_root() {
|
||||||
|
use std::sync::mpsc;
|
||||||
|
|
||||||
|
let background =
|
||||||
|
BackgroundExecutor::new(Arc::new(PlatformScheduler::new(Arc::new(SmokeDispatcher))));
|
||||||
|
|
||||||
|
// `gate_rx` lets the detached child park until the test explicitly
|
||||||
|
// releases it — after we've already observed the root completing.
|
||||||
|
let (gate_tx, gate_rx) = mpsc::channel::<()>();
|
||||||
|
let (child_done_tx, child_done_rx) = mpsc::channel::<std::thread::ThreadId>();
|
||||||
|
|
||||||
|
let task = background.spawn_dedicated(move |executor| async move {
|
||||||
|
executor
|
||||||
|
.spawn(async move {
|
||||||
|
// Blocking on `recv` is normally wrong inside an
|
||||||
|
// executor, but the dedicated thread is exclusive to
|
||||||
|
// this session, so blocking the only future on it is
|
||||||
|
// fine — this is the property `spawn_dedicated` is
|
||||||
|
// designed to provide.
|
||||||
|
gate_rx
|
||||||
|
.recv()
|
||||||
|
.expect("gate sender dropped before child resumed");
|
||||||
|
child_done_tx
|
||||||
|
.send(std::thread::current().id())
|
||||||
|
.expect("child_done receiver dropped");
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
// Root finishes here. The detached child must keep the
|
||||||
|
// dedicated thread alive until it completes.
|
||||||
|
});
|
||||||
|
|
||||||
|
futures::executor::block_on(task);
|
||||||
|
|
||||||
|
// Negative assertion: the child has not finished, because the gate
|
||||||
|
// hasn't been released yet.
|
||||||
|
assert!(
|
||||||
|
child_done_rx
|
||||||
|
.recv_timeout(Duration::from_millis(50))
|
||||||
|
.is_err(),
|
||||||
|
"detached child finished before being released"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Release the gate. The detached child should now complete on the
|
||||||
|
// dedicated thread.
|
||||||
|
gate_tx.send(()).expect("gate receiver dropped");
|
||||||
|
|
||||||
|
let child_thread_id = child_done_rx
|
||||||
|
.recv_timeout(Duration::from_secs(2))
|
||||||
|
.expect("detached child failed to complete after gate was released");
|
||||||
|
assert_ne!(
|
||||||
|
child_thread_id,
|
||||||
|
std::thread::current().id(),
|
||||||
|
"detached child ran on the test thread instead of the dedicated thread"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1330,10 +1330,11 @@ impl Window {
|
||||||
WindowBounds::Windowed(_) => {}
|
WindowBounds::Windowed(_) => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let accessibility_force_disabled = cx.accessibility_force_disabled;
|
||||||
let a11y_active_flag = Arc::new(AtomicBool::new(false));
|
let a11y_active_flag = Arc::new(AtomicBool::new(false));
|
||||||
|
|
||||||
#[cfg(not(target_family = "wasm"))]
|
#[cfg(not(target_family = "wasm"))]
|
||||||
{
|
if !accessibility_force_disabled {
|
||||||
let initial_tree = accesskit::TreeUpdate {
|
let initial_tree = accesskit::TreeUpdate {
|
||||||
nodes: vec![(ROOT_NODE_ID, accesskit::Node::new(accesskit::Role::Window))],
|
nodes: vec![(ROOT_NODE_ID, accesskit::Node::new(accesskit::Role::Window))],
|
||||||
tree: Some(accesskit::Tree::new(ROOT_NODE_ID)),
|
tree: Some(accesskit::Tree::new(ROOT_NODE_ID)),
|
||||||
|
|
@ -1717,7 +1718,7 @@ impl Window {
|
||||||
captured_hitbox: None,
|
captured_hitbox: None,
|
||||||
#[cfg(any(feature = "inspector", debug_assertions))]
|
#[cfg(any(feature = "inspector", debug_assertions))]
|
||||||
inspector: None,
|
inspector: None,
|
||||||
a11y: A11y::new(a11y_active_flag),
|
a11y: A11y::new(a11y_active_flag, accessibility_force_disabled),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,10 @@ pub(crate) type A11yActionListener =
|
||||||
/// Manages the AccessKit tree that is built each frame and the mappings
|
/// Manages the AccessKit tree that is built each frame and the mappings
|
||||||
/// needed to dispatch incoming action requests back to the right elements.
|
/// needed to dispatch incoming action requests back to the right elements.
|
||||||
pub(crate) struct A11y {
|
pub(crate) struct A11y {
|
||||||
|
/// Whether accessibility has been [forcibly disabled] for this window.
|
||||||
|
///
|
||||||
|
/// [forcibly disabled]: crate::Application::new_inaccessible
|
||||||
|
force_disabled: bool,
|
||||||
/// Whether a11y features have been requested by the system.
|
/// Whether a11y features have been requested by the system.
|
||||||
///
|
///
|
||||||
/// Updated by AccessKit using callbacks provided to the adapter. Can change
|
/// Updated by AccessKit using callbacks provided to the adapter. Can change
|
||||||
|
|
@ -131,8 +135,9 @@ pub(crate) struct A11y {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl A11y {
|
impl A11y {
|
||||||
pub(crate) fn new(active_flag: Arc<AtomicBool>) -> Self {
|
pub(crate) fn new(active_flag: Arc<AtomicBool>, force_disabled: bool) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
force_disabled,
|
||||||
active_flag,
|
active_flag,
|
||||||
active_this_frame: false,
|
active_this_frame: false,
|
||||||
nodes: A11yNodeBuilder::new(),
|
nodes: A11yNodeBuilder::new(),
|
||||||
|
|
@ -147,7 +152,7 @@ impl A11y {
|
||||||
/// See the docs for [`Self::active_flag`] and [`Self::active_this_frame`]
|
/// See the docs for [`Self::active_flag`] and [`Self::active_this_frame`]
|
||||||
/// for more commentary.
|
/// for more commentary.
|
||||||
pub(crate) fn sync_active_flag(&mut self) {
|
pub(crate) fn sync_active_flag(&mut self) {
|
||||||
self.active_this_frame = self.active_flag.load(Ordering::SeqCst);
|
self.active_this_frame = !self.force_disabled && self.active_flag.load(Ordering::SeqCst);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn is_active(&self) -> bool {
|
pub(crate) fn is_active(&self) -> bool {
|
||||||
|
|
@ -164,7 +169,21 @@ impl A11y {
|
||||||
|
|
||||||
/// Finalize the tree and produce a [`TreeUpdate`] for the platform adapter.
|
/// Finalize the tree and produce a [`TreeUpdate`] for the platform adapter.
|
||||||
pub(crate) fn end_frame(&mut self) -> TreeUpdate {
|
pub(crate) fn end_frame(&mut self) -> TreeUpdate {
|
||||||
self.nodes.finalize()
|
let tree_update = self.nodes.finalize();
|
||||||
|
|
||||||
|
// Zed currently doesn't set any a11y APIs on *any* UI elements, so a
|
||||||
|
// tree with nodes other than the root indicates a bug in the
|
||||||
|
// `TreeUpdate`-producing logic.
|
||||||
|
//
|
||||||
|
// Remove this when adding aria attributes.
|
||||||
|
if tree_update.nodes.len() > 1 {
|
||||||
|
log::warn!(
|
||||||
|
"expected an empty a11y tree update (only the root node), but got {} nodes; Zed has no accessible UI elements yet",
|
||||||
|
tree_update.nodes.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
tree_update
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ default = []
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
gpui = { workspace = true, features = ["test-support"] }
|
gpui = { workspace = true, features = ["test-support"] }
|
||||||
settings = { workspace = true, features = ["test-support"] }
|
settings = { workspace = true, features = ["test-support"] }
|
||||||
|
tempfile.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -449,45 +449,48 @@ pub fn all_schema_file_associations(
|
||||||
.flat_map(|(_, glob_strings)| glob_strings)
|
.flat_map(|(_, glob_strings)| glob_strings)
|
||||||
.cloned();
|
.cloned();
|
||||||
let jsonc_globs = extension_globs.chain(override_globs).collect::<Vec<_>>();
|
let jsonc_globs = extension_globs.chain(override_globs).collect::<Vec<_>>();
|
||||||
|
let settings_file_matches = schema_file_match_entries(paths::settings_file());
|
||||||
|
let keymap_file_matches = schema_file_match_entries(paths::keymap_file());
|
||||||
|
let mut tasks_file_matches = schema_file_match_entries(paths::tasks_file());
|
||||||
|
tasks_file_matches.push(
|
||||||
|
paths::local_tasks_file_relative_path()
|
||||||
|
.as_unix_str()
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
let mut debug_file_matches = schema_file_match_entries(paths::debug_scenarios_file());
|
||||||
|
debug_file_matches.push(
|
||||||
|
paths::local_debug_file_relative_path()
|
||||||
|
.as_unix_str()
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
let snippet_file_matches =
|
||||||
|
schema_file_match_entries(paths::snippets_dir().join("*.json").as_path());
|
||||||
|
|
||||||
let mut file_associations = serde_json::json!([
|
let mut file_associations = serde_json::json!([
|
||||||
{
|
{
|
||||||
"fileMatch": [
|
"fileMatch": settings_file_matches,
|
||||||
schema_file_match(paths::settings_file()),
|
|
||||||
],
|
|
||||||
"url": format!("{SCHEMA_URI_PREFIX}settings"),
|
"url": format!("{SCHEMA_URI_PREFIX}settings"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fileMatch": [
|
"fileMatch": [
|
||||||
paths::local_settings_file_relative_path()],
|
paths::local_settings_file_relative_path()
|
||||||
|
],
|
||||||
"url": format!("{SCHEMA_URI_PREFIX}project_settings"),
|
"url": format!("{SCHEMA_URI_PREFIX}project_settings"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fileMatch": [schema_file_match(paths::keymap_file())],
|
"fileMatch": keymap_file_matches,
|
||||||
"url": format!("{SCHEMA_URI_PREFIX}keymap"),
|
"url": format!("{SCHEMA_URI_PREFIX}keymap"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fileMatch": [
|
"fileMatch": tasks_file_matches,
|
||||||
schema_file_match(paths::tasks_file()),
|
|
||||||
paths::local_tasks_file_relative_path()
|
|
||||||
],
|
|
||||||
"url": format!("{SCHEMA_URI_PREFIX}tasks"),
|
"url": format!("{SCHEMA_URI_PREFIX}tasks"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fileMatch": [
|
"fileMatch": debug_file_matches,
|
||||||
schema_file_match(paths::debug_scenarios_file()),
|
|
||||||
paths::local_debug_file_relative_path()
|
|
||||||
],
|
|
||||||
"url": format!("{SCHEMA_URI_PREFIX}debug_tasks"),
|
"url": format!("{SCHEMA_URI_PREFIX}debug_tasks"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fileMatch": [
|
"fileMatch": snippet_file_matches,
|
||||||
schema_file_match(
|
|
||||||
paths::snippets_dir()
|
|
||||||
.join("*.json")
|
|
||||||
.as_path()
|
|
||||||
)
|
|
||||||
],
|
|
||||||
"url": format!("{SCHEMA_URI_PREFIX}snippets"),
|
"url": format!("{SCHEMA_URI_PREFIX}snippets"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -619,11 +622,80 @@ fn root_schema_from_action_schema(
|
||||||
schema
|
schema
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build the LSP fileMatch entries for `path`.
|
||||||
|
///
|
||||||
|
/// The JSON LSP matches incoming file URIs against these glob patterns,
|
||||||
|
/// so we register both the symlinked location (if any) and the resolved
|
||||||
|
/// canonical path. Without this, opening `~/.config/zed/settings.json`
|
||||||
|
/// when it is a symlink to a file under a different directory no longer
|
||||||
|
/// binds the settings schema (zed-industries/zed#54888).
|
||||||
|
fn schema_file_match_entries(path: &std::path::Path) -> Vec<String> {
|
||||||
|
let mut out = Vec::with_capacity(2);
|
||||||
|
out.push(stripped_match(path));
|
||||||
|
if let Ok(canonical) = path.canonicalize() {
|
||||||
|
if canonical != path {
|
||||||
|
out.push(stripped_match(&canonical));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn schema_file_match(path: &std::path::Path) -> String {
|
fn stripped_match(path: &std::path::Path) -> String {
|
||||||
path.strip_prefix(path.parent().unwrap().parent().unwrap())
|
let parent = path.parent().and_then(|p| p.parent()).unwrap_or(path);
|
||||||
.unwrap()
|
path.strip_prefix(parent)
|
||||||
|
.unwrap_or(path)
|
||||||
.display()
|
.display()
|
||||||
.to_string()
|
.to_string()
|
||||||
.replace('\\', "/")
|
.replace('\\', "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stripped_match_drops_two_parent_components() {
|
||||||
|
let path = PathBuf::from("/home/user/.config/zed/settings.json");
|
||||||
|
assert_eq!(stripped_match(&path), "zed/settings.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn schema_file_match_entries_returns_single_for_regular_file() {
|
||||||
|
let tmp = tempfile::TempDir::new().unwrap();
|
||||||
|
// Some platforms expose the temp directory through a symlinked prefix.
|
||||||
|
// Canonicalize the root so this test only covers non-symlinked files.
|
||||||
|
let root = tmp.path().canonicalize().unwrap();
|
||||||
|
let zed_dir = root.join("zed");
|
||||||
|
fs::create_dir(&zed_dir).unwrap();
|
||||||
|
let regular = zed_dir.join("settings.json");
|
||||||
|
fs::write(®ular, "{}").unwrap();
|
||||||
|
let entries = schema_file_match_entries(®ular);
|
||||||
|
assert_eq!(entries.len(), 1);
|
||||||
|
assert_eq!(entries[0], "zed/settings.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn schema_file_match_entries_returns_both_for_symlink() {
|
||||||
|
let tmp = tempfile::TempDir::new().unwrap();
|
||||||
|
let zed_dir = tmp.path().join("zed");
|
||||||
|
fs::create_dir(&zed_dir).unwrap();
|
||||||
|
let target = tmp.path().join("settings_target.json");
|
||||||
|
fs::write(&target, "{}").unwrap();
|
||||||
|
let link = zed_dir.join("settings.json");
|
||||||
|
#[cfg(unix)]
|
||||||
|
std::os::unix::fs::symlink(&target, &link).unwrap();
|
||||||
|
#[cfg(windows)]
|
||||||
|
std::os::windows::fs::symlink_file(&target, &link).unwrap();
|
||||||
|
let entries = schema_file_match_entries(&link);
|
||||||
|
assert!(entries.iter().any(|entry| entry == "zed/settings.json"));
|
||||||
|
assert!(
|
||||||
|
entries
|
||||||
|
.iter()
|
||||||
|
.any(|entry| entry.ends_with("settings_target.json"))
|
||||||
|
);
|
||||||
|
assert_eq!(entries.len(), 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -297,6 +297,19 @@ pub enum Operation {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum BufferEditSource {
|
||||||
|
User,
|
||||||
|
Agent,
|
||||||
|
Remote,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BufferEditSource {
|
||||||
|
pub fn is_local(self) -> bool {
|
||||||
|
!matches!(self, Self::Remote)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// An event that occurs in a buffer.
|
/// An event that occurs in a buffer.
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub enum BufferEvent {
|
pub enum BufferEvent {
|
||||||
|
|
@ -307,7 +320,7 @@ pub enum BufferEvent {
|
||||||
is_local: bool,
|
is_local: bool,
|
||||||
},
|
},
|
||||||
/// The buffer was edited.
|
/// The buffer was edited.
|
||||||
Edited { is_local: bool },
|
Edited { source: BufferEditSource },
|
||||||
/// The buffer's `dirty` bit changed.
|
/// The buffer's `dirty` bit changed.
|
||||||
DirtyChanged,
|
DirtyChanged,
|
||||||
/// The buffer was saved.
|
/// The buffer was saved.
|
||||||
|
|
@ -2433,6 +2446,14 @@ impl Buffer {
|
||||||
self.end_transaction_at(Instant::now(), cx)
|
self.end_transaction_at(Instant::now(), cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn end_transaction_with_source(
|
||||||
|
&mut self,
|
||||||
|
source: BufferEditSource,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Option<TransactionId> {
|
||||||
|
self.end_transaction_at_internal(Instant::now(), source, cx)
|
||||||
|
}
|
||||||
|
|
||||||
/// Terminates the current transaction, providing the current time. Subsequent transactions
|
/// Terminates the current transaction, providing the current time. Subsequent transactions
|
||||||
/// that occur within a short period of time will be grouped together. This
|
/// that occur within a short period of time will be grouped together. This
|
||||||
/// is controlled by the buffer's undo grouping duration.
|
/// is controlled by the buffer's undo grouping duration.
|
||||||
|
|
@ -2440,6 +2461,15 @@ impl Buffer {
|
||||||
&mut self,
|
&mut self,
|
||||||
now: Instant,
|
now: Instant,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
|
) -> Option<TransactionId> {
|
||||||
|
self.end_transaction_at_internal(now, BufferEditSource::User, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end_transaction_at_internal(
|
||||||
|
&mut self,
|
||||||
|
now: Instant,
|
||||||
|
source: BufferEditSource,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
) -> Option<TransactionId> {
|
) -> Option<TransactionId> {
|
||||||
assert!(self.transaction_depth > 0);
|
assert!(self.transaction_depth > 0);
|
||||||
self.transaction_depth -= 1;
|
self.transaction_depth -= 1;
|
||||||
|
|
@ -2449,7 +2479,7 @@ impl Buffer {
|
||||||
false
|
false
|
||||||
};
|
};
|
||||||
if let Some((transaction_id, start_version)) = self.text.end_transaction_at(now) {
|
if let Some((transaction_id, start_version)) = self.text.end_transaction_at(now) {
|
||||||
self.did_edit(&start_version, was_dirty, true, cx);
|
self.did_edit(&start_version, was_dirty, source, cx);
|
||||||
Some(transaction_id)
|
Some(transaction_id)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|
@ -2844,7 +2874,7 @@ impl Buffer {
|
||||||
&mut self,
|
&mut self,
|
||||||
old_version: &clock::Global,
|
old_version: &clock::Global,
|
||||||
was_dirty: bool,
|
was_dirty: bool,
|
||||||
is_local: bool,
|
source: BufferEditSource,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
self.was_changed();
|
self.was_changed();
|
||||||
|
|
@ -2854,7 +2884,7 @@ impl Buffer {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.reparse(cx, true);
|
self.reparse(cx, true);
|
||||||
cx.emit(BufferEvent::Edited { is_local });
|
cx.emit(BufferEvent::Edited { source });
|
||||||
let is_dirty = self.is_dirty();
|
let is_dirty = self.is_dirty();
|
||||||
if was_dirty != is_dirty {
|
if was_dirty != is_dirty {
|
||||||
cx.emit(BufferEvent::DirtyChanged);
|
cx.emit(BufferEvent::DirtyChanged);
|
||||||
|
|
@ -2976,7 +3006,7 @@ impl Buffer {
|
||||||
self.text.apply_ops(buffer_ops);
|
self.text.apply_ops(buffer_ops);
|
||||||
self.deferred_ops.insert(deferred_ops);
|
self.deferred_ops.insert(deferred_ops);
|
||||||
self.flush_deferred_ops(cx);
|
self.flush_deferred_ops(cx);
|
||||||
self.did_edit(&old_version, was_dirty, false, cx);
|
self.did_edit(&old_version, was_dirty, BufferEditSource::Remote, cx);
|
||||||
// Notify independently of whether the buffer was edited as the operations could include a
|
// Notify independently of whether the buffer was edited as the operations could include a
|
||||||
// selection update.
|
// selection update.
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
|
@ -3131,7 +3161,7 @@ impl Buffer {
|
||||||
|
|
||||||
if let Some((transaction_id, operation)) = self.text.undo() {
|
if let Some((transaction_id, operation)) = self.text.undo() {
|
||||||
self.send_operation(Operation::Buffer(operation), true, cx);
|
self.send_operation(Operation::Buffer(operation), true, cx);
|
||||||
self.did_edit(&old_version, was_dirty, true, cx);
|
self.did_edit(&old_version, was_dirty, BufferEditSource::User, cx);
|
||||||
self.restore_encoding_for_transaction(transaction_id, was_dirty);
|
self.restore_encoding_for_transaction(transaction_id, was_dirty);
|
||||||
Some(transaction_id)
|
Some(transaction_id)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -3149,7 +3179,7 @@ impl Buffer {
|
||||||
let old_version = self.version.clone();
|
let old_version = self.version.clone();
|
||||||
if let Some(operation) = self.text.undo_transaction(transaction_id) {
|
if let Some(operation) = self.text.undo_transaction(transaction_id) {
|
||||||
self.send_operation(Operation::Buffer(operation), true, cx);
|
self.send_operation(Operation::Buffer(operation), true, cx);
|
||||||
self.did_edit(&old_version, was_dirty, true, cx);
|
self.did_edit(&old_version, was_dirty, BufferEditSource::User, cx);
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
|
|
@ -3171,7 +3201,7 @@ impl Buffer {
|
||||||
self.send_operation(Operation::Buffer(operation), true, cx);
|
self.send_operation(Operation::Buffer(operation), true, cx);
|
||||||
}
|
}
|
||||||
if undone {
|
if undone {
|
||||||
self.did_edit(&old_version, was_dirty, true, cx)
|
self.did_edit(&old_version, was_dirty, BufferEditSource::User, cx)
|
||||||
}
|
}
|
||||||
undone
|
undone
|
||||||
}
|
}
|
||||||
|
|
@ -3181,7 +3211,7 @@ impl Buffer {
|
||||||
let operation = self.text.undo_operations(counts);
|
let operation = self.text.undo_operations(counts);
|
||||||
let old_version = self.version.clone();
|
let old_version = self.version.clone();
|
||||||
self.send_operation(Operation::Buffer(operation), true, cx);
|
self.send_operation(Operation::Buffer(operation), true, cx);
|
||||||
self.did_edit(&old_version, was_dirty, true, cx);
|
self.did_edit(&old_version, was_dirty, BufferEditSource::User, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Manually redoes a specific transaction in the buffer's redo history.
|
/// Manually redoes a specific transaction in the buffer's redo history.
|
||||||
|
|
@ -3191,7 +3221,7 @@ impl Buffer {
|
||||||
|
|
||||||
if let Some((transaction_id, operation)) = self.text.redo() {
|
if let Some((transaction_id, operation)) = self.text.redo() {
|
||||||
self.send_operation(Operation::Buffer(operation), true, cx);
|
self.send_operation(Operation::Buffer(operation), true, cx);
|
||||||
self.did_edit(&old_version, was_dirty, true, cx);
|
self.did_edit(&old_version, was_dirty, BufferEditSource::User, cx);
|
||||||
self.restore_encoding_for_transaction(transaction_id, was_dirty);
|
self.restore_encoding_for_transaction(transaction_id, was_dirty);
|
||||||
Some(transaction_id)
|
Some(transaction_id)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -3232,7 +3262,7 @@ impl Buffer {
|
||||||
self.send_operation(Operation::Buffer(operation), true, cx);
|
self.send_operation(Operation::Buffer(operation), true, cx);
|
||||||
}
|
}
|
||||||
if redone {
|
if redone {
|
||||||
self.did_edit(&old_version, was_dirty, true, cx)
|
self.did_edit(&old_version, was_dirty, BufferEditSource::User, cx)
|
||||||
}
|
}
|
||||||
redone
|
redone
|
||||||
}
|
}
|
||||||
|
|
@ -3342,7 +3372,7 @@ impl Buffer {
|
||||||
if !ops.is_empty() {
|
if !ops.is_empty() {
|
||||||
for op in ops {
|
for op in ops {
|
||||||
self.send_operation(Operation::Buffer(op), true, cx);
|
self.send_operation(Operation::Buffer(op), true, cx);
|
||||||
self.did_edit(&old_version, was_dirty, true, cx);
|
self.did_edit(&old_version, was_dirty, BufferEditSource::User, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -460,16 +460,24 @@ fn test_edit_events(cx: &mut gpui::App) {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
mem::take(&mut *buffer_1_events.lock()),
|
mem::take(&mut *buffer_1_events.lock()),
|
||||||
vec![
|
vec![
|
||||||
BufferEvent::Edited { is_local: true },
|
BufferEvent::Edited {
|
||||||
|
source: BufferEditSource::User
|
||||||
|
},
|
||||||
BufferEvent::DirtyChanged,
|
BufferEvent::DirtyChanged,
|
||||||
BufferEvent::Edited { is_local: true },
|
BufferEvent::Edited {
|
||||||
BufferEvent::Edited { is_local: true },
|
source: BufferEditSource::User
|
||||||
|
},
|
||||||
|
BufferEvent::Edited {
|
||||||
|
source: BufferEditSource::User
|
||||||
|
},
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
mem::take(&mut *buffer_2_events.lock()),
|
mem::take(&mut *buffer_2_events.lock()),
|
||||||
vec![
|
vec![
|
||||||
BufferEvent::Edited { is_local: false },
|
BufferEvent::Edited {
|
||||||
|
source: BufferEditSource::Remote
|
||||||
|
},
|
||||||
BufferEvent::DirtyChanged
|
BufferEvent::DirtyChanged
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
@ -487,14 +495,18 @@ fn test_edit_events(cx: &mut gpui::App) {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
mem::take(&mut *buffer_1_events.lock()),
|
mem::take(&mut *buffer_1_events.lock()),
|
||||||
vec![
|
vec![
|
||||||
BufferEvent::Edited { is_local: true },
|
BufferEvent::Edited {
|
||||||
|
source: BufferEditSource::User
|
||||||
|
},
|
||||||
BufferEvent::DirtyChanged,
|
BufferEvent::DirtyChanged,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
mem::take(&mut *buffer_2_events.lock()),
|
mem::take(&mut *buffer_2_events.lock()),
|
||||||
vec![
|
vec![
|
||||||
BufferEvent::Edited { is_local: false },
|
BufferEvent::Edited {
|
||||||
|
source: BufferEditSource::Remote
|
||||||
|
},
|
||||||
BufferEvent::DirtyChanged
|
BufferEvent::DirtyChanged
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -670,12 +670,22 @@ impl LanguageModel for BedrockModel {
|
||||||
value: "high".into(),
|
value: "high".into(),
|
||||||
is_default: true,
|
is_default: true,
|
||||||
},
|
},
|
||||||
|
language_model::LanguageModelEffortLevel {
|
||||||
|
name: "XHigh".into(),
|
||||||
|
value: "xhigh".into(),
|
||||||
|
is_default: false,
|
||||||
|
},
|
||||||
language_model::LanguageModelEffortLevel {
|
language_model::LanguageModelEffortLevel {
|
||||||
name: "Max".into(),
|
name: "Max".into(),
|
||||||
value: "max".into(),
|
value: "max".into(),
|
||||||
is_default: false,
|
is_default: false,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
.into_iter()
|
||||||
|
.filter(|effort_level| {
|
||||||
|
effort_level.value != "xhigh" || self.model.supports_xhigh_adaptive_thinking()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
|
|
@ -1128,6 +1138,7 @@ pub fn into_bedrock(
|
||||||
"low" => Some(bedrock::BedrockAdaptiveThinkingEffort::Low),
|
"low" => Some(bedrock::BedrockAdaptiveThinkingEffort::Low),
|
||||||
"medium" => Some(bedrock::BedrockAdaptiveThinkingEffort::Medium),
|
"medium" => Some(bedrock::BedrockAdaptiveThinkingEffort::Medium),
|
||||||
"high" => Some(bedrock::BedrockAdaptiveThinkingEffort::High),
|
"high" => Some(bedrock::BedrockAdaptiveThinkingEffort::High),
|
||||||
|
"xhigh" => Some(bedrock::BedrockAdaptiveThinkingEffort::XHigh),
|
||||||
"max" => Some(bedrock::BedrockAdaptiveThinkingEffort::Max),
|
"max" => Some(bedrock::BedrockAdaptiveThinkingEffort::Max),
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -721,7 +721,13 @@ impl Component for ZedAiConfiguration {
|
||||||
ComponentScope::Onboarding
|
ComponentScope::Onboarding
|
||||||
}
|
}
|
||||||
|
|
||||||
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
|
fn description() -> &'static str {
|
||||||
|
"The configuration surface for Zed's hosted AI models, \
|
||||||
|
showing the user's connection status, current plan, trial eligibility, \
|
||||||
|
and entry points for enabling the Zed model provider."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
|
||||||
struct PreviewConfiguration {
|
struct PreviewConfiguration {
|
||||||
plan: Option<Plan>,
|
plan: Option<Plan>,
|
||||||
is_connected: bool,
|
is_connected: bool,
|
||||||
|
|
@ -741,7 +747,6 @@ impl Component for ZedAiConfiguration {
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.p_4()
|
.p_4()
|
||||||
.gap_4()
|
.gap_4()
|
||||||
|
|
@ -828,7 +833,6 @@ impl Component for ZedAiConfiguration {
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
])
|
])
|
||||||
.into_any_element(),
|
.into_any_element()
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -773,7 +773,6 @@ impl CopilotResponsesEventMapper {
|
||||||
|
|
||||||
copilot_responses::StreamEvent::Completed { response } => {
|
copilot_responses::StreamEvent::Completed { response } => {
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
events.extend(self.capture_reasoning_items_from_output(&response.output));
|
|
||||||
if let Some(usage) = response.usage {
|
if let Some(usage) = response.usage {
|
||||||
events.push(Ok(LanguageModelCompletionEvent::UsageUpdate(TokenUsage {
|
events.push(Ok(LanguageModelCompletionEvent::UsageUpdate(TokenUsage {
|
||||||
input_tokens: usage.input_tokens.unwrap_or(0),
|
input_tokens: usage.input_tokens.unwrap_or(0),
|
||||||
|
|
@ -805,7 +804,6 @@ impl CopilotResponsesEventMapper {
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
events.extend(self.capture_reasoning_items_from_output(&response.output));
|
|
||||||
if let Some(usage) = response.usage {
|
if let Some(usage) = response.usage {
|
||||||
events.push(Ok(LanguageModelCompletionEvent::UsageUpdate(TokenUsage {
|
events.push(Ok(LanguageModelCompletionEvent::UsageUpdate(TokenUsage {
|
||||||
input_tokens: usage.input_tokens.unwrap_or(0),
|
input_tokens: usage.input_tokens.unwrap_or(0),
|
||||||
|
|
@ -847,28 +845,6 @@ impl CopilotResponsesEventMapper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn capture_reasoning_items_from_output(
|
|
||||||
&mut self,
|
|
||||||
output: &[copilot_responses::ResponseOutputItem],
|
|
||||||
) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
|
|
||||||
let mut events = Vec::new();
|
|
||||||
for item in output {
|
|
||||||
if let copilot_responses::ResponseOutputItem::Reasoning {
|
|
||||||
id,
|
|
||||||
summary: _,
|
|
||||||
encrypted_content,
|
|
||||||
} = item
|
|
||||||
{
|
|
||||||
if let Some(reasoning_item) =
|
|
||||||
reasoning_input_item_from_output(&id, encrypted_content.clone())
|
|
||||||
{
|
|
||||||
events.extend(self.capture_reasoning_item(reasoning_item));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
events
|
|
||||||
}
|
|
||||||
|
|
||||||
fn capture_reasoning_item(
|
fn capture_reasoning_item(
|
||||||
&mut self,
|
&mut self,
|
||||||
reasoning_item: copilot_responses::ResponseReasoningInputItem,
|
reasoning_item: copilot_responses::ResponseReasoningInputItem,
|
||||||
|
|
@ -1597,6 +1573,60 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn responses_stream_ignores_reasoning_items_repeated_in_completed_output() {
|
||||||
|
let events = vec![
|
||||||
|
responses::StreamEvent::OutputItemDone {
|
||||||
|
output_index: 0,
|
||||||
|
sequence_number: None,
|
||||||
|
item: responses::ResponseOutputItem::Reasoning {
|
||||||
|
id: "r1".into(),
|
||||||
|
summary: Some(Vec::new()),
|
||||||
|
encrypted_content: Some("ENC1".into()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
responses::StreamEvent::Completed {
|
||||||
|
response: responses::Response {
|
||||||
|
output: vec![
|
||||||
|
responses::ResponseOutputItem::Reasoning {
|
||||||
|
id: "r1".into(),
|
||||||
|
summary: Some(Vec::new()),
|
||||||
|
encrypted_content: Some("ENC1".into()),
|
||||||
|
},
|
||||||
|
responses::ResponseOutputItem::Reasoning {
|
||||||
|
id: "r2".into(),
|
||||||
|
summary: Some(Vec::new()),
|
||||||
|
encrypted_content: Some("ENC2".into()),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let mapped = map_events(events);
|
||||||
|
let reasoning_details = mapped
|
||||||
|
.iter()
|
||||||
|
.filter_map(|event| match event {
|
||||||
|
LanguageModelCompletionEvent::ReasoningDetails(details) => Some(details),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
reasoning_details,
|
||||||
|
vec![&json!({
|
||||||
|
"reasoning_items": [
|
||||||
|
{
|
||||||
|
"id": "r1",
|
||||||
|
"summary": [],
|
||||||
|
"encrypted_content": "ENC1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn into_copilot_responses_replays_reasoning_details() {
|
fn into_copilot_responses_replays_reasoning_details() {
|
||||||
let model = test_responses_model();
|
let model = test_responses_model();
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ fn reasoning_effort_display(effort: ReasoningEffort) -> (&'static str, &'static
|
||||||
ReasoningEffort::Low => ("Low", "low"),
|
ReasoningEffort::Low => ("Low", "low"),
|
||||||
ReasoningEffort::Medium => ("Medium", "medium"),
|
ReasoningEffort::Medium => ("Medium", "medium"),
|
||||||
ReasoningEffort::High => ("High", "high"),
|
ReasoningEffort::High => ("High", "high"),
|
||||||
ReasoningEffort::XHigh => ("Max", "max"),
|
ReasoningEffort::XHigh => ("XHigh", "xhigh"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,10 @@ use gpui::{AnyView, App, AsyncApp, Context, Entity, Task, TaskExt, Window};
|
||||||
use http_client::HttpClient;
|
use http_client::HttpClient;
|
||||||
use language_model::{
|
use language_model::{
|
||||||
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
|
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
|
||||||
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
|
LanguageModelCompletionEvent, LanguageModelEffortLevel, LanguageModelId, LanguageModelName,
|
||||||
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
|
||||||
LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolSchemaFormat, RateLimiter,
|
LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice,
|
||||||
env_var,
|
LanguageModelToolSchemaFormat, RateLimiter, env_var,
|
||||||
};
|
};
|
||||||
use open_ai::ResponseStreamEvent;
|
use open_ai::ResponseStreamEvent;
|
||||||
pub use settings::XaiAvailableModel as AvailableModel;
|
pub use settings::XaiAvailableModel as AvailableModel;
|
||||||
|
|
@ -255,6 +255,75 @@ impl XAiLanguageModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn x_ai_reasoning_efforts(model: &x_ai::Model) -> &'static [open_ai::ReasoningEffort] {
|
||||||
|
if model.supports_reasoning_effort() {
|
||||||
|
&[
|
||||||
|
open_ai::ReasoningEffort::None,
|
||||||
|
open_ai::ReasoningEffort::Low,
|
||||||
|
open_ai::ReasoningEffort::Medium,
|
||||||
|
open_ai::ReasoningEffort::High,
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
&[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_thinking_reasoning_effort(model: &x_ai::Model) -> Option<open_ai::ReasoningEffort> {
|
||||||
|
if model.supports_reasoning_effort() {
|
||||||
|
Some(open_ai::ReasoningEffort::Low)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reasoning_effort_for_request(
|
||||||
|
request: &LanguageModelRequest,
|
||||||
|
model: &x_ai::Model,
|
||||||
|
) -> Option<open_ai::ReasoningEffort> {
|
||||||
|
let supported_efforts = x_ai_reasoning_efforts(model);
|
||||||
|
if supported_efforts.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if request.thinking_allowed {
|
||||||
|
request
|
||||||
|
.thinking_effort
|
||||||
|
.as_deref()
|
||||||
|
.and_then(|effort| effort.parse::<open_ai::ReasoningEffort>().ok())
|
||||||
|
.filter(|effort| supported_efforts.contains(effort))
|
||||||
|
.filter(|effort| *effort != open_ai::ReasoningEffort::None)
|
||||||
|
.or_else(|| default_thinking_reasoning_effort(model))
|
||||||
|
} else if supported_efforts.contains(&open_ai::ReasoningEffort::None) {
|
||||||
|
Some(open_ai::ReasoningEffort::None)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supported_thinking_effort_levels(model: &x_ai::Model) -> Vec<LanguageModelEffortLevel> {
|
||||||
|
let default_effort = default_thinking_reasoning_effort(model);
|
||||||
|
x_ai_reasoning_efforts(model)
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter_map(|effort| {
|
||||||
|
let (name, value) = match effort {
|
||||||
|
open_ai::ReasoningEffort::None => return None,
|
||||||
|
open_ai::ReasoningEffort::Minimal => ("Minimal", "minimal"),
|
||||||
|
open_ai::ReasoningEffort::Low => ("Low", "low"),
|
||||||
|
open_ai::ReasoningEffort::Medium => ("Medium", "medium"),
|
||||||
|
open_ai::ReasoningEffort::High => ("High", "high"),
|
||||||
|
open_ai::ReasoningEffort::XHigh => ("Extra High", "xhigh"),
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(LanguageModelEffortLevel {
|
||||||
|
name: name.into(),
|
||||||
|
value: value.into(),
|
||||||
|
is_default: Some(effort) == default_effort,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
impl LanguageModel for XAiLanguageModel {
|
impl LanguageModel for XAiLanguageModel {
|
||||||
fn id(&self) -> LanguageModelId {
|
fn id(&self) -> LanguageModelId {
|
||||||
self.id.clone()
|
self.id.clone()
|
||||||
|
|
@ -291,6 +360,15 @@ impl LanguageModel for XAiLanguageModel {
|
||||||
| LanguageModelToolChoice::None => true,
|
| LanguageModelToolChoice::None => true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn supports_thinking(&self) -> bool {
|
||||||
|
self.model.supports_reasoning_effort()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supported_effort_levels(&self) -> Vec<LanguageModelEffortLevel> {
|
||||||
|
supported_thinking_effort_levels(&self.model)
|
||||||
|
}
|
||||||
|
|
||||||
fn tool_input_format(&self) -> LanguageModelToolSchemaFormat {
|
fn tool_input_format(&self) -> LanguageModelToolSchemaFormat {
|
||||||
if self.model.requires_json_schema_subset() {
|
if self.model.requires_json_schema_subset() {
|
||||||
LanguageModelToolSchemaFormat::JsonSchemaSubset
|
LanguageModelToolSchemaFormat::JsonSchemaSubset
|
||||||
|
|
@ -329,13 +407,14 @@ impl LanguageModel for XAiLanguageModel {
|
||||||
LanguageModelCompletionError,
|
LanguageModelCompletionError,
|
||||||
>,
|
>,
|
||||||
> {
|
> {
|
||||||
|
let reasoning_effort = reasoning_effort_for_request(&request, &self.model);
|
||||||
let request = crate::provider::open_ai::into_open_ai(
|
let request = crate::provider::open_ai::into_open_ai(
|
||||||
request,
|
request,
|
||||||
self.model.id(),
|
self.model.id(),
|
||||||
self.model.supports_parallel_tool_calls(),
|
self.model.supports_parallel_tool_calls(),
|
||||||
self.model.supports_prompt_cache_key(),
|
self.model.supports_prompt_cache_key(),
|
||||||
self.max_output_tokens(),
|
self.max_output_tokens(),
|
||||||
None,
|
reasoning_effort,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
let completions = self.stream_completion(request, cx);
|
let completions = self.stream_completion(request, cx);
|
||||||
|
|
@ -428,6 +507,56 @@ impl ConfigurationView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grok_43_supports_selectable_thinking_effort_levels() {
|
||||||
|
let effort_levels = supported_thinking_effort_levels(&x_ai::Model::Grok43);
|
||||||
|
let values = effort_levels
|
||||||
|
.iter()
|
||||||
|
.map(|level| level.value.as_ref())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
assert_eq!(values, ["low", "medium", "high"]);
|
||||||
|
assert_eq!(
|
||||||
|
effort_levels
|
||||||
|
.iter()
|
||||||
|
.find(|level| level.is_default)
|
||||||
|
.map(|level| level.value.as_ref()),
|
||||||
|
Some("low")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grok_43_request_uses_selected_reasoning_effort() {
|
||||||
|
let request = LanguageModelRequest {
|
||||||
|
thinking_allowed: true,
|
||||||
|
thinking_effort: Some("high".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
reasoning_effort_for_request(&request, &x_ai::Model::Grok43),
|
||||||
|
Some(open_ai::ReasoningEffort::High)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grok_43_request_uses_none_when_thinking_is_disabled() {
|
||||||
|
let request = LanguageModelRequest {
|
||||||
|
thinking_allowed: false,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
reasoning_effort_for_request(&request, &x_ai::Model::Grok43),
|
||||||
|
Some(open_ai::ReasoningEffort::None)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Render for ConfigurationView {
|
impl Render for ConfigurationView {
|
||||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let env_var_set = self.state.read(cx).api_key_state.is_from_env_var();
|
let env_var_set = self.state.read(cx).api_key_state.is_from_env_var();
|
||||||
|
|
|
||||||
|
|
@ -49,8 +49,27 @@ pub(crate) struct AudioStack {
|
||||||
|
|
||||||
impl AudioStack {
|
impl AudioStack {
|
||||||
pub(crate) fn new(executor: BackgroundExecutor) -> Self {
|
pub(crate) fn new(executor: BackgroundExecutor) -> Self {
|
||||||
|
// AGC2's `adaptive_digital` is what actually levels speech toward a target;
|
||||||
|
// the `gain_controller2.enabled` master switch alone leaves it off, which
|
||||||
|
// historically meant capture was effectively unleveled. Defaults match
|
||||||
|
// what Chrome/Meet ship with -- in particular `max_gain_db = 50` paired
|
||||||
|
// with `max_output_noise_level_dbfs = -50`, which lets the AGC reach
|
||||||
|
// very quiet talkers while the noise-level estimator backs off before
|
||||||
|
// boosting amplifies the noise floor.
|
||||||
let apm = Arc::new(Mutex::new(apm::AudioProcessingModule::new(
|
let apm = Arc::new(Mutex::new(apm::AudioProcessingModule::new(
|
||||||
true, true, true, true,
|
apm::AudioProcessingConfig {
|
||||||
|
echo_canceller_enabled: true,
|
||||||
|
gain_controller2: apm::GainController2Config {
|
||||||
|
enabled: true,
|
||||||
|
adaptive_digital: apm::AdaptiveDigitalConfig {
|
||||||
|
enabled: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
high_pass_filter_enabled: true,
|
||||||
|
noise_suppression_enabled: true,
|
||||||
|
},
|
||||||
)));
|
)));
|
||||||
let mixer = Arc::new(Mutex::new(audio_mixer::AudioMixer::new()));
|
let mixer = Arc::new(Mutex::new(audio_mixer::AudioMixer::new()));
|
||||||
Self {
|
Self {
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,8 @@ use gpui::{
|
||||||
use language::{CharClassifier, Language, LanguageRegistry, Rope};
|
use language::{CharClassifier, Language, LanguageRegistry, Rope};
|
||||||
use parser::CodeBlockMetadata;
|
use parser::CodeBlockMetadata;
|
||||||
use parser::{
|
use parser::{
|
||||||
MarkdownEvent, MarkdownTag, MarkdownTagEnd, parse_links_only, parse_markdown_with_options,
|
MarkdownEvent, MarkdownTag, MarkdownTagEnd, ParsedMetadataBlock, parse_links_only,
|
||||||
|
parse_markdown_with_options,
|
||||||
};
|
};
|
||||||
use pulldown_cmark::{Alignment, BlockQuoteKind};
|
use pulldown_cmark::{Alignment, BlockQuoteKind};
|
||||||
use sum_tree::TreeMap;
|
use sum_tree::TreeMap;
|
||||||
|
|
@ -350,6 +351,7 @@ pub struct MarkdownOptions {
|
||||||
pub parse_html: bool,
|
pub parse_html: bool,
|
||||||
pub render_mermaid_diagrams: bool,
|
pub render_mermaid_diagrams: bool,
|
||||||
pub parse_heading_slugs: bool,
|
pub parse_heading_slugs: bool,
|
||||||
|
pub render_metadata_blocks: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
|
|
@ -847,6 +849,7 @@ impl Markdown {
|
||||||
let should_parse_html = self.options.parse_html;
|
let should_parse_html = self.options.parse_html;
|
||||||
let should_render_mermaid_diagrams = self.options.render_mermaid_diagrams;
|
let should_render_mermaid_diagrams = self.options.render_mermaid_diagrams;
|
||||||
let should_parse_heading_slugs = self.options.parse_heading_slugs;
|
let should_parse_heading_slugs = self.options.parse_heading_slugs;
|
||||||
|
let should_parse_metadata_blocks = self.options.render_metadata_blocks;
|
||||||
let language_registry = self.language_registry.clone();
|
let language_registry = self.language_registry.clone();
|
||||||
let fallback = self.fallback_code_block_language.clone();
|
let fallback = self.fallback_code_block_language.clone();
|
||||||
|
|
||||||
|
|
@ -860,6 +863,7 @@ impl Markdown {
|
||||||
languages_by_path: TreeMap::default(),
|
languages_by_path: TreeMap::default(),
|
||||||
root_block_starts: Arc::default(),
|
root_block_starts: Arc::default(),
|
||||||
html_blocks: BTreeMap::default(),
|
html_blocks: BTreeMap::default(),
|
||||||
|
metadata_blocks: BTreeMap::default(),
|
||||||
mermaid_diagrams: BTreeMap::default(),
|
mermaid_diagrams: BTreeMap::default(),
|
||||||
heading_slugs: HashMap::default(),
|
heading_slugs: HashMap::default(),
|
||||||
footnote_definitions: HashMap::default(),
|
footnote_definitions: HashMap::default(),
|
||||||
|
|
@ -868,13 +872,18 @@ impl Markdown {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let parsed =
|
let parsed = parse_markdown_with_options(
|
||||||
parse_markdown_with_options(&source, should_parse_html, should_parse_heading_slugs);
|
&source,
|
||||||
|
should_parse_html,
|
||||||
|
should_parse_heading_slugs,
|
||||||
|
should_parse_metadata_blocks,
|
||||||
|
);
|
||||||
let events = parsed.events;
|
let events = parsed.events;
|
||||||
let language_names = parsed.language_names;
|
let language_names = parsed.language_names;
|
||||||
let paths = parsed.language_paths;
|
let paths = parsed.language_paths;
|
||||||
let root_block_starts = parsed.root_block_starts;
|
let root_block_starts = parsed.root_block_starts;
|
||||||
let html_blocks = parsed.html_blocks;
|
let html_blocks = parsed.html_blocks;
|
||||||
|
let metadata_blocks = parsed.metadata_blocks;
|
||||||
let heading_slugs = parsed.heading_slugs;
|
let heading_slugs = parsed.heading_slugs;
|
||||||
let footnote_definitions = parsed.footnote_definitions;
|
let footnote_definitions = parsed.footnote_definitions;
|
||||||
let mermaid_diagrams = if should_render_mermaid_diagrams {
|
let mermaid_diagrams = if should_render_mermaid_diagrams {
|
||||||
|
|
@ -942,6 +951,7 @@ impl Markdown {
|
||||||
languages_by_path,
|
languages_by_path,
|
||||||
root_block_starts: Arc::from(root_block_starts),
|
root_block_starts: Arc::from(root_block_starts),
|
||||||
html_blocks,
|
html_blocks,
|
||||||
|
metadata_blocks,
|
||||||
mermaid_diagrams,
|
mermaid_diagrams,
|
||||||
heading_slugs,
|
heading_slugs,
|
||||||
footnote_definitions,
|
footnote_definitions,
|
||||||
|
|
@ -1070,6 +1080,7 @@ pub struct ParsedMarkdown {
|
||||||
pub languages_by_path: TreeMap<Arc<str>, Arc<Language>>,
|
pub languages_by_path: TreeMap<Arc<str>, Arc<Language>>,
|
||||||
pub root_block_starts: Arc<[usize]>,
|
pub root_block_starts: Arc<[usize]>,
|
||||||
pub(crate) html_blocks: BTreeMap<usize, html::html_parser::ParsedHtmlBlock>,
|
pub(crate) html_blocks: BTreeMap<usize, html::html_parser::ParsedHtmlBlock>,
|
||||||
|
pub(crate) metadata_blocks: BTreeMap<usize, ParsedMetadataBlock>,
|
||||||
pub(crate) mermaid_diagrams: BTreeMap<usize, ParsedMarkdownMermaidDiagram>,
|
pub(crate) mermaid_diagrams: BTreeMap<usize, ParsedMarkdownMermaidDiagram>,
|
||||||
pub heading_slugs: HashMap<SharedString, usize>,
|
pub heading_slugs: HashMap<SharedString, usize>,
|
||||||
pub footnote_definitions: HashMap<SharedString, usize>,
|
pub footnote_definitions: HashMap<SharedString, usize>,
|
||||||
|
|
@ -1398,6 +1409,114 @@ impl MarkdownElement {
|
||||||
builder.pop_text_style();
|
builder.pop_text_style();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn push_metadata_block(
|
||||||
|
&self,
|
||||||
|
builder: &mut MarkdownElementBuilder,
|
||||||
|
source: &str,
|
||||||
|
metadata_block: &ParsedMetadataBlock,
|
||||||
|
markdown_end: usize,
|
||||||
|
cx: &App,
|
||||||
|
) {
|
||||||
|
let content_range = &metadata_block.content_range;
|
||||||
|
if let Some(rows) = metadata_block.rows.as_deref() {
|
||||||
|
builder.push_div(
|
||||||
|
div()
|
||||||
|
.grid()
|
||||||
|
.grid_cols(2)
|
||||||
|
.w_full()
|
||||||
|
.mb_2()
|
||||||
|
.border_1()
|
||||||
|
.border_color(cx.theme().colors().border)
|
||||||
|
.rounded_sm()
|
||||||
|
.overflow_hidden(),
|
||||||
|
content_range,
|
||||||
|
markdown_end,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (row_index, row) in rows.iter().enumerate() {
|
||||||
|
self.push_metadata_cell(
|
||||||
|
builder,
|
||||||
|
source,
|
||||||
|
row.key.clone(),
|
||||||
|
content_range,
|
||||||
|
markdown_end,
|
||||||
|
MetadataCellStyle {
|
||||||
|
row_index,
|
||||||
|
is_key: true,
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
self.push_metadata_cell(
|
||||||
|
builder,
|
||||||
|
source,
|
||||||
|
row.value.clone(),
|
||||||
|
content_range,
|
||||||
|
markdown_end,
|
||||||
|
MetadataCellStyle {
|
||||||
|
row_index,
|
||||||
|
is_key: false,
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.pop_div();
|
||||||
|
} else {
|
||||||
|
let mut metadata_block = div().w_full().rounded_md();
|
||||||
|
metadata_block.style().refine(&self.style.code_block);
|
||||||
|
builder.push_text_style(self.style.code_block.text.to_owned());
|
||||||
|
builder.push_code_block(None);
|
||||||
|
builder.push_div(metadata_block, content_range, markdown_end);
|
||||||
|
builder.push_text(&source[content_range.clone()], content_range.clone());
|
||||||
|
builder.trim_trailing_newline();
|
||||||
|
builder.pop_div();
|
||||||
|
builder.pop_code_block();
|
||||||
|
builder.pop_text_style();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_metadata_cell(
|
||||||
|
&self,
|
||||||
|
builder: &mut MarkdownElementBuilder,
|
||||||
|
source: &str,
|
||||||
|
text_range: Range<usize>,
|
||||||
|
block_range: &Range<usize>,
|
||||||
|
markdown_end: usize,
|
||||||
|
cell_style: MetadataCellStyle,
|
||||||
|
cx: &App,
|
||||||
|
) {
|
||||||
|
builder.push_div(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.min_w_0()
|
||||||
|
.px_2()
|
||||||
|
.py_1()
|
||||||
|
.border_color(cx.theme().colors().border)
|
||||||
|
.when(cell_style.row_index > 0, |this| this.border_t_1())
|
||||||
|
.when(!cell_style.is_key, |this| this.border_l_1())
|
||||||
|
.when(cell_style.is_key, |this| {
|
||||||
|
this.bg(cx.theme().colors().panel_background)
|
||||||
|
}),
|
||||||
|
block_range,
|
||||||
|
markdown_end,
|
||||||
|
);
|
||||||
|
|
||||||
|
let text_style = if cell_style.is_key {
|
||||||
|
TextStyleRefinement {
|
||||||
|
color: Some(cx.theme().colors().text_muted),
|
||||||
|
font_weight: Some(FontWeight::SEMIBOLD),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
TextStyleRefinement::default()
|
||||||
|
};
|
||||||
|
builder.push_text_style(text_style);
|
||||||
|
builder.push_text(&source[text_range.clone()], text_range);
|
||||||
|
builder.pop_text_style();
|
||||||
|
builder.pop_div();
|
||||||
|
}
|
||||||
|
|
||||||
fn push_markdown_list_item(
|
fn push_markdown_list_item(
|
||||||
&self,
|
&self,
|
||||||
builder: &mut MarkdownElementBuilder,
|
builder: &mut MarkdownElementBuilder,
|
||||||
|
|
@ -1809,6 +1928,7 @@ impl Element for MarkdownElement {
|
||||||
let mut current_img_block_range: Option<Range<usize>> = None;
|
let mut current_img_block_range: Option<Range<usize>> = None;
|
||||||
let mut handled_html_block = false;
|
let mut handled_html_block = false;
|
||||||
let mut rendered_mermaid_block = false;
|
let mut rendered_mermaid_block = false;
|
||||||
|
let mut rendered_metadata_block = false;
|
||||||
for (index, (range, event)) in parsed_markdown.events.iter().enumerate() {
|
for (index, (range, event)) in parsed_markdown.events.iter().enumerate() {
|
||||||
// Skip alt text for images that rendered
|
// Skip alt text for images that rendered
|
||||||
if let Some(current_img_block_range) = ¤t_img_block_range
|
if let Some(current_img_block_range) = ¤t_img_block_range
|
||||||
|
|
@ -1832,6 +1952,13 @@ impl Element for MarkdownElement {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if rendered_metadata_block {
|
||||||
|
if matches!(event, MarkdownEvent::End(MarkdownTagEnd::MetadataBlock(_))) {
|
||||||
|
rendered_metadata_block = false;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
MarkdownEvent::RootStart => {
|
MarkdownEvent::RootStart => {
|
||||||
if self.show_root_block_markers {
|
if self.show_root_block_markers {
|
||||||
|
|
@ -2147,7 +2274,20 @@ impl Element for MarkdownElement {
|
||||||
);
|
);
|
||||||
builder.push_div(div().flex_1().w_0(), range, markdown_end);
|
builder.push_div(div().flex_1().w_0(), range, markdown_end);
|
||||||
}
|
}
|
||||||
MarkdownTag::MetadataBlock(_) => {}
|
MarkdownTag::MetadataBlock(_) => {
|
||||||
|
if let Some(metadata_block) =
|
||||||
|
parsed_markdown.metadata_blocks.get(&range.start)
|
||||||
|
{
|
||||||
|
self.push_metadata_block(
|
||||||
|
&mut builder,
|
||||||
|
&parsed_markdown.source,
|
||||||
|
metadata_block,
|
||||||
|
markdown_end,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
rendered_metadata_block = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
MarkdownTag::Table(alignments) => {
|
MarkdownTag::Table(alignments) => {
|
||||||
builder.table.start(alignments.clone());
|
builder.table.start(alignments.clone());
|
||||||
|
|
||||||
|
|
@ -2359,6 +2499,7 @@ impl Element for MarkdownElement {
|
||||||
builder.pop_div();
|
builder.pop_div();
|
||||||
builder.pop_div();
|
builder.pop_div();
|
||||||
}
|
}
|
||||||
|
MarkdownTagEnd::MetadataBlock(_) => {}
|
||||||
_ => log::debug!("unsupported markdown tag end: {:?}", tag),
|
_ => log::debug!("unsupported markdown tag end: {:?}", tag),
|
||||||
},
|
},
|
||||||
MarkdownEvent::Text => {
|
MarkdownEvent::Text => {
|
||||||
|
|
@ -2752,6 +2893,11 @@ fn alignment_to_text_align(alignment: Alignment) -> Option<TextAlign> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct MetadataCellStyle {
|
||||||
|
row_index: usize,
|
||||||
|
is_key: bool,
|
||||||
|
}
|
||||||
|
|
||||||
struct MarkdownElementBuilder {
|
struct MarkdownElementBuilder {
|
||||||
div_stack: Vec<AnyDiv>,
|
div_stack: Vec<AnyDiv>,
|
||||||
rendered_lines: Vec<RenderedLine>,
|
rendered_lines: Vec<RenderedLine>,
|
||||||
|
|
@ -3586,6 +3732,34 @@ mod tests {
|
||||||
render_markdown_with_language_registry(markdown, None, cx)
|
render_markdown_with_language_registry(markdown, None, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn test_frontmatter_renders_without_delimiters(cx: &mut TestAppContext) {
|
||||||
|
let rendered = render_markdown_with_options(
|
||||||
|
"---\ntitle: Post\n---\nBody",
|
||||||
|
None,
|
||||||
|
MarkdownOptions {
|
||||||
|
render_metadata_blocks: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
assert_eq!(rendered.text_for_range(0..24), "title\nPost\nBody");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn test_frontmatter_falls_back_to_code_block_for_nested_yaml(cx: &mut TestAppContext) {
|
||||||
|
let rendered = render_markdown_with_options(
|
||||||
|
"---\ntags:\n - zed\n---\nBody",
|
||||||
|
None,
|
||||||
|
MarkdownOptions {
|
||||||
|
render_metadata_blocks: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
assert_eq!(rendered.text_for_range(0..26), "tags:\n - zed\nBody");
|
||||||
|
}
|
||||||
|
|
||||||
fn render_markdown_with_code_span_link(
|
fn render_markdown_with_code_span_link(
|
||||||
markdown: &str,
|
markdown: &str,
|
||||||
callback: impl Fn(&str, &App) -> Option<SharedString> + 'static,
|
callback: impl Fn(&str, &App) -> Option<SharedString> + 'static,
|
||||||
|
|
@ -3873,7 +4047,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_table_checkbox_detection() {
|
fn test_table_checkbox_detection() {
|
||||||
let md = "| Done |\n|------|\n| [x] |\n| [ ] |";
|
let md = "| Done |\n|------|\n| [x] |\n| [ ] |";
|
||||||
let events = crate::parser::parse_markdown_with_options(md, false, false).events;
|
let events = crate::parser::parse_markdown_with_options(md, false, false, false).events;
|
||||||
|
|
||||||
let mut in_table = false;
|
let mut in_table = false;
|
||||||
let mut cell_texts: Vec<String> = Vec::new();
|
let mut cell_texts: Vec<String> = Vec::new();
|
||||||
|
|
@ -3915,7 +4089,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_table_checkbox_marker_source_range() {
|
fn test_table_checkbox_marker_source_range() {
|
||||||
let md = "| Done |\n|------|\n| [x] |\n| [ ] |";
|
let md = "| Done |\n|------|\n| [x] |\n| [ ] |";
|
||||||
let events = crate::parser::parse_markdown_with_options(md, false, false).events;
|
let events = crate::parser::parse_markdown_with_options(md, false, false, false).events;
|
||||||
|
|
||||||
let mut in_cell = false;
|
let mut in_cell = false;
|
||||||
let mut pending_text = String::new();
|
let mut pending_text = String::new();
|
||||||
|
|
@ -4192,7 +4366,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn has_code_block(markdown: &str) -> bool {
|
fn has_code_block(markdown: &str) -> bool {
|
||||||
let parsed_data = parse_markdown_with_options(markdown, false, false);
|
let parsed_data = parse_markdown_with_options(markdown, false, false, false);
|
||||||
parsed_data
|
parsed_data
|
||||||
.events
|
.events
|
||||||
.iter()
|
.iter()
|
||||||
|
|
|
||||||
|
|
@ -686,7 +686,8 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_extract_mermaid_diagrams_parses_scale() {
|
fn test_extract_mermaid_diagrams_parses_scale() {
|
||||||
let markdown = "```mermaid 150\ngraph TD;\n```\n\n```rust\nfn main() {}\n```";
|
let markdown = "```mermaid 150\ngraph TD;\n```\n\n```rust\nfn main() {}\n```";
|
||||||
let events = crate::parser::parse_markdown_with_options(markdown, false, false).events;
|
let events =
|
||||||
|
crate::parser::parse_markdown_with_options(markdown, false, false, false).events;
|
||||||
let diagrams = extract_mermaid_diagrams(markdown, &events);
|
let diagrams = extract_mermaid_diagrams(markdown, &events);
|
||||||
|
|
||||||
assert_eq!(diagrams.len(), 1);
|
assert_eq!(diagrams.len(), 1);
|
||||||
|
|
@ -702,7 +703,8 @@ mod tests {
|
||||||
"```mermaid\nblock-beta\n```\n\n",
|
"```mermaid\nblock-beta\n```\n\n",
|
||||||
"```mermaid\nflowchart TD\n A --> B\n```",
|
"```mermaid\nflowchart TD\n A --> B\n```",
|
||||||
);
|
);
|
||||||
let events = crate::parser::parse_markdown_with_options(markdown, false, false).events;
|
let events =
|
||||||
|
crate::parser::parse_markdown_with_options(markdown, false, false, false).events;
|
||||||
let diagrams = extract_mermaid_diagrams(markdown, &events);
|
let diagrams = extract_mermaid_diagrams(markdown, &events);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
diagrams.len(),
|
diagrams.len(),
|
||||||
|
|
|
||||||
|
|
@ -37,10 +37,23 @@ pub(crate) struct ParsedMarkdownData {
|
||||||
pub language_paths: HashSet<Arc<str>>,
|
pub language_paths: HashSet<Arc<str>>,
|
||||||
pub root_block_starts: Vec<usize>,
|
pub root_block_starts: Vec<usize>,
|
||||||
pub html_blocks: BTreeMap<usize, html::html_parser::ParsedHtmlBlock>,
|
pub html_blocks: BTreeMap<usize, html::html_parser::ParsedHtmlBlock>,
|
||||||
|
pub metadata_blocks: BTreeMap<usize, ParsedMetadataBlock>,
|
||||||
pub heading_slugs: HashMap<SharedString, usize>,
|
pub heading_slugs: HashMap<SharedString, usize>,
|
||||||
pub footnote_definitions: HashMap<SharedString, usize>,
|
pub footnote_definitions: HashMap<SharedString, usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub(crate) struct ParsedMetadataBlock {
|
||||||
|
pub content_range: Range<usize>,
|
||||||
|
pub rows: Option<Vec<MetadataRow>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub(crate) struct MetadataRow {
|
||||||
|
pub key: Range<usize>,
|
||||||
|
pub value: Range<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
impl ParseState {
|
impl ParseState {
|
||||||
fn push_event(&mut self, range: Range<usize>, event: MarkdownEvent) {
|
fn push_event(&mut self, range: Range<usize>, event: MarkdownEvent) {
|
||||||
match &event {
|
match &event {
|
||||||
|
|
@ -149,27 +162,83 @@ fn build_heading_slugs(
|
||||||
slugs
|
slugs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_metadata_table_rows(source: &str, source_range: Range<usize>) -> Option<Vec<MetadataRow>> {
|
||||||
|
let mut rows = Vec::new();
|
||||||
|
let mut line_start = source_range.start;
|
||||||
|
|
||||||
|
for line in source[source_range].split_inclusive('\n') {
|
||||||
|
let line_end = line_start + line.len();
|
||||||
|
let content_end = line_start + line.trim_end_matches(['\r', '\n']).len();
|
||||||
|
let content_range = line_start..content_end;
|
||||||
|
let line_text = &source[content_range.clone()];
|
||||||
|
|
||||||
|
if line_text.is_empty()
|
||||||
|
|| line_text
|
||||||
|
.chars()
|
||||||
|
.next()
|
||||||
|
.is_some_and(|character| character.is_whitespace())
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let delimiter = line_text.find(':')?;
|
||||||
|
let key = trim_metadata_range(source, content_range.start..content_range.start + delimiter);
|
||||||
|
let value = trim_metadata_range(
|
||||||
|
source,
|
||||||
|
content_range.start + delimiter + 1..content_range.end,
|
||||||
|
);
|
||||||
|
if key.is_empty() || value.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.push(MetadataRow { key, value });
|
||||||
|
line_start = line_end;
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows.is_empty() { None } else { Some(rows) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn trim_metadata_range(source: &str, range: Range<usize>) -> Range<usize> {
|
||||||
|
let text = &source[range.clone()];
|
||||||
|
let start_offset = text.len() - text.trim_start().len();
|
||||||
|
let end_offset = text.trim_end().len();
|
||||||
|
let start = range.start + start_offset;
|
||||||
|
let end = (range.start + end_offset).max(start);
|
||||||
|
start..end
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn parse_markdown_with_options(
|
pub(crate) fn parse_markdown_with_options(
|
||||||
text: &str,
|
text: &str,
|
||||||
parse_html: bool,
|
parse_html: bool,
|
||||||
parse_heading_slugs: bool,
|
parse_heading_slugs: bool,
|
||||||
|
parse_metadata_blocks: bool,
|
||||||
) -> ParsedMarkdownData {
|
) -> ParsedMarkdownData {
|
||||||
let mut state = ParseState::default();
|
let mut state = ParseState::default();
|
||||||
let mut language_names = HashSet::default();
|
let mut language_names = HashSet::default();
|
||||||
let mut language_paths = HashSet::default();
|
let mut language_paths = HashSet::default();
|
||||||
let mut html_blocks = BTreeMap::default();
|
let mut html_blocks = BTreeMap::default();
|
||||||
|
let mut metadata_blocks = BTreeMap::default();
|
||||||
let mut within_link = false;
|
let mut within_link = false;
|
||||||
let mut within_code_block = false;
|
let mut within_code_block = false;
|
||||||
let mut within_metadata = false;
|
let mut within_metadata = false;
|
||||||
let mut parser = Parser::new_ext(text, PARSE_OPTIONS)
|
let mut current_metadata_block_start = None;
|
||||||
|
let mut metadata_block_content_range: Option<Range<usize>> = None;
|
||||||
|
let parse_options = if parse_metadata_blocks {
|
||||||
|
PARSE_OPTIONS.union(Options::ENABLE_YAML_STYLE_METADATA_BLOCKS)
|
||||||
|
} else {
|
||||||
|
PARSE_OPTIONS
|
||||||
|
};
|
||||||
|
let mut parser = Parser::new_ext(text, parse_options)
|
||||||
.into_offset_iter()
|
.into_offset_iter()
|
||||||
.peekable();
|
.peekable();
|
||||||
while let Some((pulldown_event, range)) = parser.next() {
|
while let Some((pulldown_event, range)) = parser.next() {
|
||||||
if within_metadata {
|
if within_metadata && !parse_metadata_blocks {
|
||||||
if let pulldown_cmark::Event::End(pulldown_cmark::TagEnd::MetadataBlock { .. }) =
|
if let pulldown_cmark::Event::End(pulldown_cmark::TagEnd::MetadataBlock(_)) =
|
||||||
pulldown_event
|
pulldown_event
|
||||||
{
|
{
|
||||||
within_metadata = false;
|
within_metadata = false;
|
||||||
|
current_metadata_block_start = None;
|
||||||
|
metadata_block_content_range = None;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -216,10 +285,15 @@ pub(crate) fn parse_markdown_with_options(
|
||||||
id: SharedString::from(id.into_string()),
|
id: SharedString::from(id.into_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pulldown_cmark::Tag::MetadataBlock(_kind) => {
|
pulldown_cmark::Tag::MetadataBlock(kind) => {
|
||||||
within_metadata = true;
|
within_metadata = true;
|
||||||
|
current_metadata_block_start = Some(range.start);
|
||||||
|
metadata_block_content_range = None;
|
||||||
|
if !parse_metadata_blocks {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
MarkdownTag::MetadataBlock(kind)
|
||||||
|
}
|
||||||
pulldown_cmark::Tag::CodeBlock(pulldown_cmark::CodeBlockKind::Indented) => {
|
pulldown_cmark::Tag::CodeBlock(pulldown_cmark::CodeBlockKind::Indented) => {
|
||||||
within_code_block = true;
|
within_code_block = true;
|
||||||
MarkdownTag::CodeBlock {
|
MarkdownTag::CodeBlock {
|
||||||
|
|
@ -347,6 +421,25 @@ pub(crate) fn parse_markdown_with_options(
|
||||||
within_link = false;
|
within_link = false;
|
||||||
} else if let pulldown_cmark::TagEnd::CodeBlock = tag {
|
} else if let pulldown_cmark::TagEnd::CodeBlock = tag {
|
||||||
within_code_block = false;
|
within_code_block = false;
|
||||||
|
} else if let pulldown_cmark::TagEnd::MetadataBlock(_) = tag {
|
||||||
|
within_metadata = false;
|
||||||
|
let block_start = current_metadata_block_start.take();
|
||||||
|
let content_range = metadata_block_content_range.take();
|
||||||
|
if parse_metadata_blocks
|
||||||
|
&& let (Some(block_start), Some(content_range)) =
|
||||||
|
(block_start, content_range)
|
||||||
|
{
|
||||||
|
metadata_blocks.insert(
|
||||||
|
block_start,
|
||||||
|
ParsedMetadataBlock {
|
||||||
|
rows: parse_metadata_table_rows(text, content_range.clone()),
|
||||||
|
content_range,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if !parse_metadata_blocks {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
state.push_event(range, MarkdownEvent::End(tag));
|
state.push_event(range, MarkdownEvent::End(tag));
|
||||||
}
|
}
|
||||||
|
|
@ -363,6 +456,18 @@ pub(crate) fn parse_markdown_with_options(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if within_metadata {
|
||||||
|
match &mut metadata_block_content_range {
|
||||||
|
Some(content_range) => {
|
||||||
|
content_range.start = content_range.start.min(range.start);
|
||||||
|
content_range.end = content_range.end.max(range.end);
|
||||||
|
}
|
||||||
|
None => metadata_block_content_range = Some(range.clone()),
|
||||||
|
}
|
||||||
|
state.push_event(range, MarkdownEvent::Text);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if within_code_block {
|
if within_code_block {
|
||||||
let (range, event) = event_for(text, range, &parsed);
|
let (range, event) = event_for(text, range, &parsed);
|
||||||
state.push_event(range, event);
|
state.push_event(range, event);
|
||||||
|
|
@ -541,6 +646,7 @@ pub(crate) fn parse_markdown_with_options(
|
||||||
language_paths,
|
language_paths,
|
||||||
root_block_starts: state.root_block_starts,
|
root_block_starts: state.root_block_starts,
|
||||||
html_blocks,
|
html_blocks,
|
||||||
|
metadata_blocks,
|
||||||
heading_slugs,
|
heading_slugs,
|
||||||
footnote_definitions,
|
footnote_definitions,
|
||||||
}
|
}
|
||||||
|
|
@ -798,8 +904,8 @@ mod tests {
|
||||||
use super::MarkdownTag::*;
|
use super::MarkdownTag::*;
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
const UNWANTED_OPTIONS: Options = Options::ENABLE_YAML_STYLE_METADATA_BLOCKS
|
const CONDITIONAL_OPTIONS: Options = Options::ENABLE_YAML_STYLE_METADATA_BLOCKS;
|
||||||
.union(Options::ENABLE_MATH)
|
const UNWANTED_OPTIONS: Options = Options::ENABLE_MATH
|
||||||
.union(Options::ENABLE_DEFINITION_LIST)
|
.union(Options::ENABLE_DEFINITION_LIST)
|
||||||
.union(Options::ENABLE_WIKILINKS);
|
.union(Options::ENABLE_WIKILINKS);
|
||||||
|
|
||||||
|
|
@ -807,21 +913,174 @@ mod tests {
|
||||||
fn all_options_considered() {
|
fn all_options_considered() {
|
||||||
// The purpose of this is to fail when new options are added to pulldown_cmark, so that they
|
// The purpose of this is to fail when new options are added to pulldown_cmark, so that they
|
||||||
// can be evaluated for inclusion.
|
// can be evaluated for inclusion.
|
||||||
assert_eq!(PARSE_OPTIONS.union(UNWANTED_OPTIONS), Options::all());
|
assert_eq!(
|
||||||
|
PARSE_OPTIONS
|
||||||
|
.union(CONDITIONAL_OPTIONS)
|
||||||
|
.union(UNWANTED_OPTIONS),
|
||||||
|
Options::all()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn wanted_and_unwanted_options_disjoint() {
|
fn wanted_and_unwanted_options_disjoint() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
PARSE_OPTIONS.intersection(UNWANTED_OPTIONS),
|
PARSE_OPTIONS
|
||||||
|
.union(CONDITIONAL_OPTIONS)
|
||||||
|
.intersection(UNWANTED_OPTIONS),
|
||||||
Options::empty()
|
Options::empty()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_yaml_style_metadata_block() {
|
||||||
|
assert_eq!(
|
||||||
|
parse_markdown_with_options("---\ntitle: Post\n---\n# Heading", false, false, true),
|
||||||
|
ParsedMarkdownData {
|
||||||
|
events: vec![
|
||||||
|
(0..19, RootStart),
|
||||||
|
(0..19, Start(MetadataBlock(MetadataBlockKind::YamlStyle))),
|
||||||
|
(4..16, Text),
|
||||||
|
(
|
||||||
|
0..19,
|
||||||
|
End(MarkdownTagEnd::MetadataBlock(MetadataBlockKind::YamlStyle))
|
||||||
|
),
|
||||||
|
(0..19, RootEnd(0)),
|
||||||
|
(20..29, RootStart),
|
||||||
|
(
|
||||||
|
20..29,
|
||||||
|
Start(Heading {
|
||||||
|
level: HeadingLevel::H1,
|
||||||
|
id: None,
|
||||||
|
classes: Vec::new(),
|
||||||
|
attrs: Vec::new(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
(22..29, Text),
|
||||||
|
(20..29, End(MarkdownTagEnd::Heading(HeadingLevel::H1))),
|
||||||
|
(20..29, RootEnd(1)),
|
||||||
|
],
|
||||||
|
root_block_starts: vec![0, 20],
|
||||||
|
metadata_blocks: BTreeMap::from_iter([(
|
||||||
|
0,
|
||||||
|
ParsedMetadataBlock {
|
||||||
|
content_range: 4..16,
|
||||||
|
rows: Some(vec![MetadataRow {
|
||||||
|
key: 4..9,
|
||||||
|
value: 11..15,
|
||||||
|
}]),
|
||||||
|
},
|
||||||
|
)]),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_metadata_block_text_is_verbatim() {
|
||||||
|
let parsed =
|
||||||
|
parse_markdown_with_options("---\nurl: https://zed.dev\n---\nBody", false, false, true);
|
||||||
|
assert!(
|
||||||
|
parsed
|
||||||
|
.events
|
||||||
|
.iter()
|
||||||
|
.all(|(_, event)| !matches!(event, Start(Link { .. })))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_metadata_blocks_store_table_rows() {
|
||||||
|
let parsed = parse_markdown_with_options(
|
||||||
|
"---\ntitle: Post\nauthor: Zed\n---\nBody",
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
parsed.metadata_blocks,
|
||||||
|
BTreeMap::from_iter([(
|
||||||
|
0,
|
||||||
|
ParsedMetadataBlock {
|
||||||
|
content_range: 4..28,
|
||||||
|
rows: Some(vec![
|
||||||
|
MetadataRow {
|
||||||
|
key: 4..9,
|
||||||
|
value: 11..15,
|
||||||
|
},
|
||||||
|
MetadataRow {
|
||||||
|
key: 16..22,
|
||||||
|
value: 24..27,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_metadata_blocks_store_fallback_for_nested_yaml() {
|
||||||
|
let parsed =
|
||||||
|
parse_markdown_with_options("---\ntags:\n - zed\n---\nBody", false, false, true);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
parsed.metadata_blocks,
|
||||||
|
BTreeMap::from_iter([(
|
||||||
|
0,
|
||||||
|
ParsedMetadataBlock {
|
||||||
|
content_range: 4..18,
|
||||||
|
rows: None,
|
||||||
|
},
|
||||||
|
)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_metadata_table_rows_parse_simple_colon_pairs() {
|
||||||
|
let source = "title: Post\nauthor: Zed\n";
|
||||||
|
let Some(rows) = parse_metadata_table_rows(source, 0..source.len()) else {
|
||||||
|
panic!("expected metadata rows");
|
||||||
|
};
|
||||||
|
let pairs = rows
|
||||||
|
.into_iter()
|
||||||
|
.map(|row| (&source[row.key], &source[row.value]))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
assert_eq!(pairs, vec![("title", "Post"), ("author", "Zed")]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_metadata_table_rows_reject_non_simple_colon_pairs() {
|
||||||
|
for source in [
|
||||||
|
"tags:\n - zed\n",
|
||||||
|
"title = Post\n",
|
||||||
|
"title:\n",
|
||||||
|
"title: \n",
|
||||||
|
": Post\n",
|
||||||
|
" title: Post\n",
|
||||||
|
"\n",
|
||||||
|
] {
|
||||||
|
assert!(parse_metadata_table_rows(source, 0..source.len()).is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_trim_metadata_range_returns_valid_empty_range() {
|
||||||
|
let source = "key: \n";
|
||||||
|
let trimmed = trim_metadata_range(source, 4..7);
|
||||||
|
|
||||||
|
assert_eq!(trimmed, 7..7);
|
||||||
|
assert!(source[trimmed].is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_html_comments() {
|
fn test_html_comments() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_markdown_with_options(" <!--\nrdoc-file=string.c\n-->\nReturns", false, false),
|
parse_markdown_with_options(
|
||||||
|
" <!--\nrdoc-file=string.c\n-->\nReturns",
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
),
|
||||||
ParsedMarkdownData {
|
ParsedMarkdownData {
|
||||||
events: vec
|
)
|
||||||
.events,
|
.events,
|
||||||
vec![
|
vec![
|
||||||
|
|
@ -925,6 +1186,7 @@ mod tests {
|
||||||
"-- --- ... \"double quoted\" 'single quoted' ----------",
|
"-- --- ... \"double quoted\" 'single quoted' ----------",
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
|
false,
|
||||||
),
|
),
|
||||||
ParsedMarkdownData {
|
ParsedMarkdownData {
|
||||||
events: vec![
|
events: vec![
|
||||||
|
|
@ -957,7 +1219,12 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_code_block_metadata() {
|
fn test_code_block_metadata() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_markdown_with_options("```rust\nfn main() {\n let a = 1;\n}\n```", false, false),
|
parse_markdown_with_options(
|
||||||
|
"```rust\nfn main() {\n let a = 1;\n}\n```",
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
),
|
||||||
ParsedMarkdownData {
|
ParsedMarkdownData {
|
||||||
events: vec![
|
events: vec![
|
||||||
(0..37, RootStart),
|
(0..37, RootStart),
|
||||||
|
|
@ -986,7 +1253,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_markdown_with_options(" fn main() {}", false, false),
|
parse_markdown_with_options(" fn main() {}", false, false, false),
|
||||||
ParsedMarkdownData {
|
ParsedMarkdownData {
|
||||||
events: vec![
|
events: vec![
|
||||||
(4..16, RootStart),
|
(4..16, RootStart),
|
||||||
|
|
@ -1012,7 +1279,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn assert_code_block_does_not_emit_links(markdown: &str) {
|
fn assert_code_block_does_not_emit_links(markdown: &str) {
|
||||||
let parsed = parse_markdown_with_options(markdown, false, false);
|
let parsed = parse_markdown_with_options(markdown, false, false, false);
|
||||||
let mut code_block_depth = 0;
|
let mut code_block_depth = 0;
|
||||||
let mut code_block_count = 0;
|
let mut code_block_count = 0;
|
||||||
let mut saw_text_inside_code_block = false;
|
let mut saw_text_inside_code_block = false;
|
||||||
|
|
@ -1064,9 +1331,54 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_metadata_blocks_do_not_affect_root_blocks() {
|
fn test_metadata_blocks_are_root_blocks() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_markdown_with_options("+++\ntitle = \"Example\"\n+++\n\nParagraph", false, false),
|
parse_markdown_with_options(
|
||||||
|
"+++\ntitle = \"Example\"\n+++\n\nParagraph",
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true
|
||||||
|
),
|
||||||
|
ParsedMarkdownData {
|
||||||
|
events: vec![
|
||||||
|
(0..25, RootStart),
|
||||||
|
(0..25, Start(MetadataBlock(MetadataBlockKind::PlusesStyle))),
|
||||||
|
(4..22, Text),
|
||||||
|
(
|
||||||
|
0..25,
|
||||||
|
End(MarkdownTagEnd::MetadataBlock(
|
||||||
|
MetadataBlockKind::PlusesStyle
|
||||||
|
))
|
||||||
|
),
|
||||||
|
(0..25, RootEnd(0)),
|
||||||
|
(27..36, RootStart),
|
||||||
|
(27..36, Start(Paragraph)),
|
||||||
|
(27..36, Text),
|
||||||
|
(27..36, End(MarkdownTagEnd::Paragraph)),
|
||||||
|
(27..36, RootEnd(1)),
|
||||||
|
],
|
||||||
|
root_block_starts: vec![0, 27],
|
||||||
|
metadata_blocks: BTreeMap::from_iter([(
|
||||||
|
0,
|
||||||
|
ParsedMetadataBlock {
|
||||||
|
content_range: 4..22,
|
||||||
|
rows: None,
|
||||||
|
},
|
||||||
|
)]),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_metadata_blocks_are_omitted_by_default() {
|
||||||
|
assert_eq!(
|
||||||
|
parse_markdown_with_options(
|
||||||
|
"+++\ntitle = \"Example\"\n+++\n\nParagraph",
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
),
|
||||||
ParsedMarkdownData {
|
ParsedMarkdownData {
|
||||||
events: vec![
|
events: vec![
|
||||||
(27..36, RootStart),
|
(27..36, RootStart),
|
||||||
|
|
@ -1088,7 +1400,7 @@ mod tests {
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| [x] | Fix bug |
|
| [x] | Fix bug |
|
||||||
| [ ] | Add feature |";
|
| [ ] | Add feature |";
|
||||||
let parsed = parse_markdown_with_options(markdown, false, false);
|
let parsed = parse_markdown_with_options(markdown, false, false, false);
|
||||||
|
|
||||||
let mut in_table = false;
|
let mut in_table = false;
|
||||||
let mut saw_task_list_marker = false;
|
let mut saw_task_list_marker = false;
|
||||||
|
|
@ -1164,6 +1476,7 @@ mod tests {
|
||||||
"Text with a footnote[^1] and some more text.\n\n[^1]: This is the footnote content.",
|
"Text with a footnote[^1] and some more text.\n\n[^1]: This is the footnote content.",
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parsed.events,
|
parsed.events,
|
||||||
|
|
@ -1194,6 +1507,7 @@ mod tests {
|
||||||
"Text[^a] and[^b].\n\n[^a]: First.\n\n[^b]: Second.",
|
"Text[^a] and[^b].\n\n[^a]: First.\n\n[^b]: Second.",
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
assert_eq!(parsed.footnote_definitions.len(), 2);
|
assert_eq!(parsed.footnote_definitions.len(), 2);
|
||||||
assert!(parsed.footnote_definitions.contains_key("a"));
|
assert!(parsed.footnote_definitions.contains_key("a"));
|
||||||
|
|
@ -1211,6 +1525,7 @@ mod tests {
|
||||||
"https:/\\/example.com is equivalent to https://example.com!",
|
"https:/\\/example.com is equivalent to https://example.com!",
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
|
false,
|
||||||
)
|
)
|
||||||
.events,
|
.events,
|
||||||
vec![
|
vec![
|
||||||
|
|
@ -1253,6 +1568,7 @@ mod tests {
|
||||||
"Visit https://example.com/cat\\/é‍☕ for coffee!",
|
"Visit https://example.com/cat\\/é‍☕ for coffee!",
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
|
false,
|
||||||
)
|
)
|
||||||
.events,
|
.events,
|
||||||
[
|
[
|
||||||
|
|
@ -1286,6 +1602,7 @@ mod tests {
|
||||||
"# Hello World\n\n## Code `block`\n\n### Third Level\n\n#### Fourth Level\n\n## Hello World",
|
"# Hello World\n\n## Code `block`\n\n### Third Level\n\n#### Fourth Level\n\n## Hello World",
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
assert_eq!(parsed.heading_slugs.len(), 5);
|
assert_eq!(parsed.heading_slugs.len(), 5);
|
||||||
assert!(parsed.heading_slugs.contains_key("hello-world"));
|
assert!(parsed.heading_slugs.contains_key("hello-world"));
|
||||||
|
|
@ -1301,6 +1618,7 @@ mod tests {
|
||||||
"# Duplicate\n\nText\n\n## Duplicate\n\nMore text",
|
"# Duplicate\n\nText\n\n## Duplicate\n\nMore text",
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
let first = parsed.heading_slugs.get("duplicate").copied();
|
let first = parsed.heading_slugs.get("duplicate").copied();
|
||||||
let second = parsed.heading_slugs.get("duplicate-1").copied();
|
let second = parsed.heading_slugs.get("duplicate-1").copied();
|
||||||
|
|
@ -1311,7 +1629,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_heading_slug_collision_with_dedup_suffix() {
|
fn test_heading_slug_collision_with_dedup_suffix() {
|
||||||
let parsed = parse_markdown_with_options("# Foo\n\n## Foo\n\n## Foo 1", false, true);
|
let parsed = parse_markdown_with_options("# Foo\n\n## Foo\n\n## Foo 1", false, true, false);
|
||||||
assert_eq!(parsed.heading_slugs.len(), 3);
|
assert_eq!(parsed.heading_slugs.len(), 3);
|
||||||
assert!(parsed.heading_slugs.contains_key("foo"));
|
assert!(parsed.heading_slugs.contains_key("foo"));
|
||||||
assert!(parsed.heading_slugs.contains_key("foo-1"));
|
assert!(parsed.heading_slugs.contains_key("foo-1"));
|
||||||
|
|
@ -1323,7 +1641,7 @@ mod tests {
|
||||||
use pulldown_cmark::BlockQuoteKind;
|
use pulldown_cmark::BlockQuoteKind;
|
||||||
|
|
||||||
let markdown = "\n> [!NOTE]\n> A note.\n\n> [!TIP]\n> A tip.\n\n> [!IMPORTANT]\n> Important.\n\n> [!WARNING]\n> A warning.\n\n> [!CAUTION]\n> A caution.\n\n> Plain quote.\n";
|
let markdown = "\n> [!NOTE]\n> A note.\n\n> [!TIP]\n> A tip.\n\n> [!IMPORTANT]\n> Important.\n\n> [!WARNING]\n> A warning.\n\n> [!CAUTION]\n> A caution.\n\n> Plain quote.\n";
|
||||||
let parsed = parse_markdown_with_options(markdown, false, false);
|
let parsed = parse_markdown_with_options(markdown, false, false, false);
|
||||||
|
|
||||||
let block_quote_kinds: Vec<_> = parsed
|
let block_quote_kinds: Vec<_> = parsed
|
||||||
.events
|
.events
|
||||||
|
|
|
||||||
|
|
@ -223,6 +223,7 @@ impl MarkdownPreviewView {
|
||||||
parse_html: true,
|
parse_html: true,
|
||||||
render_mermaid_diagrams: true,
|
render_mermaid_diagrams: true,
|
||||||
parse_heading_slugs: true,
|
parse_heading_slugs: true,
|
||||||
|
render_metadata_blocks: true,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
cx,
|
cx,
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,11 @@ use futures_lite::future::yield_now;
|
||||||
use gpui::{App, Context, Entity, EventEmitter};
|
use gpui::{App, Context, Entity, EventEmitter};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use language::{
|
use language::{
|
||||||
AutoindentMode, Buffer, BufferChunks, BufferRow, BufferSnapshot, Capability, CharClassifier,
|
AutoindentMode, Buffer, BufferChunks, BufferEditSource, BufferRow, BufferSnapshot, Capability,
|
||||||
CharKind, CharScopeContext, Chunk, CursorShape, DiagnosticEntryRef, File, IndentGuideSettings,
|
CharClassifier, CharKind, CharScopeContext, Chunk, CursorShape, DiagnosticEntryRef, File,
|
||||||
IndentSize, Language, LanguageAwareStyling, LanguageScope, OffsetRangeExt, OffsetUtf16,
|
IndentGuideSettings, IndentSize, Language, LanguageAwareStyling, LanguageScope, OffsetRangeExt,
|
||||||
Outline, OutlineItem, Point, PointUtf16, Selection, TextDimension, TextObject, ToOffset as _,
|
OffsetUtf16, Outline, OutlineItem, Point, PointUtf16, Selection, TextDimension, TextObject,
|
||||||
ToPoint as _, TransactionId, TreeSitterOptions, Unclipped,
|
ToOffset as _, ToPoint as _, TransactionId, TreeSitterOptions, Unclipped,
|
||||||
language_settings::{AllLanguageSettings, LanguageSettings},
|
language_settings::{AllLanguageSettings, LanguageSettings},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -110,7 +110,7 @@ pub enum Event {
|
||||||
DiffHunksToggled,
|
DiffHunksToggled,
|
||||||
Edited {
|
Edited {
|
||||||
edited_buffer: Option<Entity<Buffer>>,
|
edited_buffer: Option<Entity<Buffer>>,
|
||||||
is_local: bool,
|
source: BufferEditSource,
|
||||||
},
|
},
|
||||||
TransactionUndone {
|
TransactionUndone {
|
||||||
transaction_id: TransactionId,
|
transaction_id: TransactionId,
|
||||||
|
|
@ -1828,7 +1828,7 @@ impl MultiBuffer {
|
||||||
}
|
}
|
||||||
cx.emit(Event::Edited {
|
cx.emit(Event::Edited {
|
||||||
edited_buffer: None,
|
edited_buffer: None,
|
||||||
is_local: true,
|
source: BufferEditSource::User,
|
||||||
});
|
});
|
||||||
cx.emit(Event::BuffersRemoved { removed_buffer_ids });
|
cx.emit(Event::BuffersRemoved { removed_buffer_ids });
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
|
@ -1952,9 +1952,9 @@ impl MultiBuffer {
|
||||||
use language::BufferEvent;
|
use language::BufferEvent;
|
||||||
let buffer_id = buffer.read(cx).remote_id();
|
let buffer_id = buffer.read(cx).remote_id();
|
||||||
cx.emit(match event {
|
cx.emit(match event {
|
||||||
&BufferEvent::Edited { is_local } => Event::Edited {
|
&BufferEvent::Edited { source } => Event::Edited {
|
||||||
edited_buffer: Some(buffer),
|
edited_buffer: Some(buffer),
|
||||||
is_local,
|
source,
|
||||||
},
|
},
|
||||||
BufferEvent::DirtyChanged => Event::DirtyChanged,
|
BufferEvent::DirtyChanged => Event::DirtyChanged,
|
||||||
BufferEvent::Saved => Event::Saved,
|
BufferEvent::Saved => Event::Saved,
|
||||||
|
|
@ -2044,7 +2044,7 @@ impl MultiBuffer {
|
||||||
}
|
}
|
||||||
cx.emit(Event::Edited {
|
cx.emit(Event::Edited {
|
||||||
edited_buffer: None,
|
edited_buffer: None,
|
||||||
is_local: true,
|
source: BufferEditSource::User,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2090,7 +2090,7 @@ impl MultiBuffer {
|
||||||
}
|
}
|
||||||
cx.emit(Event::Edited {
|
cx.emit(Event::Edited {
|
||||||
edited_buffer: None,
|
edited_buffer: None,
|
||||||
is_local: true,
|
source: BufferEditSource::User,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2313,7 +2313,7 @@ impl MultiBuffer {
|
||||||
cx.emit(Event::DiffHunksToggled);
|
cx.emit(Event::DiffHunksToggled);
|
||||||
cx.emit(Event::Edited {
|
cx.emit(Event::Edited {
|
||||||
edited_buffer: None,
|
edited_buffer: None,
|
||||||
is_local: true,
|
source: BufferEditSource::User,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2449,7 +2449,7 @@ impl MultiBuffer {
|
||||||
cx.emit(Event::DiffHunksToggled);
|
cx.emit(Event::DiffHunksToggled);
|
||||||
cx.emit(Event::Edited {
|
cx.emit(Event::Edited {
|
||||||
edited_buffer: None,
|
edited_buffer: None,
|
||||||
is_local: true,
|
source: BufferEditSource::User,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3102,7 +3102,7 @@ impl MultiBuffer {
|
||||||
cx.emit(Event::DiffHunksToggled);
|
cx.emit(Event::DiffHunksToggled);
|
||||||
cx.emit(Event::Edited {
|
cx.emit(Event::Edited {
|
||||||
edited_buffer: None,
|
edited_buffer: None,
|
||||||
is_local: true,
|
source: BufferEditSource::User,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue