mirror of
https://github.com/zed-industries/zed.git
synced 2026-05-31 19:05:00 +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_router/ @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)
|
||||
/crates/shell_command_parser/ @zed-industries/ai-team
|
||||
/crates/vercel/ @zed-industries/ai-team
|
||||
|
|
@ -181,7 +180,6 @@
|
|||
/crates/fs_benchmarks/ @zed-industries/infrastructure-team
|
||||
/crates/http_client/ @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/paths/ @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
|
||||
if-no-files-found: error
|
||||
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:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
|
|
|||
119
Cargo.lock
generated
119
Cargo.lock
generated
|
|
@ -109,7 +109,6 @@ dependencies = [
|
|||
"parking_lot",
|
||||
"portable-pty",
|
||||
"project",
|
||||
"prompt_store",
|
||||
"rand 0.9.4",
|
||||
"sandbox",
|
||||
"serde",
|
||||
|
|
@ -234,6 +233,7 @@ dependencies = [
|
|||
"agent_settings",
|
||||
"agent_skills",
|
||||
"anyhow",
|
||||
"assets",
|
||||
"async-channel 2.5.0",
|
||||
"async-io",
|
||||
"chrono",
|
||||
|
|
@ -290,6 +290,7 @@ dependencies = [
|
|||
"tempfile",
|
||||
"text",
|
||||
"theme",
|
||||
"theme_settings",
|
||||
"thiserror 2.0.17",
|
||||
"ui",
|
||||
"unindent",
|
||||
|
|
@ -404,7 +405,7 @@ dependencies = [
|
|||
"agent-client-protocol",
|
||||
"anyhow",
|
||||
"collections",
|
||||
"convert_case 0.8.0",
|
||||
"convert_case 0.11.0",
|
||||
"fs",
|
||||
"futures 0.3.32",
|
||||
"gpui",
|
||||
|
|
@ -426,6 +427,7 @@ name = "agent_skills"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
"const_format",
|
||||
"fs",
|
||||
"futures 0.3.32",
|
||||
|
|
@ -434,6 +436,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml_ng",
|
||||
"url",
|
||||
"util",
|
||||
]
|
||||
|
||||
|
|
@ -512,7 +515,6 @@ dependencies = [
|
|||
"remote_server",
|
||||
"reqwest_client",
|
||||
"rope",
|
||||
"rules_library",
|
||||
"schemars 1.0.4",
|
||||
"search",
|
||||
"semver",
|
||||
|
|
@ -597,15 +599,14 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "alacritty_terminal"
|
||||
version = "0.25.1"
|
||||
source = "git+https://github.com/zed-industries/alacritty?rev=9d9640d4#9d9640d4e56d67a09d049f9c0a300aae08d4f61e"
|
||||
version = "0.26.1-dev"
|
||||
source = "git+https://github.com/zed-industries/alacritty?rev=fcf32feacb367b75ec84dd40f041e4fd411d3cc1#fcf32feacb367b75ec84dd40f041e4fd411d3cc1"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bitflags 2.10.0",
|
||||
"home",
|
||||
"libc",
|
||||
"log",
|
||||
"mach2 0.5.0",
|
||||
"miow",
|
||||
"parking_lot",
|
||||
"piper",
|
||||
|
|
@ -2161,7 +2162,7 @@ dependencies = [
|
|||
"bitflags 2.10.0",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools 0.11.0",
|
||||
"itertools 0.10.5",
|
||||
"log",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
|
|
@ -2181,7 +2182,7 @@ dependencies = [
|
|||
"bitflags 2.10.0",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools 0.11.0",
|
||||
"itertools 0.10.5",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
|
|
@ -3880,6 +3881,8 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"sqlez",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -5309,7 +5312,7 @@ dependencies = [
|
|||
"libc",
|
||||
"option-ext",
|
||||
"redox_users 0.5.2",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -5779,7 +5782,7 @@ dependencies = [
|
|||
"client",
|
||||
"clock",
|
||||
"collections",
|
||||
"convert_case 0.8.0",
|
||||
"convert_case 0.11.0",
|
||||
"criterion",
|
||||
"ctor",
|
||||
"dap",
|
||||
|
|
@ -6144,7 +6147,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -7600,7 +7603,7 @@ dependencies = [
|
|||
"gobject-sys",
|
||||
"libc",
|
||||
"system-deps",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -9061,7 +9064,7 @@ dependencies = [
|
|||
"libc",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2 0.6.3",
|
||||
"socket2 0.5.10",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
|
|
@ -9079,7 +9082,7 @@ dependencies = [
|
|||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core 0.62.2",
|
||||
"windows-core 0.56.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -9333,7 +9336,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.16.1",
|
||||
"hashbrown 0.15.5",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
|
@ -9746,6 +9749,7 @@ dependencies = [
|
|||
"settings",
|
||||
"snippet_provider",
|
||||
"task",
|
||||
"tempfile",
|
||||
"theme",
|
||||
"util",
|
||||
]
|
||||
|
|
@ -10143,7 +10147,7 @@ dependencies = [
|
|||
"cloud_api_types",
|
||||
"collections",
|
||||
"component",
|
||||
"convert_case 0.8.0",
|
||||
"convert_case 0.11.0",
|
||||
"copilot",
|
||||
"copilot_chat",
|
||||
"copilot_ui",
|
||||
|
|
@ -10476,7 +10480,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "libwebrtc"
|
||||
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 = [
|
||||
"cxx",
|
||||
"glib",
|
||||
|
|
@ -10586,7 +10590,7 @@ checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
|
|||
[[package]]
|
||||
name = "livekit"
|
||||
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 = [
|
||||
"base64 0.22.1",
|
||||
"bmrng",
|
||||
|
|
@ -10612,7 +10616,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "livekit-api"
|
||||
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 = [
|
||||
"base64 0.21.7",
|
||||
"futures-util",
|
||||
|
|
@ -10639,7 +10643,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "livekit-protocol"
|
||||
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 = [
|
||||
"futures-util",
|
||||
"livekit-runtime",
|
||||
|
|
@ -10655,7 +10659,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "livekit-runtime"
|
||||
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 = [
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
|
|
@ -11361,7 +11365,7 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"anyhow",
|
||||
"collections",
|
||||
"convert_case 0.8.0",
|
||||
"convert_case 0.11.0",
|
||||
"log",
|
||||
"pretty_assertions",
|
||||
"serde_json",
|
||||
|
|
@ -11722,16 +11726,6 @@ dependencies = [
|
|||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nc"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures 0.3.32",
|
||||
"net",
|
||||
"smol",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk"
|
||||
version = "0.9.0"
|
||||
|
|
@ -11957,7 +11951,7 @@ version = "0.50.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -14489,7 +14483,6 @@ dependencies = [
|
|||
"db",
|
||||
"fs",
|
||||
"futures 0.3.32",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"handlebars 4.5.0",
|
||||
"heed",
|
||||
|
|
@ -14497,7 +14490,6 @@ dependencies = [
|
|||
"log",
|
||||
"parking_lot",
|
||||
"paths",
|
||||
"rope",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum 0.27.2",
|
||||
|
|
@ -14596,7 +14588,7 @@ checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4"
|
|||
dependencies = [
|
||||
"bytes 1.11.1",
|
||||
"heck 0.5.0",
|
||||
"itertools 0.11.0",
|
||||
"itertools 0.10.5",
|
||||
"log",
|
||||
"multimap",
|
||||
"once_cell",
|
||||
|
|
@ -14629,7 +14621,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.11.0",
|
||||
"itertools 0.10.5",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
|
|
@ -14891,7 +14883,7 @@ dependencies = [
|
|||
"quinn-udp",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustls 0.23.40",
|
||||
"socket2 0.6.3",
|
||||
"socket2 0.5.10",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tracing",
|
||||
|
|
@ -14928,9 +14920,9 @@ dependencies = [
|
|||
"cfg_aliases 0.2.1",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2 0.6.3",
|
||||
"socket2 0.5.10",
|
||||
"tracing",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -16014,33 +16006,6 @@ version = "0.3.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "runtimelib"
|
||||
version = "1.4.0"
|
||||
|
|
@ -16182,7 +16147,7 @@ dependencies = [
|
|||
"errno 0.3.14",
|
||||
"libc",
|
||||
"linux-raw-sys 0.11.0",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -17339,9 +17304,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.3.18"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
|
||||
checksum = "b2a0c28ca5908dbdbcd52e6fdaa00358ab88637f8ab33e1f188dd510eb44b53d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"signal-hook-registry",
|
||||
|
|
@ -18804,7 +18769,7 @@ dependencies = [
|
|||
"getrandom 0.3.4",
|
||||
"once_cell",
|
||||
"rustix 1.1.2",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -19521,7 +19486,7 @@ name = "toolchain_selector"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"convert_case 0.8.0",
|
||||
"convert_case 0.11.0",
|
||||
"editor",
|
||||
"futures 0.3.32",
|
||||
"fuzzy",
|
||||
|
|
@ -19729,7 +19694,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "2fb391ac70462b3097a755618fbf9c8f95ecc1eb379a414f7b46f202ed10db1f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"windows-targets 0.52.6",
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -21522,7 +21487,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "webrtc-sys"
|
||||
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 = [
|
||||
"cc",
|
||||
"cxx",
|
||||
|
|
@ -21536,7 +21501,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "webrtc-sys-build"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"fs2",
|
||||
|
|
@ -21834,7 +21799,7 @@ version = "0.1.11"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -23512,6 +23477,7 @@ dependencies = [
|
|||
"agent-client-protocol",
|
||||
"agent_servers",
|
||||
"agent_settings",
|
||||
"agent_skills",
|
||||
"agent_ui",
|
||||
"anyhow",
|
||||
"ashpd",
|
||||
|
|
@ -23594,7 +23560,6 @@ dependencies = [
|
|||
"migrator",
|
||||
"mimalloc",
|
||||
"miniprofiler_ui",
|
||||
"nc",
|
||||
"node_runtime",
|
||||
"notifications",
|
||||
"onboarding",
|
||||
|
|
|
|||
14
Cargo.toml
14
Cargo.toml
|
|
@ -137,7 +137,6 @@ members = [
|
|||
"crates/miniprofiler_ui",
|
||||
"crates/mistral",
|
||||
"crates/multi_buffer",
|
||||
"crates/nc",
|
||||
"crates/net",
|
||||
"crates/node_runtime",
|
||||
"crates/notifications",
|
||||
|
|
@ -172,7 +171,6 @@ members = [
|
|||
"crates/reqwest_client",
|
||||
"crates/rope",
|
||||
"crates/rpc",
|
||||
"crates/rules_library",
|
||||
"crates/sandbox",
|
||||
"crates/skill_creator",
|
||||
"crates/scheduler",
|
||||
|
|
@ -399,7 +397,6 @@ migrator = { path = "crates/migrator" }
|
|||
mistral = { path = "crates/mistral" }
|
||||
multi_buffer = { path = "crates/multi_buffer" }
|
||||
miniprofiler_ui = { path = "crates/miniprofiler_ui" }
|
||||
nc = { path = "crates/nc" }
|
||||
net = { path = "crates/net" }
|
||||
node_runtime = { path = "crates/node_runtime" }
|
||||
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"] }
|
||||
rope = { path = "crates/rope" }
|
||||
rpc = { path = "crates/rpc" }
|
||||
rules_library = { path = "crates/rules_library" }
|
||||
skill_creator = { path = "crates/skill_creator" }
|
||||
scheduler = { path = "crates/scheduler" }
|
||||
sandbox = { path = "crates/sandbox" }
|
||||
|
|
@ -511,7 +507,7 @@ accesskit_unix = "0.21.0"
|
|||
accesskit_windows = "0.32.1"
|
||||
agent-client-protocol = { version = "=0.12.1", features = ["unstable"] }
|
||||
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"
|
||||
anyhow = "1.0.86"
|
||||
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-foundation = "=0.2.0"
|
||||
const_format = "0.2"
|
||||
convert_case = "0.8.0"
|
||||
convert_case = "0.11.0"
|
||||
core-foundation = "=0.10.0"
|
||||
core-foundation-sys = "0.8.6"
|
||||
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" }
|
||||
windows-capture = { git = "https://github.com/zed-industries/windows-capture.git", rev = "f0d6c1b6691db75461b732f6d5ff56eed002eeb9" }
|
||||
calloop = { git = "https://github.com/zed-industries/calloop" }
|
||||
livekit = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "147fbca3d4b592d96d33f5e6a84b59fc0b5d9bf1" }
|
||||
libwebrtc = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "147fbca3d4b592d96d33f5e6a84b59fc0b5d9bf1" }
|
||||
webrtc-sys = { 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 = "c3a55bbc207008f1ca3474b6037fdd3c443cad0f" }
|
||||
webrtc-sys = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "c3a55bbc207008f1ca3474b6037fdd3c443cad0f" }
|
||||
|
||||
[profile.dev]
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
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",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "RulesLibrary",
|
||||
"bindings": {
|
||||
"new": "rules_library::NewRule",
|
||||
"ctrl-n": "rules_library::NewRule",
|
||||
"ctrl-shift-s": "rules_library::ToggleDefaultRule",
|
||||
"ctrl-w": "workspace::CloseWindow",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
"context": "BufferSearchBar",
|
||||
"bindings": {
|
||||
|
|
|
|||
|
|
@ -427,15 +427,6 @@
|
|||
"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",
|
||||
"use_key_equivalents": true,
|
||||
|
|
|
|||
|
|
@ -383,15 +383,6 @@
|
|||
"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",
|
||||
"use_key_equivalents": true,
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ parking_lot = { workspace = true, optional = true }
|
|||
image.workspace = true
|
||||
portable-pty.workspace = true
|
||||
project.workspace = true
|
||||
prompt_store.workspace = true
|
||||
sandbox.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
|
|
|||
|
|
@ -16,7 +16,9 @@ use gpui::{
|
|||
};
|
||||
use itertools::Itertools;
|
||||
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};
|
||||
pub use mention::*;
|
||||
use project::lsp_store::{FormatTrigger, LspFormatTarget};
|
||||
|
|
@ -773,6 +775,7 @@ impl ContentBlock {
|
|||
None,
|
||||
MarkdownOptions {
|
||||
render_mermaid_diagrams: true,
|
||||
render_metadata_blocks: true,
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
|
|
@ -2912,7 +2915,9 @@ impl AcpThread {
|
|||
});
|
||||
|
||||
let format_on_save = buffer.update(cx, |buffer, cx| {
|
||||
buffer.start_transaction();
|
||||
buffer.edit(edits, None, cx);
|
||||
buffer.end_transaction_with_source(BufferEditSource::Agent, cx);
|
||||
|
||||
let settings =
|
||||
language::language_settings::LanguageSettings::for_buffer(buffer, cx);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
use agent_client_protocol::schema as acp;
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
use file_icons::FileIcons;
|
||||
use prompt_store::{PromptId, UserPromptId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
|
|
@ -37,10 +36,6 @@ pub enum MentionUri {
|
|||
id: acp::SessionId,
|
||||
name: String,
|
||||
},
|
||||
Rule {
|
||||
id: PromptId,
|
||||
name: String,
|
||||
},
|
||||
Diagnostics {
|
||||
#[serde(default = "default_include_errors")]
|
||||
include_errors: bool,
|
||||
|
|
@ -205,13 +200,6 @@ impl MentionUri {
|
|||
id: acp::SessionId::new(thread_id),
|
||||
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" {
|
||||
let mut include_errors = default_include_errors();
|
||||
let mut include_warnings = false;
|
||||
|
|
@ -342,7 +330,6 @@ impl MentionUri {
|
|||
MentionUri::PastedImage { name } => name.clone(),
|
||||
MentionUri::Symbol { name, .. } => name.clone(),
|
||||
MentionUri::Thread { name, .. } => name.clone(),
|
||||
MentionUri::Rule { name, .. } => name.clone(),
|
||||
MentionUri::Diagnostics { .. } => "Diagnostics".to_string(),
|
||||
MentionUri::TerminalSelection { line_count } => {
|
||||
if *line_count == 1 {
|
||||
|
|
@ -443,7 +430,6 @@ impl MentionUri {
|
|||
.unwrap_or_else(|| IconName::Folder.path().into()),
|
||||
MentionUri::Symbol { .. } => IconName::Code.path().into(),
|
||||
MentionUri::Thread { .. } => IconName::Thread.path().into(),
|
||||
MentionUri::Rule { .. } => IconName::Reader.path().into(),
|
||||
MentionUri::Diagnostics { .. } => IconName::Warning.path().into(),
|
||||
MentionUri::TerminalSelection { .. } => IconName::Terminal.path().into(),
|
||||
MentionUri::Selection { .. } => IconName::Reader.path().into(),
|
||||
|
|
@ -526,12 +512,6 @@ impl MentionUri {
|
|||
url.query_pairs_mut().append_pair("name", name);
|
||||
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 {
|
||||
include_errors,
|
||||
include_warnings,
|
||||
|
|
@ -811,20 +791,6 @@ mod tests {
|
|||
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]
|
||||
fn test_parse_skill_uri_round_trip() {
|
||||
let skill_uri = MentionUri::Skill {
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ zed_env_vars.workspace = true
|
|||
zstd.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
assets.workspace = true
|
||||
async-io.workspace = true
|
||||
agent_servers = { 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"] }
|
||||
|
||||
theme = { workspace = true, "features" = ["test-support"] }
|
||||
theme_settings.workspace = true
|
||||
|
||||
unindent = { workspace = true }
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use std::{
|
||||
any::Any,
|
||||
future::Future,
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
|
|
@ -14,26 +15,40 @@ use agent_settings::{AgentSettings, ToolRules};
|
|||
use criterion::{
|
||||
BatchSize, BenchmarkId, Criterion, Throughput, black_box, criterion_group, criterion_main,
|
||||
};
|
||||
use futures::{pin_mut, task::noop_waker};
|
||||
use gpui::{AppContext as _, BackgroundExecutor, Entity, TestAppContext, UpdateGlobal as _};
|
||||
use editor::{Editor, EditorStyle};
|
||||
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 project::{FakeFs, Project};
|
||||
use prompt_store::ProjectContext;
|
||||
use rand::{Rng as _, SeedableRng as _, rngs::StdRng};
|
||||
use serde_json::{Value, json};
|
||||
use settings::{Settings as _, SettingsStore};
|
||||
use ui::IntoElement as _;
|
||||
|
||||
const SEED: u64 = 0x5EED_5EED;
|
||||
const OLD_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)]
|
||||
struct EditFixture {
|
||||
name: &'static str,
|
||||
old_file_text: String,
|
||||
expected_file_text: String,
|
||||
old_text: String,
|
||||
new_text: String,
|
||||
edits: Vec<EditOp>,
|
||||
}
|
||||
|
||||
struct BenchmarkHarness {
|
||||
|
|
@ -43,6 +58,12 @@ struct BenchmarkHarness {
|
|||
partial_payloads: Vec<Value>,
|
||||
final_payload: Value,
|
||||
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 {
|
||||
|
|
@ -50,19 +71,18 @@ impl Drop for BenchmarkHarness {
|
|||
// Release our handles to the entities first.
|
||||
self.edit_tool.take();
|
||||
self.thread.take();
|
||||
self.editor.take();
|
||||
self.keep_alive.clear();
|
||||
|
||||
if let Some(cx) = self.cx.take() {
|
||||
// `ActionLog` holds buffers strongly via `tracked_buffers`, and spawns a background
|
||||
// diff-maintenance task that also captures a strong `Entity<Buffer>`. Releasing the
|
||||
// last handle to the action log only marks its entity for deferred release; the
|
||||
// entity's value (and the buffer handles inside) is not actually dropped until
|
||||
// `flush_effects` runs `release_dropped_entities`. Even then, the cancelled task's
|
||||
// captured handle does not drop until the executor pumps the cancellation through.
|
||||
//
|
||||
// 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`.
|
||||
if let Some(mut cx) = self.cx.take() {
|
||||
// Close the editor window so the editor entity and the buffer handles
|
||||
// it holds are released, then pump the executor so cancelled editor /
|
||||
// action-log background tasks drop their captured handles before the
|
||||
// leak detector runs on `TestAppContext` drop.
|
||||
if let Some(window) = self.window.take() {
|
||||
cx.update_window(window, |_, window, _| window.remove_window())
|
||||
.ok();
|
||||
}
|
||||
cx.update(|_| {});
|
||||
cx.executor().run_until_parked();
|
||||
cx.quit();
|
||||
|
|
@ -76,9 +96,10 @@ fn edit_file_tool_streaming(c: &mut Criterion) {
|
|||
group.sample_size(10);
|
||||
|
||||
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(
|
||||
BenchmarkId::new(fixture.name, fixture.old_text.len()),
|
||||
BenchmarkId::new(fixture.name, fixture.old_file_text.len()),
|
||||
&fixture,
|
||||
|bench, fixture| {
|
||||
bench.iter_batched(
|
||||
|
|
@ -107,26 +128,168 @@ fn edit_file_tool_streaming(c: &mut Criterion) {
|
|||
fn setup_harness(fixture: EditFixture) -> BenchmarkHarness {
|
||||
let mut cx = init_context();
|
||||
let executor = cx.executor();
|
||||
let (edit_tool, thread) = block_on_executor(
|
||||
let parts = block_on_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!({
|
||||
"path": "root/src/workspace_snapshot.rs",
|
||||
"edits": [{
|
||||
"old_text": fixture.old_text,
|
||||
"new_text": fixture.new_text,
|
||||
}],
|
||||
"path": FILE_PROJECT_PATH,
|
||||
"edits": fixture
|
||||
.edits
|
||||
.iter()
|
||||
.map(|edit| json!({ "old_text": edit.old_text, "new_text": edit.new_text }))
|
||||
.collect::<Vec<_>>(),
|
||||
});
|
||||
|
||||
BenchmarkHarness {
|
||||
cx: Some(cx),
|
||||
edit_tool: Some(edit_tool),
|
||||
thread: Some(thread),
|
||||
edit_tool: Some(parts.edit_tool),
|
||||
thread: Some(parts.thread),
|
||||
partial_payloads,
|
||||
final_payload,
|
||||
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| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
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| {
|
||||
store.update_user_settings(cx, |settings| {
|
||||
settings
|
||||
|
|
@ -142,6 +308,7 @@ fn init_context() -> TestAppContext {
|
|||
.all_languages
|
||||
.defaults
|
||||
.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
|
||||
}
|
||||
|
||||
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 {
|
||||
let (mut sender, input): (_, ToolInput<EditFileToolInput>) = ToolInput::test();
|
||||
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");
|
||||
}
|
||||
|
||||
fn streamed_partial_payloads(old_text: &str, new_text: &str) -> Vec<Value> {
|
||||
let path = "root/src/workspace_snapshot.rs";
|
||||
let mut payloads = Vec::new();
|
||||
/// Builds the streamed partial payloads for a (possibly multi-edit) session,
|
||||
/// mirroring how the agent reveals one edit at a time: earlier edits stay
|
||||
/// 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 }));
|
||||
payloads.push(json!({ "path": path }));
|
||||
for index in 0..edits.len() {
|
||||
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) {
|
||||
payloads.push(json!({
|
||||
"path": path,
|
||||
"edits": [{ "old_text": &old_text[..old_end] }],
|
||||
}));
|
||||
}
|
||||
for old_end in chunk_ends(&edit.old_text, OLD_TEXT_CHUNK_SIZE) {
|
||||
let mut arr = completed.clone();
|
||||
arr.push(json!({ "old_text": &edit.old_text[..old_end] }));
|
||||
payloads.push(json!({ "path": path, "edits": arr }));
|
||||
}
|
||||
|
||||
payloads.push(json!({
|
||||
"path": path,
|
||||
"edits": [{ "old_text": old_text, "new_text": "" }],
|
||||
}));
|
||||
let mut arr = completed.clone();
|
||||
arr.push(json!({ "old_text": edit.old_text, "new_text": "" }));
|
||||
payloads.push(json!({ "path": path, "edits": arr }));
|
||||
|
||||
for new_end in chunk_ends(new_text, NEW_TEXT_CHUNK_SIZE) {
|
||||
payloads.push(json!({
|
||||
"path": path,
|
||||
"edits": [{
|
||||
"old_text": old_text,
|
||||
"new_text": &new_text[..new_end],
|
||||
}],
|
||||
}));
|
||||
for new_end in chunk_ends(&edit.new_text, NEW_TEXT_CHUNK_SIZE) {
|
||||
let mut arr = completed.clone();
|
||||
arr.push(json!({ "old_text": edit.old_text, "new_text": &edit.new_text[..new_end] }));
|
||||
payloads.push(json!({ "path": path, "edits": arr }));
|
||||
}
|
||||
}
|
||||
|
||||
payloads
|
||||
|
|
@ -326,6 +454,7 @@ fn fixtures() -> Vec<EditFixture> {
|
|||
EditPattern::InsertHelperBlocks { every_nth_line: 9 },
|
||||
SEED + 3,
|
||||
),
|
||||
make_large_multi_edit_fixture("large_multi_edit", 80, 16, SEED + 4),
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -375,11 +504,106 @@ fn make_fixture(
|
|||
name,
|
||||
old_file_text,
|
||||
expected_file_text,
|
||||
old_text,
|
||||
new_text,
|
||||
edits: vec![EditOp { old_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> {
|
||||
let mut range = match pattern {
|
||||
EditPattern::LocalizedRewrite {
|
||||
|
|
|
|||
|
|
@ -316,17 +316,6 @@ impl UserMessage {
|
|||
MentionUri::Thread { .. } => {
|
||||
write!(&mut thread_context, "\n{}\n", content).ok();
|
||||
}
|
||||
MentionUri::Rule { .. } => {
|
||||
write!(
|
||||
&mut rules_context,
|
||||
"\n{}",
|
||||
MarkdownCodeBlock {
|
||||
tag: "",
|
||||
text: content
|
||||
}
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
MentionUri::Fetch { url } => {
|
||||
write!(&mut fetch_context, "\nFetch: {}\n\n{}", url, content).ok();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use collections::HashSet;
|
|||
use futures::{FutureExt, channel::oneshot};
|
||||
use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity};
|
||||
use language::language_settings::{self, FormatOnSave};
|
||||
use language::{Buffer, BufferEvent, LanguageRegistry};
|
||||
use language::{Buffer, BufferEditSource, BufferEvent, LanguageRegistry};
|
||||
use language_model::LanguageModelToolResultContent;
|
||||
use project::lsp_store::{FormatTrigger, LspFormatTarget};
|
||||
use project::{AgentLocation, Project, ProjectPath};
|
||||
|
|
@ -620,21 +620,14 @@ impl EditPipeline {
|
|||
|
||||
log::debug!("new_text_chunk: done=true, final_text='{}'", final_text);
|
||||
|
||||
if !final_text.is_empty() {
|
||||
let char_ops = streaming_diff.push_new(&final_text);
|
||||
apply_char_operations(
|
||||
&char_ops,
|
||||
buffer,
|
||||
&original_snapshot,
|
||||
&mut edit_cursor,
|
||||
&context.action_log,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
let remaining_ops = streaming_diff.finish();
|
||||
let mut char_ops = if final_text.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
streaming_diff.push_new(&final_text)
|
||||
};
|
||||
char_ops.extend(streaming_diff.finish());
|
||||
apply_char_operations(
|
||||
&remaining_ops,
|
||||
&char_ops,
|
||||
buffer,
|
||||
&original_snapshot,
|
||||
&mut edit_cursor,
|
||||
|
|
@ -902,16 +895,17 @@ fn apply_char_operations(
|
|||
action_log: &Entity<ActionLog>,
|
||||
cx: &mut AsyncApp,
|
||||
) {
|
||||
let mut edits: Vec<_> = Vec::new();
|
||||
for op in ops {
|
||||
match op {
|
||||
CharOperation::Insert { text } => {
|
||||
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 } => {
|
||||
let delete_end = *edit_cursor + bytes;
|
||||
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;
|
||||
}
|
||||
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(
|
||||
|
|
@ -975,7 +972,9 @@ fn agent_edit_buffer<I, S, T>(
|
|||
{
|
||||
cx.update(|cx| {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.start_transaction();
|
||||
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));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ path = "agent_skills.rs"
|
|||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
base64.workspace = true
|
||||
const_format.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
|
|
@ -20,6 +21,7 @@ gpui.workspace = true
|
|||
paths.workspace = true
|
||||
serde.workspace = true
|
||||
serde_yaml_ng.workspace = true
|
||||
url.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use gpui::{Global, SharedString};
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use url::Url;
|
||||
use util::paths::component_matches_ignore_ascii_case;
|
||||
|
||||
/// First segment of the skills directory path: `.agents`.
|
||||
|
|
@ -731,6 +732,58 @@ pub fn is_agents_skills_path(path: &Path) -> bool {
|
|||
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)]
|
||||
mod tests {
|
||||
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_connection.workspace = true
|
||||
rope.workspace = true
|
||||
rules_library.workspace = true
|
||||
skill_creator.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
|
|
|
|||
|
|
@ -38,7 +38,10 @@ use crate::ExpandMessageEditor;
|
|||
use crate::ManageProfiles;
|
||||
use crate::agent_connection_store::AgentConnectionStore;
|
||||
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::{
|
||||
AddContextServer, AgentDiffPane, ConversationView, CopyThreadToClipboard, Follow,
|
||||
|
|
@ -75,7 +78,6 @@ use gpui::{
|
|||
use language::LanguageRegistry;
|
||||
use language_model::LanguageModelRegistry;
|
||||
use project::{Project, ProjectPath, Worktree};
|
||||
use prompt_store::PromptStore;
|
||||
use settings::TerminalDockPosition;
|
||||
use settings::{NotifyWhenAgentWaiting, Settings, update_settings_file};
|
||||
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_subscription: Option<Subscription>,
|
||||
last_known_title: String,
|
||||
last_known_terminal_title: String,
|
||||
last_observed_program: Option<String>,
|
||||
working_directory: Option<PathBuf>,
|
||||
created_at: DateTime<Utc>,
|
||||
|
|
@ -880,32 +883,58 @@ struct AgentTerminal {
|
|||
}
|
||||
|
||||
impl AgentTerminal {
|
||||
fn title(&self, 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);
|
||||
if terminal.breadcrumb_text.is_empty() {
|
||||
let title = terminal.title(true);
|
||||
if title == "Terminal" {
|
||||
SharedString::from("")
|
||||
} else {
|
||||
title.into()
|
||||
}
|
||||
fn terminal_title_for_view(view: &TerminalView, cx: &App) -> SharedString {
|
||||
let terminal = view.terminal().read(cx);
|
||||
if terminal.breadcrumb_text.is_empty() {
|
||||
let title = terminal.title(true);
|
||||
if title == "Terminal" {
|
||||
SharedString::from("")
|
||||
} else {
|
||||
terminal.breadcrumb_text.clone().into()
|
||||
title.into()
|
||||
}
|
||||
};
|
||||
} else {
|
||||
terminal.breadcrumb_text.clone().into()
|
||||
}
|
||||
}
|
||||
|
||||
if title.is_empty() && !self.last_known_title.is_empty() {
|
||||
SharedString::from(self.last_known_title.clone())
|
||||
fn current_terminal_title(&self, cx: &App) -> SharedString {
|
||||
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 {
|
||||
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 {
|
||||
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 changed = self.last_known_title != title.as_ref();
|
||||
if changed {
|
||||
|
|
@ -1019,7 +1048,6 @@ pub struct AgentPanel {
|
|||
fs: Arc<dyn Fs>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
thread_store: Entity<ThreadStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
connection_store: Entity<AgentConnectionStore>,
|
||||
context_server_registry: Entity<ContextServerRegistry>,
|
||||
configuration: Option<Entity<AgentConfiguration>>,
|
||||
|
|
@ -1140,13 +1168,8 @@ impl AgentPanel {
|
|||
workspace: WeakEntity<Workspace>,
|
||||
mut cx: AsyncWindowContext,
|
||||
) -> Task<Result<Entity<Self>>> {
|
||||
let prompt_store = cx.update(|_window, cx| PromptStore::global(cx));
|
||||
let kvp = cx.update(|_window, cx| KeyValueStore::global(cx)).ok();
|
||||
cx.spawn(async move |cx| {
|
||||
let prompt_store = match prompt_store {
|
||||
Ok(prompt_store) => prompt_store.await.ok(),
|
||||
Err(_) => None,
|
||||
};
|
||||
let workspace_id = workspace
|
||||
.read_with(cx, |workspace, _| workspace.database_id())
|
||||
.ok()
|
||||
|
|
@ -1271,7 +1294,7 @@ impl AgentPanel {
|
|||
};
|
||||
|
||||
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| {
|
||||
let is_via_collab = panel.project.read(cx).is_via_collab();
|
||||
|
|
@ -1351,12 +1374,7 @@ impl AgentPanel {
|
|||
})
|
||||
}
|
||||
|
||||
pub(crate) fn new(
|
||||
workspace: &Workspace,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
pub(crate) fn new(workspace: &Workspace, _window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
let user_store = workspace.app_state().user_store.clone();
|
||||
let project = workspace.project();
|
||||
|
|
@ -1438,7 +1456,6 @@ impl AgentPanel {
|
|||
project: project.clone(),
|
||||
fs: fs.clone(),
|
||||
language_registry,
|
||||
prompt_store,
|
||||
connection_store,
|
||||
configuration: 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> {
|
||||
&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 {
|
||||
view: terminal_view,
|
||||
title_editor: None,
|
||||
title_editor_initial_title: None,
|
||||
title_editor_subscription: None,
|
||||
last_known_title: initial_title
|
||||
.map(|title| title.to_string())
|
||||
.unwrap_or_default(),
|
||||
last_known_title: last_known_terminal_title.clone(),
|
||||
last_known_terminal_title,
|
||||
last_observed_program: None,
|
||||
working_directory,
|
||||
created_at: created_at.unwrap_or_else(Utc::now),
|
||||
|
|
@ -2164,7 +2179,7 @@ impl AgentPanel {
|
|||
let project = self.project.read(cx);
|
||||
Some(TerminalThreadMetadata {
|
||||
terminal_id,
|
||||
title: terminal.title(cx),
|
||||
title: terminal.terminal_title(cx),
|
||||
custom_title: terminal.custom_title(cx),
|
||||
created_at: terminal.created_at,
|
||||
worktree_paths: project.worktree_paths(cx),
|
||||
|
|
@ -2242,10 +2257,7 @@ impl AgentPanel {
|
|||
}
|
||||
|
||||
fn terminal_restore_initial_title(metadata: &TerminalThreadMetadata) -> Option<SharedString> {
|
||||
metadata
|
||||
.custom_title
|
||||
.clone()
|
||||
.or_else(|| (!metadata.title.is_empty()).then(|| metadata.title.clone()))
|
||||
(!metadata.title.is_empty()).then(|| metadata.title.clone())
|
||||
}
|
||||
|
||||
fn edit_terminal_title(
|
||||
|
|
@ -2263,7 +2275,7 @@ impl AgentPanel {
|
|||
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 = cx.new(|cx| {
|
||||
let mut editor = Editor::single_line(window, cx);
|
||||
|
|
@ -2331,7 +2343,7 @@ impl AgentPanel {
|
|||
if !title_editor.read(cx).is_focused(window) {
|
||||
return;
|
||||
}
|
||||
let Some((terminal_view, initial_title)) =
|
||||
let Some((terminal_view, initial_title, terminal_title)) =
|
||||
self.terminals.get(&terminal_id).and_then(|terminal| {
|
||||
terminal
|
||||
.title_editor
|
||||
|
|
@ -2341,25 +2353,23 @@ impl AgentPanel {
|
|||
(
|
||||
terminal.view.clone(),
|
||||
terminal.title_editor_initial_title.clone(),
|
||||
terminal.terminal_title(cx),
|
||||
)
|
||||
})
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let new_title = title_editor.read(cx).text(cx).trim().to_string();
|
||||
if initial_title.as_deref().map(str::trim) == Some(new_title.as_str()) {
|
||||
let new_title = title_editor.read(cx).text(cx);
|
||||
if initial_title.as_deref() == Some(new_title.as_str()) {
|
||||
return;
|
||||
}
|
||||
let label = if new_title.is_empty() {
|
||||
let label = if new_title.trim().is_empty()
|
||||
|| new_title == terminal_title_without_prefix(terminal_title.as_ref())
|
||||
{
|
||||
None
|
||||
} else {
|
||||
let terminal_title = terminal_view.read(cx).terminal().read(cx).title(true);
|
||||
if new_title == terminal_title {
|
||||
None
|
||||
} else {
|
||||
Some(new_title)
|
||||
}
|
||||
Some(new_title)
|
||||
};
|
||||
|
||||
cx.defer(move |cx| {
|
||||
|
|
@ -3251,6 +3261,13 @@ impl AgentPanel {
|
|||
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>) {
|
||||
let this = cx.weak_entity();
|
||||
let on_saved: Rc<dyn Fn(&mut App)> = Rc::new(move |cx: &mut App| {
|
||||
|
|
@ -4361,7 +4378,6 @@ impl AgentPanel {
|
|||
workspace.clone(),
|
||||
project,
|
||||
thread_store,
|
||||
self.prompt_store.clone(),
|
||||
source,
|
||||
window,
|
||||
cx,
|
||||
|
|
@ -6053,7 +6069,7 @@ impl Dismissable for TrialEndUpsell {
|
|||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl AgentPanel {
|
||||
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
|
||||
|
|
@ -6560,7 +6576,7 @@ mod tests {
|
|||
|
||||
// Set up workspace A: with an active thread.
|
||||
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| {
|
||||
|
|
@ -6586,7 +6602,7 @@ mod tests {
|
|||
|
||||
// Set up workspace B: ClaudeCode, no active thread.
|
||||
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| {
|
||||
|
|
@ -6689,7 +6705,7 @@ mod tests {
|
|||
|
||||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), 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| {
|
||||
|
|
@ -6884,7 +6900,7 @@ mod tests {
|
|||
|
||||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), 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| {
|
||||
|
|
@ -6961,7 +6977,7 @@ mod tests {
|
|||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), 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| {
|
||||
|
|
@ -7053,7 +7069,7 @@ mod tests {
|
|||
});
|
||||
|
||||
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
|
||||
|
|
@ -7152,7 +7168,7 @@ mod tests {
|
|||
.unwrap();
|
||||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), 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);
|
||||
panel
|
||||
});
|
||||
|
|
@ -7252,12 +7268,12 @@ mod tests {
|
|||
|
||||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), 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);
|
||||
panel
|
||||
});
|
||||
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);
|
||||
panel
|
||||
});
|
||||
|
|
@ -7632,7 +7648,7 @@ mod tests {
|
|||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), 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);
|
||||
panel
|
||||
});
|
||||
|
|
@ -7819,7 +7835,7 @@ mod tests {
|
|||
|
||||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), 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);
|
||||
panel
|
||||
});
|
||||
|
|
@ -8048,7 +8064,7 @@ mod tests {
|
|||
|
||||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), 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);
|
||||
panel
|
||||
});
|
||||
|
|
@ -8134,7 +8150,7 @@ mod tests {
|
|||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), 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);
|
||||
panel
|
||||
});
|
||||
|
|
@ -8224,7 +8240,7 @@ mod tests {
|
|||
let mut cx = VisualTestContext::from_window(multi_workspace.into(), 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)
|
||||
|
|
@ -8271,7 +8287,7 @@ mod tests {
|
|||
register_test_sidebar(threads_list_active, &mut 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.focus_panel::<AgentPanel>(window, cx);
|
||||
panel
|
||||
|
|
@ -8401,7 +8417,7 @@ mod tests {
|
|||
let mut cx = VisualTestContext::from_window(multi_workspace.into(), 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);
|
||||
panel
|
||||
});
|
||||
|
|
@ -8514,7 +8530,7 @@ mod tests {
|
|||
let mut cx = VisualTestContext::from_window(multi_workspace.into(), 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);
|
||||
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]
|
||||
async fn test_terminal_bell_marks_and_activation_clears_notification(cx: &mut TestAppContext) {
|
||||
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 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);
|
||||
panel
|
||||
});
|
||||
|
|
@ -10173,7 +10365,7 @@ mod tests {
|
|||
let mut cx = VisualTestContext::from_window(multi_workspace.into(), 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
|
||||
|
|
@ -10442,7 +10634,7 @@ mod tests {
|
|||
|
||||
// Set up workspace A with agent_a
|
||||
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.selected_agent = agent_a.clone();
|
||||
|
|
@ -10450,7 +10642,7 @@ mod tests {
|
|||
|
||||
// Set up workspace B with agent_b
|
||||
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.selected_agent = agent_b.clone();
|
||||
|
|
@ -10521,7 +10713,7 @@ mod tests {
|
|||
};
|
||||
|
||||
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);
|
||||
panel
|
||||
});
|
||||
|
|
@ -10578,7 +10770,7 @@ mod tests {
|
|||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), 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);
|
||||
panel
|
||||
});
|
||||
|
|
@ -10668,7 +10860,7 @@ mod tests {
|
|||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), 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);
|
||||
panel
|
||||
});
|
||||
|
|
@ -10756,7 +10948,7 @@ mod tests {
|
|||
workspace.update(cx, |workspace, _cx| workspace.set_random_database_id());
|
||||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), 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);
|
||||
panel
|
||||
});
|
||||
|
|
@ -10866,7 +11058,7 @@ mod tests {
|
|||
workspace.update(cx, |workspace, _cx| workspace.set_random_database_id());
|
||||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), 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);
|
||||
panel
|
||||
});
|
||||
|
|
@ -10972,7 +11164,7 @@ mod tests {
|
|||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), 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);
|
||||
panel
|
||||
});
|
||||
|
|
@ -11471,7 +11663,7 @@ mod tests {
|
|||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), 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();
|
||||
|
|
@ -11572,7 +11764,7 @@ mod tests {
|
|||
|
||||
// Create the agent panel and add it to the workspace.
|
||||
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);
|
||||
panel
|
||||
});
|
||||
|
|
@ -11782,7 +11974,7 @@ mod tests {
|
|||
let mut cx = VisualTestContext::from_window(multi_workspace.into(), 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);
|
||||
panel
|
||||
});
|
||||
|
|
@ -12019,7 +12211,7 @@ mod tests {
|
|||
|
||||
// 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 = 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);
|
||||
panel
|
||||
});
|
||||
|
|
@ -12043,7 +12235,7 @@ mod tests {
|
|||
|
||||
// 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 = 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);
|
||||
panel
|
||||
});
|
||||
|
|
@ -12113,7 +12305,7 @@ mod tests {
|
|||
|
||||
// Set up panel_a with draft text.
|
||||
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);
|
||||
panel
|
||||
});
|
||||
|
|
@ -12137,7 +12329,7 @@ mod tests {
|
|||
|
||||
// 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 = 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);
|
||||
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,
|
||||
};
|
||||
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 schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
|
@ -550,7 +550,7 @@ pub fn init(
|
|||
cx: &mut App,
|
||||
) {
|
||||
agent::ThreadStore::init_global(cx);
|
||||
rules_library::init(cx);
|
||||
prompt_store::init(cx);
|
||||
skill_creator::init(cx);
|
||||
if !is_eval {
|
||||
// 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,
|
||||
};
|
||||
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::{
|
||||
CompletionIntent, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
|
||||
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
|
|
@ -978,7 +980,7 @@ impl CodegenAlternative {
|
|||
buffer.finalize_last_transaction(cx);
|
||||
buffer.start_transaction(cx);
|
||||
buffer.edit(edits, None, cx);
|
||||
buffer.end_transaction(cx)
|
||||
buffer.end_transaction_with_source(BufferEditSource::Agent, cx)
|
||||
});
|
||||
|
||||
if let Some(transaction) = transaction {
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ use markdown::{
|
|||
};
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use project::{AgentId, AgentServerStore, Project, ProjectEntryId, ProjectPath};
|
||||
use prompt_store::{PromptId, PromptStore};
|
||||
|
||||
use crate::message_editor::SessionCapabilities;
|
||||
use crate::{AgentThreadSource, DEFAULT_THREAD_TITLE, resolve_agent_image};
|
||||
|
|
@ -75,7 +74,6 @@ use workspace::{
|
|||
path_link::sanitize_path_text,
|
||||
};
|
||||
use zed_actions::agent::{Chat, ToggleModelSelector};
|
||||
use zed_actions::assistant::OpenRulesLibrary;
|
||||
|
||||
use super::config_options::ConfigOptionsView;
|
||||
use super::entry_view_state::EntryViewState;
|
||||
|
|
@ -531,7 +529,6 @@ pub struct ConversationView {
|
|||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
thread_store: Option<Entity<ThreadStore>>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
pub(crate) thread_id: ThreadId,
|
||||
pub(crate) root_session_id: Option<acp::SessionId>,
|
||||
server_state: ServerState,
|
||||
|
|
@ -738,7 +735,6 @@ impl ConversationView {
|
|||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
thread_store: Option<Entity<ThreadStore>>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
source: AgentThreadSource,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
|
|
@ -795,7 +791,6 @@ impl ConversationView {
|
|||
workspace,
|
||||
project: project.clone(),
|
||||
thread_store,
|
||||
prompt_store,
|
||||
thread_id,
|
||||
root_session_id: resume_session_id.clone(),
|
||||
server_state: Self::initial_state(
|
||||
|
|
@ -1104,7 +1099,6 @@ impl ConversationView {
|
|||
self.workspace.clone(),
|
||||
self.project.downgrade(),
|
||||
self.thread_store.clone(),
|
||||
self.prompt_store.clone(),
|
||||
session_capabilities.clone(),
|
||||
self.agent.agent_id(),
|
||||
)
|
||||
|
|
@ -1273,7 +1267,6 @@ impl ConversationView {
|
|||
self.project.downgrade(),
|
||||
self.code_span_resolver.clone(),
|
||||
self.thread_store.clone(),
|
||||
self.prompt_store.clone(),
|
||||
initial_content,
|
||||
subscriptions,
|
||||
window,
|
||||
|
|
@ -2492,7 +2485,6 @@ impl ConversationView {
|
|||
workspace.clone(),
|
||||
project.clone(),
|
||||
None,
|
||||
None,
|
||||
session_capabilities.clone(),
|
||||
agent_name.clone(),
|
||||
"",
|
||||
|
|
@ -3721,7 +3713,6 @@ pub(crate) mod tests {
|
|||
workspace.downgrade(),
|
||||
project,
|
||||
Some(thread_store),
|
||||
None,
|
||||
AgentThreadSource::AgentPanel,
|
||||
window,
|
||||
cx,
|
||||
|
|
@ -3858,7 +3849,6 @@ pub(crate) mod tests {
|
|||
workspace.downgrade(),
|
||||
project,
|
||||
Some(thread_store),
|
||||
None,
|
||||
AgentThreadSource::AgentPanel,
|
||||
window,
|
||||
cx,
|
||||
|
|
@ -3940,7 +3930,6 @@ pub(crate) mod tests {
|
|||
workspace.downgrade(),
|
||||
project,
|
||||
Some(thread_store),
|
||||
None,
|
||||
AgentThreadSource::AgentPanel,
|
||||
window,
|
||||
cx,
|
||||
|
|
@ -4079,7 +4068,6 @@ pub(crate) mod tests {
|
|||
workspace.downgrade(),
|
||||
project.clone(),
|
||||
Some(thread_store),
|
||||
None,
|
||||
AgentThreadSource::AgentPanel,
|
||||
window,
|
||||
cx,
|
||||
|
|
@ -4364,7 +4352,7 @@ pub(crate) mod tests {
|
|||
let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), 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.focus_panel::<crate::AgentPanel>(window, cx);
|
||||
panel
|
||||
|
|
@ -4405,7 +4393,6 @@ pub(crate) mod tests {
|
|||
workspace.downgrade(),
|
||||
project.clone(),
|
||||
Some(thread_store),
|
||||
None,
|
||||
AgentThreadSource::AgentPanel,
|
||||
window,
|
||||
cx,
|
||||
|
|
@ -4504,7 +4491,6 @@ pub(crate) mod tests {
|
|||
workspace.downgrade(),
|
||||
project.clone(),
|
||||
Some(thread_store),
|
||||
None,
|
||||
AgentThreadSource::AgentPanel,
|
||||
window,
|
||||
cx,
|
||||
|
|
@ -4580,7 +4566,6 @@ pub(crate) mod tests {
|
|||
workspace.downgrade(),
|
||||
project.clone(),
|
||||
Some(thread_store),
|
||||
None,
|
||||
AgentThreadSource::AgentPanel,
|
||||
window,
|
||||
cx,
|
||||
|
|
@ -4648,7 +4633,6 @@ pub(crate) mod tests {
|
|||
workspace.downgrade(),
|
||||
project.clone(),
|
||||
Some(thread_store),
|
||||
None,
|
||||
AgentThreadSource::AgentPanel,
|
||||
window,
|
||||
cx,
|
||||
|
|
@ -4724,7 +4708,7 @@ pub(crate) mod tests {
|
|||
let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), 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);
|
||||
|
||||
// Open the dock and activate the agent panel so it's visible
|
||||
|
|
@ -4770,7 +4754,6 @@ pub(crate) mod tests {
|
|||
workspace1.downgrade(),
|
||||
project1.clone(),
|
||||
Some(thread_store),
|
||||
None,
|
||||
AgentThreadSource::AgentPanel,
|
||||
window,
|
||||
cx,
|
||||
|
|
@ -4992,7 +4975,6 @@ pub(crate) mod tests {
|
|||
workspace.downgrade(),
|
||||
project,
|
||||
Some(thread_store),
|
||||
None,
|
||||
AgentThreadSource::AgentPanel,
|
||||
window,
|
||||
cx,
|
||||
|
|
@ -5651,7 +5633,6 @@ pub(crate) mod tests {
|
|||
workspace.downgrade(),
|
||||
project.clone(),
|
||||
Some(thread_store.clone()),
|
||||
None,
|
||||
AgentThreadSource::AgentPanel,
|
||||
window,
|
||||
cx,
|
||||
|
|
@ -8113,9 +8094,17 @@ pub(crate) mod tests {
|
|||
async fn test_permission_row_hidden_when_inline_bounds_unavailable(cx: &mut TestAppContext) {
|
||||
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;
|
||||
|
||||
// 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| {
|
||||
assert!(
|
||||
view.render_main_agent_awaiting_permission(window, cx)
|
||||
|
|
@ -8176,8 +8165,8 @@ pub(crate) mod tests {
|
|||
let (_view, thread_view, entry_ix, cx) =
|
||||
setup_pending_permission_thread("perm-scroll", cx).await;
|
||||
|
||||
// Start off-screen below the viewport — row visible because the item
|
||||
// has bounds that do not intersect the viewport.
|
||||
// Start off-screen below the viewport. The row is visible because the
|
||||
// item has bounds that do not intersect the viewport.
|
||||
draw_thread_list_at(
|
||||
&thread_view,
|
||||
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]
|
||||
async fn test_permission_row_disappears_when_authorized(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
|
@ -8556,7 +8608,6 @@ pub(crate) mod tests {
|
|||
workspace.downgrade(),
|
||||
project,
|
||||
Some(thread_store),
|
||||
None,
|
||||
AgentThreadSource::AgentPanel,
|
||||
window,
|
||||
cx,
|
||||
|
|
|
|||
|
|
@ -683,7 +683,6 @@ impl ThreadView {
|
|||
project: WeakEntity<Project>,
|
||||
code_span_resolver: AgentCodeSpanResolver,
|
||||
thread_store: Option<Entity<ThreadStore>>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
initial_content: Option<AgentInitialContent>,
|
||||
mut subscriptions: Vec<Subscription>,
|
||||
window: &mut Window,
|
||||
|
|
@ -703,7 +702,6 @@ impl ThreadView {
|
|||
workspace.clone(),
|
||||
project.clone(),
|
||||
thread_store,
|
||||
prompt_store,
|
||||
session_capabilities.clone(),
|
||||
agent_id.clone(),
|
||||
&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(
|
||||
&self,
|
||||
window: &Window,
|
||||
|
|
@ -3073,9 +3062,13 @@ impl ThreadView {
|
|||
let thread = self.thread.read(cx);
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
|
||||
|
|
@ -3118,7 +3111,7 @@ impl ThreadView {
|
|||
Button::new("main-agent-permission-scroll-to", "Scroll")
|
||||
.label_size(LabelSize::Small)
|
||||
.end_icon(
|
||||
Icon::new(IconName::ArrowDown)
|
||||
Icon::new(scroll_icon)
|
||||
.size(IconSize::XSmall)
|
||||
.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 } => {
|
||||
cx.open_url(url.as_str());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,8 +10,7 @@ use gpui::{
|
|||
ScrollHandle, TextStyleRefinement, WeakEntity, Window,
|
||||
};
|
||||
use language::language_settings::SoftWrap;
|
||||
use project::{AgentId, Project};
|
||||
use prompt_store::PromptStore;
|
||||
use project::{AgentId, Project, project_settings::DiagnosticSeverity};
|
||||
use rope::Point;
|
||||
use settings::Settings as _;
|
||||
use terminal_view::TerminalView;
|
||||
|
|
@ -25,7 +24,6 @@ pub struct EntryViewState {
|
|||
workspace: WeakEntity<Workspace>,
|
||||
project: WeakEntity<Project>,
|
||||
thread_store: Option<Entity<ThreadStore>>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
entries: Vec<Entry>,
|
||||
session_capabilities: SharedSessionCapabilities,
|
||||
agent_id: AgentId,
|
||||
|
|
@ -36,7 +34,6 @@ impl EntryViewState {
|
|||
workspace: WeakEntity<Workspace>,
|
||||
project: WeakEntity<Project>,
|
||||
thread_store: Option<Entity<ThreadStore>>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
session_capabilities: SharedSessionCapabilities,
|
||||
agent_id: AgentId,
|
||||
) -> Self {
|
||||
|
|
@ -44,7 +41,6 @@ impl EntryViewState {
|
|||
workspace,
|
||||
project,
|
||||
thread_store,
|
||||
prompt_store,
|
||||
entries: Vec::new(),
|
||||
session_capabilities,
|
||||
agent_id,
|
||||
|
|
@ -86,7 +82,6 @@ impl EntryViewState {
|
|||
self.workspace.clone(),
|
||||
self.project.clone(),
|
||||
self.thread_store.clone(),
|
||||
self.prompt_store.clone(),
|
||||
self.session_capabilities.clone(),
|
||||
self.agent_id.clone(),
|
||||
"Edit message - @ to include context",
|
||||
|
|
@ -444,7 +439,8 @@ fn create_editor_diff(
|
|||
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.set_show_vertical_scrollbar(false, cx);
|
||||
editor.set_minimap_visibility(MinimapVisibility::Disabled, window, cx);
|
||||
|
|
@ -545,7 +541,6 @@ mod tests {
|
|||
workspace.downgrade(),
|
||||
project.downgrade(),
|
||||
thread_store,
|
||||
None,
|
||||
Arc::new(RwLock::new(SessionCapabilities::default())),
|
||||
"Test Agent".into(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ use language_model::{ConfigurationError, ConfiguredModel, LanguageModelRegistry}
|
|||
use multi_buffer::MultiBufferRow;
|
||||
use parking_lot::Mutex;
|
||||
use project::{DisableAiSettings, Project};
|
||||
use prompt_store::{PromptBuilder, PromptStore};
|
||||
use prompt_store::PromptBuilder;
|
||||
use settings::{Settings, SettingsStore};
|
||||
|
||||
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
|
||||
|
|
@ -228,7 +228,6 @@ impl InlineAssistant {
|
|||
};
|
||||
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 handle_assist =
|
||||
|
|
@ -240,7 +239,6 @@ impl InlineAssistant {
|
|||
cx.entity().downgrade(),
|
||||
workspace.project().downgrade(),
|
||||
thread_store,
|
||||
prompt_store,
|
||||
action.prompt.clone(),
|
||||
window,
|
||||
cx,
|
||||
|
|
@ -254,7 +252,6 @@ impl InlineAssistant {
|
|||
cx.entity().downgrade(),
|
||||
workspace.project().downgrade(),
|
||||
thread_store,
|
||||
prompt_store,
|
||||
action.prompt.clone(),
|
||||
window,
|
||||
cx,
|
||||
|
|
@ -437,7 +434,6 @@ impl InlineAssistant {
|
|||
workspace: WeakEntity<Workspace>,
|
||||
project: WeakEntity<Project>,
|
||||
thread_store: Entity<ThreadStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
initial_prompt: Option<String>,
|
||||
window: &mut Window,
|
||||
codegen_ranges: &[Range<Anchor>],
|
||||
|
|
@ -483,7 +479,6 @@ impl InlineAssistant {
|
|||
session_id,
|
||||
self.fs.clone(),
|
||||
thread_store.clone(),
|
||||
prompt_store.clone(),
|
||||
project.clone(),
|
||||
workspace.clone(),
|
||||
window,
|
||||
|
|
@ -574,7 +569,6 @@ impl InlineAssistant {
|
|||
workspace: WeakEntity<Workspace>,
|
||||
project: WeakEntity<Project>,
|
||||
thread_store: Entity<ThreadStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
initial_prompt: Option<String>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
|
|
@ -592,7 +586,6 @@ impl InlineAssistant {
|
|||
workspace,
|
||||
project,
|
||||
thread_store,
|
||||
prompt_store,
|
||||
initial_prompt,
|
||||
window,
|
||||
&codegen_ranges,
|
||||
|
|
@ -1915,7 +1908,6 @@ pub mod evals {
|
|||
workspace.downgrade(),
|
||||
project.downgrade(),
|
||||
thread_store,
|
||||
None,
|
||||
Some(prompt),
|
||||
window,
|
||||
cx,
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ use language_model::{LanguageModel, LanguageModelRegistry};
|
|||
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
|
||||
use parking_lot::Mutex;
|
||||
use project::Project;
|
||||
use prompt_store::PromptStore;
|
||||
use settings::Settings;
|
||||
use std::cmp;
|
||||
use std::ops::Range;
|
||||
|
|
@ -1237,7 +1236,6 @@ impl PromptEditor<BufferCodegen> {
|
|||
session_id: Uuid,
|
||||
fs: Arc<dyn Fs>,
|
||||
thread_store: Entity<ThreadStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
project: WeakEntity<Project>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
|
|
@ -1276,8 +1274,7 @@ impl PromptEditor<BufferCodegen> {
|
|||
editor
|
||||
});
|
||||
|
||||
let mention_set = cx
|
||||
.new(|_cx| MentionSet::new(project, Some(thread_store.clone()), prompt_store.clone()));
|
||||
let mention_set = cx.new(|_cx| MentionSet::new(project, Some(thread_store.clone())));
|
||||
|
||||
let model_selector_menu_handle = PopoverMenuHandle::default();
|
||||
|
||||
|
|
@ -1393,7 +1390,6 @@ impl PromptEditor<TerminalCodegen> {
|
|||
session_id: Uuid,
|
||||
fs: Arc<dyn Fs>,
|
||||
thread_store: Entity<ThreadStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
project: WeakEntity<Project>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
|
|
@ -1427,8 +1423,7 @@ impl PromptEditor<TerminalCodegen> {
|
|||
editor
|
||||
});
|
||||
|
||||
let mention_set = cx
|
||||
.new(|_cx| MentionSet::new(project, Some(thread_store.clone()), prompt_store.clone()));
|
||||
let mention_set = cx.new(|_cx| MentionSet::new(project, Some(thread_store.clone())));
|
||||
|
||||
let model_selector_menu_handle = PopoverMenuHandle::default();
|
||||
|
||||
|
|
@ -1705,7 +1700,6 @@ mod tests {
|
|||
session_id,
|
||||
fs,
|
||||
thread_store,
|
||||
None,
|
||||
project,
|
||||
workspace.downgrade(),
|
||||
window,
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ use language_model::{LanguageModelImage, LanguageModelImageExt};
|
|||
use multi_buffer::MultiBufferRow;
|
||||
use postage::stream::Stream as _;
|
||||
use project::{Project, ProjectItem, ProjectPath, Worktree};
|
||||
use prompt_store::{PromptId, PromptStore};
|
||||
use rope::Point;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
|
|
@ -61,21 +60,15 @@ pub struct MentionImage {
|
|||
pub struct MentionSet {
|
||||
project: WeakEntity<Project>,
|
||||
thread_store: Option<Entity<ThreadStore>>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
mentions: HashMap<CreaseId, (MentionUri, MentionTask)>,
|
||||
crease_entities: HashMap<CreaseId, Entity<LoadingContext>>,
|
||||
}
|
||||
|
||||
impl MentionSet {
|
||||
pub fn new(
|
||||
project: WeakEntity<Project>,
|
||||
thread_store: Option<Entity<ThreadStore>>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
) -> Self {
|
||||
pub fn new(project: WeakEntity<Project>, thread_store: Option<Entity<ThreadStore>>) -> Self {
|
||||
Self {
|
||||
project,
|
||||
thread_store,
|
||||
prompt_store,
|
||||
mentions: HashMap::default(),
|
||||
crease_entities: HashMap::default(),
|
||||
}
|
||||
|
|
@ -153,7 +146,6 @@ impl MentionSet {
|
|||
line_range,
|
||||
..
|
||||
} => self.confirm_mention_for_symbol(abs_path, line_range, cx),
|
||||
MentionUri::Rule { id, .. } => self.confirm_mention_for_rule(id, cx),
|
||||
MentionUri::Skill {
|
||||
skill_file_path, ..
|
||||
} => self.confirm_mention_for_skill(skill_file_path, cx),
|
||||
|
|
@ -327,7 +319,6 @@ impl MentionSet {
|
|||
line_range,
|
||||
..
|
||||
} => self.confirm_mention_for_symbol(abs_path, line_range, cx),
|
||||
MentionUri::Rule { id, .. } => self.confirm_mention_for_rule(id, cx),
|
||||
MentionUri::Skill {
|
||||
skill_file_path, ..
|
||||
} => 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(
|
||||
&mut self,
|
||||
source_range: Range<text::Anchor>,
|
||||
|
|
@ -773,7 +746,7 @@ mod tests {
|
|||
fs.insert_tree("/project", json!({"file": ""})).await;
|
||||
let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
|
||||
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| {
|
||||
mention_set.confirm_mention_for_thread(acp::SessionId::new("thread-1"), cx)
|
||||
|
|
@ -799,7 +772,7 @@ mod tests {
|
|||
)
|
||||
.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 http_client = project.read(cx).client().http_client();
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ use project::AgentId;
|
|||
use project::{
|
||||
CompletionIntent, InlayHint, InlayHintLabel, InlayId, Project, ProjectPath, Worktree,
|
||||
};
|
||||
use prompt_store::PromptStore;
|
||||
use rope::Point;
|
||||
use settings::Settings;
|
||||
use std::{cmp::min, fmt::Write, ops::Range, rc::Rc, sync::Arc};
|
||||
|
|
@ -453,7 +452,6 @@ impl MessageEditor {
|
|||
workspace: WeakEntity<Workspace>,
|
||||
project: WeakEntity<Project>,
|
||||
thread_store: Option<Entity<ThreadStore>>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
session_capabilities: SharedSessionCapabilities,
|
||||
agent_id: AgentId,
|
||||
placeholder: &str,
|
||||
|
|
@ -506,8 +504,7 @@ impl MessageEditor {
|
|||
|
||||
editor
|
||||
});
|
||||
let mention_set =
|
||||
cx.new(|_cx| MentionSet::new(project, thread_store.clone(), prompt_store.clone()));
|
||||
let mention_set = cx.new(|_cx| MentionSet::new(project, thread_store.clone()));
|
||||
let completion_provider = Rc::new(PromptCompletionProvider::new(
|
||||
MessageEditorCompletionDelegate {
|
||||
session_capabilities: session_capabilities.clone(),
|
||||
|
|
@ -2475,7 +2472,6 @@ mod tests {
|
|||
workspace.downgrade(),
|
||||
project.downgrade(),
|
||||
thread_store.clone(),
|
||||
None,
|
||||
Default::default(),
|
||||
"Test Agent".into(),
|
||||
"Test",
|
||||
|
|
@ -2576,7 +2572,6 @@ mod tests {
|
|||
workspace_handle.clone(),
|
||||
project.downgrade(),
|
||||
thread_store.clone(),
|
||||
None,
|
||||
session_capabilities.clone(),
|
||||
"Claude Agent".into(),
|
||||
"Test",
|
||||
|
|
@ -2742,7 +2737,6 @@ mod tests {
|
|||
workspace_handle,
|
||||
project.downgrade(),
|
||||
thread_store.clone(),
|
||||
None,
|
||||
session_capabilities.clone(),
|
||||
"Test Agent".into(),
|
||||
"Test",
|
||||
|
|
@ -2915,7 +2909,6 @@ mod tests {
|
|||
workspace_handle,
|
||||
project.downgrade(),
|
||||
None,
|
||||
None,
|
||||
session_capabilities.clone(),
|
||||
"Test Agent".into(),
|
||||
"Test",
|
||||
|
|
@ -3064,7 +3057,6 @@ mod tests {
|
|||
workspace_handle,
|
||||
project.downgrade(),
|
||||
Some(thread_store),
|
||||
None,
|
||||
session_capabilities.clone(),
|
||||
"Test Agent".into(),
|
||||
"Test",
|
||||
|
|
@ -3556,7 +3548,6 @@ mod tests {
|
|||
workspace.downgrade(),
|
||||
project.downgrade(),
|
||||
thread_store.clone(),
|
||||
None,
|
||||
Default::default(),
|
||||
"Test Agent".into(),
|
||||
"Test",
|
||||
|
|
@ -3657,7 +3648,6 @@ mod tests {
|
|||
workspace.downgrade(),
|
||||
project.downgrade(),
|
||||
thread_store.clone(),
|
||||
None,
|
||||
Default::default(),
|
||||
"Test Agent".into(),
|
||||
"Test",
|
||||
|
|
@ -3726,7 +3716,6 @@ mod tests {
|
|||
workspace.downgrade(),
|
||||
project.downgrade(),
|
||||
thread_store.clone(),
|
||||
None,
|
||||
Default::default(),
|
||||
"Test Agent".into(),
|
||||
"Test",
|
||||
|
|
@ -3779,7 +3768,6 @@ mod tests {
|
|||
workspace.downgrade(),
|
||||
project.downgrade(),
|
||||
thread_store.clone(),
|
||||
None,
|
||||
Default::default(),
|
||||
"Test Agent".into(),
|
||||
"Test",
|
||||
|
|
@ -3836,7 +3824,6 @@ mod tests {
|
|||
workspace.downgrade(),
|
||||
project.downgrade(),
|
||||
thread_store.clone(),
|
||||
None,
|
||||
Default::default(),
|
||||
"Test Agent".into(),
|
||||
"Test",
|
||||
|
|
@ -3894,7 +3881,6 @@ mod tests {
|
|||
workspace.downgrade(),
|
||||
project.downgrade(),
|
||||
thread_store.clone(),
|
||||
None,
|
||||
Default::default(),
|
||||
"Test Agent".into(),
|
||||
"Test",
|
||||
|
|
@ -3956,7 +3942,6 @@ mod tests {
|
|||
workspace_handle,
|
||||
project.downgrade(),
|
||||
thread_store.clone(),
|
||||
None,
|
||||
Default::default(),
|
||||
"Test Agent".into(),
|
||||
"Test",
|
||||
|
|
@ -4116,7 +4101,6 @@ mod tests {
|
|||
workspace_handle,
|
||||
project.downgrade(),
|
||||
thread_store.clone(),
|
||||
None,
|
||||
Default::default(),
|
||||
"Test Agent".into(),
|
||||
"Test",
|
||||
|
|
@ -4236,7 +4220,6 @@ mod tests {
|
|||
workspace_handle,
|
||||
project.downgrade(),
|
||||
Some(thread_store.clone()),
|
||||
None,
|
||||
Default::default(),
|
||||
"Test Agent".into(),
|
||||
"Test",
|
||||
|
|
@ -4315,7 +4298,6 @@ mod tests {
|
|||
workspace_handle,
|
||||
project.downgrade(),
|
||||
Some(thread_store),
|
||||
None,
|
||||
Default::default(),
|
||||
"Test Agent".into(),
|
||||
"Test",
|
||||
|
|
@ -4493,7 +4475,6 @@ mod tests {
|
|||
workspace_handle,
|
||||
project.downgrade(),
|
||||
Some(thread_store),
|
||||
None,
|
||||
Default::default(),
|
||||
"Test Agent".into(),
|
||||
"Test",
|
||||
|
|
@ -4905,7 +4886,6 @@ mod tests {
|
|||
workspace_handle,
|
||||
project.downgrade(),
|
||||
Some(thread_store),
|
||||
None,
|
||||
Default::default(),
|
||||
"Test Agent".into(),
|
||||
"Test",
|
||||
|
|
@ -5160,7 +5140,6 @@ mod tests {
|
|||
workspace_handle,
|
||||
project.downgrade(),
|
||||
Some(thread_store),
|
||||
None,
|
||||
Default::default(),
|
||||
"Test Agent".into(),
|
||||
"Test",
|
||||
|
|
@ -5253,7 +5232,6 @@ mod tests {
|
|||
workspace.downgrade(),
|
||||
project.downgrade(),
|
||||
None,
|
||||
None,
|
||||
Default::default(),
|
||||
"Test Agent".into(),
|
||||
"Test",
|
||||
|
|
@ -5402,7 +5380,6 @@ mod tests {
|
|||
workspace.downgrade(),
|
||||
project.downgrade(),
|
||||
None,
|
||||
None,
|
||||
Default::default(),
|
||||
"Test Agent".into(),
|
||||
"Test",
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ use language_models::provider::anthropic::telemetry::{
|
|||
AnthropicCompletionType, AnthropicEventData, AnthropicEventType, report_anthropic_event,
|
||||
};
|
||||
use project::Project;
|
||||
use prompt_store::{PromptBuilder, PromptStore};
|
||||
use prompt_store::PromptBuilder;
|
||||
use std::sync::Arc;
|
||||
use terminal_view::TerminalView;
|
||||
use ui::prelude::*;
|
||||
|
|
@ -64,7 +64,6 @@ impl TerminalInlineAssistant {
|
|||
workspace: WeakEntity<Workspace>,
|
||||
project: WeakEntity<Project>,
|
||||
thread_store: Entity<ThreadStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
initial_prompt: Option<String>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
|
|
@ -89,7 +88,6 @@ impl TerminalInlineAssistant {
|
|||
session_id,
|
||||
self.fs.clone(),
|
||||
thread_store.clone(),
|
||||
prompt_store.clone(),
|
||||
project.clone(),
|
||||
workspace.clone(),
|
||||
window,
|
||||
|
|
|
|||
|
|
@ -63,6 +63,76 @@ impl TerminalThreadMetadata {
|
|||
pub fn main_worktree_paths(&self) -> &PathList {
|
||||
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 {
|
||||
|
|
@ -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]
|
||||
async fn test_change_worktree_paths_reindexes_terminal_metadata(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
|
|
|||
|
|
@ -1888,7 +1888,7 @@ mod tests {
|
|||
.unwrap();
|
||||
let mut vcx = VisualTestContext::from_window(multi_workspace.into(), 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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,13 +103,16 @@ impl Component for EndTrialUpsell {
|
|||
"End of Trial Upsell Banner"
|
||||
}
|
||||
|
||||
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
|
||||
Some(
|
||||
v_flex()
|
||||
.child(EndTrialUpsell {
|
||||
dismiss_upsell: Arc::new(|_, _| {}),
|
||||
})
|
||||
.into_any_element(),
|
||||
)
|
||||
fn description() -> &'static str {
|
||||
"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()
|
||||
.child(EndTrialUpsell {
|
||||
dismiss_upsell: Arc::new(|_, _| {}),
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ use gpui::{
|
|||
pulsating_between,
|
||||
};
|
||||
use language::Buffer;
|
||||
use prompt_store::PromptId;
|
||||
use rope::Point;
|
||||
use settings::Settings;
|
||||
use theme_settings::ThemeSettings;
|
||||
|
|
@ -195,9 +194,6 @@ fn open_mention_uri(
|
|||
MentionUri::Thread { id, name } => {
|
||||
open_thread(workspace, id, name, window, cx);
|
||||
}
|
||||
MentionUri::Rule { id, .. } => {
|
||||
open_rule(workspace, id, window, cx);
|
||||
}
|
||||
MentionUri::Skill {
|
||||
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"
|
||||
}
|
||||
|
||||
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(
|
||||
sign_in_status: SignInStatus,
|
||||
plan: Option<Plan>,
|
||||
|
|
@ -402,41 +408,39 @@ impl Component for ZedAiOnboarding {
|
|||
.into_any_element()
|
||||
}
|
||||
|
||||
Some(
|
||||
v_flex()
|
||||
.min_w_0()
|
||||
.gap_4()
|
||||
.children(vec![
|
||||
single_example(
|
||||
"Not Signed-in",
|
||||
onboarding(SignInStatus::SignedOut, None, false),
|
||||
),
|
||||
single_example(
|
||||
"Young Account",
|
||||
onboarding(SignInStatus::SignedIn, None, true),
|
||||
),
|
||||
single_example(
|
||||
"Free Plan",
|
||||
onboarding(SignInStatus::SignedIn, Some(Plan::ZedFree), false),
|
||||
),
|
||||
single_example(
|
||||
"Pro Trial",
|
||||
onboarding(SignInStatus::SignedIn, Some(Plan::ZedProTrial), false),
|
||||
),
|
||||
single_example(
|
||||
"Pro Plan",
|
||||
onboarding(SignInStatus::SignedIn, Some(Plan::ZedPro), false),
|
||||
),
|
||||
single_example(
|
||||
"Business Plan",
|
||||
onboarding(SignInStatus::SignedIn, Some(Plan::ZedBusiness), false),
|
||||
),
|
||||
single_example(
|
||||
"Student Plan",
|
||||
onboarding(SignInStatus::SignedIn, Some(Plan::ZedStudent), false),
|
||||
),
|
||||
])
|
||||
.into_any_element(),
|
||||
)
|
||||
v_flex()
|
||||
.min_w_0()
|
||||
.gap_4()
|
||||
.children(vec![
|
||||
single_example(
|
||||
"Not Signed-in",
|
||||
onboarding(SignInStatus::SignedOut, None, false),
|
||||
),
|
||||
single_example(
|
||||
"Young Account",
|
||||
onboarding(SignInStatus::SignedIn, None, true),
|
||||
),
|
||||
single_example(
|
||||
"Free Plan",
|
||||
onboarding(SignInStatus::SignedIn, Some(Plan::ZedFree), false),
|
||||
),
|
||||
single_example(
|
||||
"Pro Trial",
|
||||
onboarding(SignInStatus::SignedIn, Some(Plan::ZedProTrial), false),
|
||||
),
|
||||
single_example(
|
||||
"Pro Plan",
|
||||
onboarding(SignInStatus::SignedIn, Some(Plan::ZedPro), false),
|
||||
),
|
||||
single_example(
|
||||
"Business Plan",
|
||||
onboarding(SignInStatus::SignedIn, Some(Plan::ZedBusiness), false),
|
||||
),
|
||||
single_example(
|
||||
"Student Plan",
|
||||
onboarding(SignInStatus::SignedIn, Some(Plan::ZedStudent), false),
|
||||
),
|
||||
])
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,9 +122,6 @@ impl Model {
|
|||
|
||||
let mut supported_effort_levels = Vec::new();
|
||||
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 [
|
||||
(Effort::Low, effort.low.as_ref()),
|
||||
(Effort::Medium, effort.medium.as_ref()),
|
||||
|
|
@ -148,7 +145,10 @@ impl Model {
|
|||
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();
|
||||
if supports_speed {
|
||||
|
|
@ -676,6 +676,8 @@ pub enum Effort {
|
|||
Low,
|
||||
Medium,
|
||||
High,
|
||||
#[serde(rename = "xhigh")]
|
||||
#[strum(serialize = "xhigh")]
|
||||
XHigh,
|
||||
Max,
|
||||
}
|
||||
|
|
@ -1056,6 +1058,17 @@ mod tests {
|
|||
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]
|
||||
fn from_listed_collects_supported_effort_levels() {
|
||||
let entry = listed_entry(
|
||||
|
|
|
|||
|
|
@ -300,6 +300,7 @@ pub fn into_anthropic(
|
|||
"low" => Some(crate::Effort::Low),
|
||||
"medium" => Some(crate::Effort::Medium),
|
||||
"high" => Some(crate::Effort::High),
|
||||
"xhigh" => Some(crate::Effort::XHigh),
|
||||
"max" => Some(crate::Effort::Max),
|
||||
_ => 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]
|
||||
fn test_no_cache_control_when_caching_disabled() {
|
||||
let request = LanguageModelRequest {
|
||||
|
|
|
|||
|
|
@ -12,9 +12,12 @@ mod real_implementation {
|
|||
|
||||
impl Default for EchoCanceller {
|
||||
fn default() -> Self {
|
||||
Self(Arc::new(Mutex::new(apm::AudioProcessingModule::new(
|
||||
true, false, false, false,
|
||||
))))
|
||||
// Sound-effect playback only feeds this APM through `process_reverse_stream`
|
||||
// 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,
|
||||
#[default]
|
||||
High,
|
||||
XHigh,
|
||||
Max,
|
||||
}
|
||||
|
||||
|
|
@ -17,6 +18,7 @@ impl BedrockAdaptiveThinkingEffort {
|
|||
Self::Low => "low",
|
||||
Self::Medium => "medium",
|
||||
Self::High => "high",
|
||||
Self::XHigh => "xhigh",
|
||||
Self::Max => "max",
|
||||
}
|
||||
}
|
||||
|
|
@ -91,6 +93,13 @@ pub enum Model {
|
|||
alias = "claude-opus-4-7-thinking-latest"
|
||||
)]
|
||||
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(
|
||||
rename = "claude-sonnet-4-6",
|
||||
alias = "claude-sonnet-4-6-latest",
|
||||
|
|
@ -210,7 +219,9 @@ impl Model {
|
|||
}
|
||||
|
||||
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)
|
||||
} else if id.starts_with("claude-opus-4-6") {
|
||||
Ok(Self::ClaudeOpus4_6)
|
||||
|
|
@ -240,6 +251,7 @@ impl Model {
|
|||
Self::ClaudeOpus4_5 => "claude-opus-4-5",
|
||||
Self::ClaudeOpus4_6 => "claude-opus-4-6",
|
||||
Self::ClaudeOpus4_7 => "claude-opus-4-7",
|
||||
Self::ClaudeOpus4_8 => "claude-opus-4-8",
|
||||
Self::ClaudeSonnet4_6 => "claude-sonnet-4-6",
|
||||
Self::Llama4Scout17B => "llama-4-scout-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_6 => "anthropic.claude-opus-4-6-v1",
|
||||
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::Llama4Scout17B => "meta.llama4-scout-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_6 => "Claude Opus 4.6",
|
||||
Self::ClaudeOpus4_7 => "Claude Opus 4.7",
|
||||
Self::ClaudeOpus4_8 => "Claude Opus 4.8",
|
||||
Self::ClaudeSonnet4_6 => "Claude Sonnet 4.6",
|
||||
Self::Llama4Scout17B => "Llama 4 Scout 17B",
|
||||
Self::Llama4Maverick17B => "Llama 4 Maverick 17B",
|
||||
|
|
@ -391,6 +405,7 @@ impl Model {
|
|||
| Self::ClaudeOpus4_5
|
||||
| Self::ClaudeOpus4_6
|
||||
| Self::ClaudeOpus4_7
|
||||
| Self::ClaudeOpus4_8
|
||||
| Self::ClaudeSonnet4_6 => 1_000_000,
|
||||
Self::ClaudeOpus4_1 => 200_000,
|
||||
Self::Llama4Scout17B | Self::Llama4Maverick17B => 128_000,
|
||||
|
|
@ -425,7 +440,7 @@ impl Model {
|
|||
| Self::ClaudeOpus4_5
|
||||
| Self::ClaudeSonnet4_6 => 64_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::Llama4Maverick17B
|
||||
| Self::Gemma3_4B
|
||||
|
|
@ -464,6 +479,7 @@ impl Model {
|
|||
| Self::ClaudeOpus4_5
|
||||
| Self::ClaudeOpus4_6
|
||||
| Self::ClaudeOpus4_7
|
||||
| Self::ClaudeOpus4_8
|
||||
| Self::ClaudeSonnet4_6 => 1.0,
|
||||
Self::Custom {
|
||||
default_temperature,
|
||||
|
|
@ -482,6 +498,7 @@ impl Model {
|
|||
| Self::ClaudeOpus4_5
|
||||
| Self::ClaudeOpus4_6
|
||||
| Self::ClaudeOpus4_7
|
||||
| Self::ClaudeOpus4_8
|
||||
| Self::ClaudeSonnet4_6 => true,
|
||||
Self::NovaLite | Self::NovaPro | Self::NovaPremier | Self::Nova2Lite => true,
|
||||
Self::MistralLarge3 | Self::PixtralLarge | Self::MagistralSmall => true,
|
||||
|
|
@ -513,6 +530,7 @@ impl Model {
|
|||
| Self::ClaudeOpus4_5
|
||||
| Self::ClaudeOpus4_6
|
||||
| Self::ClaudeOpus4_7
|
||||
| Self::ClaudeOpus4_8
|
||||
| Self::ClaudeSonnet4_6 => true,
|
||||
Self::NovaLite | Self::NovaPro => true,
|
||||
Self::PixtralLarge => true,
|
||||
|
|
@ -531,6 +549,7 @@ impl Model {
|
|||
| Self::ClaudeOpus4_5
|
||||
| Self::ClaudeOpus4_6
|
||||
| Self::ClaudeOpus4_7
|
||||
| Self::ClaudeOpus4_8
|
||||
| Self::ClaudeSonnet4_6 => true,
|
||||
Self::Custom {
|
||||
cache_configuration,
|
||||
|
|
@ -550,6 +569,7 @@ impl Model {
|
|||
| Self::ClaudeOpus4_5
|
||||
| Self::ClaudeOpus4_6
|
||||
| Self::ClaudeOpus4_7
|
||||
| Self::ClaudeOpus4_8
|
||||
| Self::ClaudeSonnet4_6
|
||||
)
|
||||
}
|
||||
|
|
@ -557,10 +577,14 @@ impl Model {
|
|||
pub fn supports_adaptive_thinking(&self) -> bool {
|
||||
matches!(
|
||||
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 {
|
||||
if self.supports_adaptive_thinking() {
|
||||
BedrockModelMode::AdaptiveThinking {
|
||||
|
|
@ -590,6 +614,7 @@ impl Model {
|
|||
| Self::ClaudeOpus4_5
|
||||
| Self::ClaudeOpus4_6
|
||||
| Self::ClaudeOpus4_7
|
||||
| Self::ClaudeOpus4_8
|
||||
| Self::ClaudeSonnet4_6
|
||||
| Self::Nova2Lite
|
||||
);
|
||||
|
|
@ -650,6 +675,7 @@ impl Model {
|
|||
| Self::ClaudeOpus4_5
|
||||
| Self::ClaudeOpus4_6
|
||||
| Self::ClaudeOpus4_7
|
||||
| Self::ClaudeOpus4_8
|
||||
| Self::ClaudeSonnet4_6
|
||||
| Self::Nova2Lite,
|
||||
"global",
|
||||
|
|
@ -667,6 +693,7 @@ impl Model {
|
|||
| Self::ClaudeOpus4_5
|
||||
| Self::ClaudeOpus4_6
|
||||
| Self::ClaudeOpus4_7
|
||||
| Self::ClaudeOpus4_8
|
||||
| Self::ClaudeSonnet4_6
|
||||
| Self::Llama4Scout17B
|
||||
| Self::Llama4Maverick17B
|
||||
|
|
@ -689,6 +716,7 @@ impl Model {
|
|||
| Self::ClaudeSonnet4_5
|
||||
| Self::ClaudeOpus4_6
|
||||
| Self::ClaudeOpus4_7
|
||||
| Self::ClaudeOpus4_8
|
||||
| Self::ClaudeSonnet4_6
|
||||
| Self::NovaLite
|
||||
| Self::NovaPro
|
||||
|
|
@ -702,6 +730,7 @@ impl Model {
|
|||
| Self::ClaudeSonnet4_5
|
||||
| Self::ClaudeOpus4_6
|
||||
| Self::ClaudeOpus4_7
|
||||
| Self::ClaudeOpus4_8
|
||||
| Self::ClaudeSonnet4_6,
|
||||
"au",
|
||||
) => Ok(format!("{}.{}", region_group, model_id)),
|
||||
|
|
@ -779,6 +808,10 @@ mod tests {
|
|||
Model::ClaudeOpus4_7.cross_region_inference_id("eu-west-1", false)?,
|
||||
"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(())
|
||||
}
|
||||
|
||||
|
|
@ -813,6 +846,10 @@ mod tests {
|
|||
Model::ClaudeOpus4_7.cross_region_inference_id("ap-southeast-2", false)?,
|
||||
"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(())
|
||||
}
|
||||
|
||||
|
|
@ -877,6 +914,10 @@ mod tests {
|
|||
Model::ClaudeOpus4_7.cross_region_inference_id("us-east-1", true)?,
|
||||
"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!(
|
||||
Model::Nova2Lite.cross_region_inference_id("us-east-1", true)?,
|
||||
"global.amazon.nova-2-lite-v1:0"
|
||||
|
|
@ -978,6 +1019,9 @@ mod tests {
|
|||
assert!(!Model::ClaudeSonnet4.supports_adaptive_thinking());
|
||||
assert!(Model::ClaudeOpus4_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!(
|
||||
Model::ClaudeSonnet4.thinking_mode(),
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ pub enum ChannelEvent {
|
|||
|
||||
impl EventEmitter<ChannelEvent> for ChannelStore {}
|
||||
|
||||
enum OpenEntityHandle<E> {
|
||||
enum OpenEntityHandle<E: 'static> {
|
||||
Open(WeakEntity<E>),
|
||||
Loading(Shared<Task<Result<Entity<E>, Arc<anyhow::Error>>>>),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ edition.workspace = true
|
|||
name = "collab"
|
||||
version = "0.44.0"
|
||||
publish.workspace = true
|
||||
license = "AGPL-3.0-or-later"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
../../LICENSE-AGPL
|
||||
|
|
@ -48,9 +48,9 @@ pub fn register_component<T: Component>() {
|
|||
let id = T::id();
|
||||
let metadata = ComponentMetadata {
|
||||
id: id.clone(),
|
||||
description: T::description().map(Into::into),
|
||||
description: SharedString::new_static(T::description()),
|
||||
name: SharedString::new_static(T::name()),
|
||||
preview: Some(T::preview),
|
||||
preview: T::preview,
|
||||
scope: T::scope(),
|
||||
sort_name: SharedString::new_static(T::sort_name()),
|
||||
status: T::status(),
|
||||
|
|
@ -69,15 +69,12 @@ pub struct ComponentRegistry {
|
|||
}
|
||||
|
||||
impl ComponentRegistry {
|
||||
pub fn previews(&self) -> Vec<&ComponentMetadata> {
|
||||
self.components
|
||||
.values()
|
||||
.filter(|c| c.preview.is_some())
|
||||
.collect()
|
||||
pub fn previews(&self) -> impl Iterator<Item = &ComponentMetadata> {
|
||||
self.components.values()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
@ -112,9 +109,9 @@ pub struct ComponentId(pub &'static str);
|
|||
#[derive(Clone)]
|
||||
pub struct ComponentMetadata {
|
||||
id: ComponentId,
|
||||
description: Option<SharedString>,
|
||||
description: SharedString,
|
||||
name: SharedString,
|
||||
preview: Option<fn(&mut Window, &mut App) -> Option<AnyElement>>,
|
||||
preview: fn(&mut Window, &mut App) -> AnyElement,
|
||||
scope: ComponentScope,
|
||||
sort_name: SharedString,
|
||||
status: ComponentStatus,
|
||||
|
|
@ -125,7 +122,7 @@ impl ComponentMetadata {
|
|||
self.id.clone()
|
||||
}
|
||||
|
||||
pub fn description(&self) -> Option<SharedString> {
|
||||
pub fn description(&self) -> SharedString {
|
||||
self.description.clone()
|
||||
}
|
||||
|
||||
|
|
@ -133,7 +130,7 @@ impl ComponentMetadata {
|
|||
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
|
||||
}
|
||||
|
||||
|
|
@ -234,17 +231,15 @@ pub trait Component {
|
|||
/// struct MyComponent;
|
||||
///
|
||||
/// impl MyComponent {
|
||||
/// fn description() -> Option<&'static str> {
|
||||
/// Some(Self::DOCS)
|
||||
/// fn description() -> &'static str {
|
||||
/// Self::DOCS
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// This will result in "This is a doc comment." being passed
|
||||
/// to the component's description.
|
||||
fn description() -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
fn description() -> &'static str;
|
||||
/// 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
|
||||
/// 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.
|
||||
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
|
||||
None
|
||||
}
|
||||
fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement;
|
||||
}
|
||||
|
||||
/// The ready status of this component.
|
||||
|
|
@ -286,14 +279,17 @@ impl ComponentStatus {
|
|||
pub fn description(&self) -> &str {
|
||||
match self {
|
||||
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 => {
|
||||
"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::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 project::Project;
|
||||
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 workspace::AppState;
|
||||
use workspace::{
|
||||
|
|
@ -197,10 +200,7 @@ impl ComponentPreview {
|
|||
.filter(|component| {
|
||||
let component_name = component.name().to_lowercase();
|
||||
let scope_name = component.scope().to_string().to_lowercase();
|
||||
let description = component
|
||||
.description()
|
||||
.map(|d| d.to_lowercase())
|
||||
.unwrap_or_default();
|
||||
let description = component.description().to_lowercase();
|
||||
|
||||
component_name.contains(&filter)
|
||||
|| scope_name.contains(&filter)
|
||||
|
|
@ -231,7 +231,7 @@ impl ComponentPreview {
|
|||
// let full_component_name = component.name();
|
||||
let scopeless_name = component.scopeless_name();
|
||||
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_scope = scope_name.to_lowercase();
|
||||
|
|
@ -445,45 +445,40 @@ impl ComponentPreview {
|
|||
let description = component.description();
|
||||
|
||||
// Build the content container
|
||||
let mut preview_container = v_flex().py_2().child(
|
||||
v_flex()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_sm()
|
||||
.w_full()
|
||||
.gap_4()
|
||||
.py_4()
|
||||
.px_6()
|
||||
.flex_none()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.text_xl()
|
||||
.child(div().child(name))
|
||||
.when(!matches!(scope, ComponentScope::None), |this| {
|
||||
this.child(div().opacity(0.5).child(format!("({})", scope)))
|
||||
}),
|
||||
)
|
||||
.when_some(description, |this, description| {
|
||||
this.child(
|
||||
v_flex()
|
||||
.py_2()
|
||||
.child(
|
||||
v_flex()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_sm()
|
||||
.w_full()
|
||||
.gap_4()
|
||||
.py_4()
|
||||
.px_6()
|
||||
.flex_none()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
h_flex().gap_1().text_xl().child(div().child(name)).when(
|
||||
scope != ComponentScope::None,
|
||||
|this| {
|
||||
this.child(div().opacity(0.5).child(format!("({})", scope)))
|
||||
},
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_ui_sm(cx)
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.max_w(px(600.0))
|
||||
.child(description),
|
||||
)
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
if let Some(preview) = component.preview() {
|
||||
preview_container = preview_container.children(preview(window, cx));
|
||||
}
|
||||
|
||||
preview_container.into_any_element()
|
||||
),
|
||||
),
|
||||
)
|
||||
.child((component.preview())(window, cx))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
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 active_page = self.active_page.clone();
|
||||
let background_color = cx.theme().colors().editor_background;
|
||||
|
||||
h_flex()
|
||||
.id("component-preview")
|
||||
|
|
@ -601,37 +597,45 @@ impl Render for ComponentPreview {
|
|||
.overflow_hidden()
|
||||
.size_full()
|
||||
.track_focus(&self.focus_handle)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.bg(background_color)
|
||||
.child(
|
||||
v_flex()
|
||||
.h_full()
|
||||
.border_r_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
gpui::uniform_list(
|
||||
"component-nav",
|
||||
sidebar_entries.len(),
|
||||
cx.processor(move |this, range: Range<usize>, _window, cx| {
|
||||
range
|
||||
.filter_map(|ix| {
|
||||
if ix < sidebar_entries.len() {
|
||||
Some(this.render_sidebar_entry(
|
||||
ix,
|
||||
&sidebar_entries[ix],
|
||||
cx,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}),
|
||||
)
|
||||
.track_scroll(&self.nav_scroll_handle)
|
||||
.p_2p5()
|
||||
.w(px(231.)) // Matches perfectly with the size of the "Component Preview" tab, if that's the first one in the pane
|
||||
.h_full()
|
||||
.flex_1(),
|
||||
div()
|
||||
.size_full()
|
||||
.child(
|
||||
gpui::uniform_list(
|
||||
"component-nav",
|
||||
sidebar_entries.len(),
|
||||
cx.processor(move |this, range: Range<usize>, _window, cx| {
|
||||
range
|
||||
.filter(|ix| ix < &sidebar_entries.len())
|
||||
.map(|ix| {
|
||||
this.render_sidebar_entry(
|
||||
ix,
|
||||
&sidebar_entries[ix],
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}),
|
||||
)
|
||||
.track_scroll(&self.nav_scroll_handle)
|
||||
.p_2p5()
|
||||
.w(px(231.)) // Matches perfectly with the size of the "Component Preview" tab, if that's the first one in the pane
|
||||
.h_full()
|
||||
.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(
|
||||
div()
|
||||
|
|
@ -961,23 +965,10 @@ impl ComponentPreviewPage {
|
|||
.children(self.render_component_status(cx)),
|
||||
),
|
||||
)
|
||||
.when_some(self.component.description(), |this, description| {
|
||||
this.child(Label::new(description).size(LabelSize::Small))
|
||||
})
|
||||
.child(Label::new(self.component.description()).size(LabelSize::Small))
|
||||
}
|
||||
|
||||
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()
|
||||
.id(("component-preview", self.reset_key))
|
||||
.size_full()
|
||||
|
|
@ -985,7 +976,7 @@ impl ComponentPreviewPage {
|
|||
.px_12()
|
||||
.py_6()
|
||||
.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) {
|
||||
env.insert(copilot_chat::COPILOT_OAUTH_ENV_VAR.to_string(), oauth_token);
|
||||
for env_var in [
|
||||
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) }
|
||||
|
|
@ -1259,6 +1265,7 @@ impl Copilot {
|
|||
| request::SignInStatus::AlreadySignedIn { .. } => {
|
||||
server.sign_in_status = SignInStatus::Authorized;
|
||||
cx.emit(Event::CopilotAuthSignedIn);
|
||||
notify_copilot_chat_auth_changed(cx);
|
||||
for buffer in self.buffers.iter().cloned().collect::<Vec<_>>() {
|
||||
if let Some(buffer) = buffer.upgrade() {
|
||||
self.register_buffer(&buffer, cx);
|
||||
|
|
@ -1278,6 +1285,7 @@ impl Copilot {
|
|||
};
|
||||
}
|
||||
cx.emit(Event::CopilotAuthSignedOut);
|
||||
notify_copilot_chat_auth_changed(cx);
|
||||
for buffer in self.buffers.iter().cloned().collect::<Vec<_>>() {
|
||||
self.unregister_buffer(&buffer);
|
||||
}
|
||||
|
|
@ -1381,6 +1389,15 @@ fn notify_did_change_config_to_server(
|
|||
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() {
|
||||
remove_matching(paths::copilot_dir(), |_| true).await
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,9 @@ paths.workspace = true
|
|||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
sqlez.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
serde_json.workspace = true
|
||||
tempfile.workspace = true
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
pub mod responses;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
|
|
@ -17,9 +17,10 @@ use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
|
|||
use paths::home_dir;
|
||||
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 GITHUB_COPILOT_OAUTH_ENV_VAR: &str = "GITHUB_COPILOT_TOKEN";
|
||||
const DEFAULT_COPILOT_API_ENDPOINT: &str = "https://api.githubcopilot.com";
|
||||
|
||||
#[derive(Default, Clone, Debug, PartialEq)]
|
||||
|
|
@ -501,6 +502,7 @@ pub struct CopilotChat {
|
|||
configuration: CopilotChatConfiguration,
|
||||
models: Option<Vec<Model>>,
|
||||
client: Arc<dyn HttpClient>,
|
||||
fs: Arc<dyn Fs>,
|
||||
}
|
||||
|
||||
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] {
|
||||
let base_dir = copilot_chat_config_dir();
|
||||
[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 {
|
||||
pub fn global(cx: &App) -> Option<gpui::Entity<Self>> {
|
||||
cx.try_global::<GlobalCopilotChat>()
|
||||
|
|
@ -546,40 +556,42 @@ impl CopilotChat {
|
|||
configuration: CopilotChatConfiguration,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let config_paths: HashSet<PathBuf> = copilot_chat_config_paths().into_iter().collect();
|
||||
let dir_path = copilot_chat_config_dir();
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let mut parent_watch_rx = watch_config_dir(
|
||||
cx.background_executor(),
|
||||
fs.clone(),
|
||||
dir_path.clone(),
|
||||
config_paths,
|
||||
);
|
||||
while let Some(contents) = parent_watch_rx.next().await {
|
||||
// Initial async scan of token sources. Live reload is driven by the
|
||||
// Copilot LSP's auth status notifications instead of watching files,
|
||||
// because SQLite WAL writes can make directory watchers racy.
|
||||
cx.spawn({
|
||||
let fs = fs.clone();
|
||||
async move |this, cx| {
|
||||
let 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| {
|
||||
this.oauth_token = oauth_token.clone();
|
||||
cx.notify();
|
||||
})?;
|
||||
let oauth_token =
|
||||
read_oauth_token(&fs, &config_paths, &oauth_domain, &auth_db_path, cx).await;
|
||||
|
||||
if oauth_token.is_some() {
|
||||
this.update(cx, |this, cx| {
|
||||
this.oauth_token = oauth_token;
|
||||
cx.notify();
|
||||
})?;
|
||||
Self::update_models(&this, cx).await?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.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 {
|
||||
oauth_token: std::env::var(COPILOT_OAUTH_ENV_VAR).ok(),
|
||||
oauth_token: oauth_token_from_env(),
|
||||
api_endpoint: None,
|
||||
models: None,
|
||||
configuration,
|
||||
client,
|
||||
fs,
|
||||
};
|
||||
|
||||
if this.oauth_token.is_some() {
|
||||
|
|
@ -764,6 +776,39 @@ impl CopilotChat {
|
|||
.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(
|
||||
|
|
@ -917,6 +962,40 @@ async fn request_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> {
|
||||
serde_json::from_str::<serde_json::Value>(&contents)
|
||||
.map(|v| {
|
||||
|
|
@ -934,6 +1013,36 @@ fn extract_oauth_token(contents: String, domain: &str) -> Option<String> {
|
|||
.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(
|
||||
client: Arc<dyn HttpClient>,
|
||||
oauth_token: String,
|
||||
|
|
@ -1751,4 +1860,61 @@ mod tests {
|
|||
"\"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")]
|
||||
pub(crate) update_remote_user_uid: Option<bool>,
|
||||
user_env_probe: Option<UserEnvProbe>,
|
||||
override_command: Option<bool>,
|
||||
pub(crate) override_command: Option<bool>,
|
||||
shutdown_action: Option<ShutdownAction>,
|
||||
init: Option<bool>,
|
||||
pub(crate) privileged: Option<bool>,
|
||||
|
|
@ -232,7 +232,7 @@ pub(crate) struct DevContainer {
|
|||
#[serde(default, deserialize_with = "deserialize_string_or_array")]
|
||||
pub(crate) docker_compose_file: Option<Vec<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) on_create_command: Option<LifecycleScript>,
|
||||
pub(crate) update_content_command: Option<LifecycleScript>,
|
||||
|
|
|
|||
|
|
@ -794,24 +794,30 @@ RUN sed -i -E 's/((^|\s)PATH=)([^\$]*)$/\1\${{PATH:-\3}}/g' /etc/profile || true
|
|||
let privileged = dev_container.privileged.unwrap_or(false)
|
||||
|| self.features.iter().any(|f| f.privileged());
|
||||
|
||||
let mut entrypoint_script_lines = vec![
|
||||
"echo Container started".to_string(),
|
||||
"trap \"exit 0\" 15".to_string(),
|
||||
];
|
||||
let entrypoint_script = if dev_container.override_command == Some(false) {
|
||||
None
|
||||
} else {
|
||||
let mut entrypoint_script_lines = vec![
|
||||
"echo Container started".to_string(),
|
||||
"trap \"exit 0\" 15".to_string(),
|
||||
];
|
||||
|
||||
for entrypoint in self.features.iter().filter_map(|f| f.entrypoint()) {
|
||||
entrypoint_script_lines.push(entrypoint.clone());
|
||||
}
|
||||
entrypoint_script_lines.append(&mut vec![
|
||||
"exec \"$@\"".to_string(),
|
||||
"while sleep 1 & wait $!; do :; done".to_string(),
|
||||
]);
|
||||
for entrypoint in self.features.iter().filter_map(|f| f.entrypoint()) {
|
||||
entrypoint_script_lines.push(entrypoint.clone());
|
||||
}
|
||||
entrypoint_script_lines.append(&mut vec![
|
||||
"exec \"$@\"".to_string(),
|
||||
"while sleep 1 & wait $!; do :; done".to_string(),
|
||||
]);
|
||||
|
||||
Some(entrypoint_script_lines.join("\n").trim().to_string())
|
||||
};
|
||||
|
||||
Ok(DockerBuildResources {
|
||||
image: base_image,
|
||||
additional_mounts: mounts,
|
||||
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?;
|
||||
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?;
|
||||
(
|
||||
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?;
|
||||
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?;
|
||||
|
||||
(
|
||||
|
|
@ -1255,13 +1269,17 @@ RUN sed -i -E 's/((^|\s)PATH=)([^\$]*)$/\1\${{PATH:-\3}}/g' /etc/profile || true
|
|||
})
|
||||
.collect();
|
||||
|
||||
let mut main_service = DockerComposeService {
|
||||
entrypoint: Some(vec![
|
||||
let entrypoint = resources.entrypoint_script.map(|script| {
|
||||
vec![
|
||||
"/bin/sh".to_string(),
|
||||
"-c".to_string(),
|
||||
resources.entrypoint_script,
|
||||
script,
|
||||
"-".to_string(),
|
||||
]),
|
||||
]
|
||||
});
|
||||
|
||||
let mut main_service = DockerComposeService {
|
||||
entrypoint,
|
||||
cap_add: Some(vec!["SYS_PTRACE".to_string()]),
|
||||
security_opt: Some(vec!["seccomp=unconfined".to_string()]),
|
||||
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(&["up", "-d"]);
|
||||
if let Some(run_services) = self.dev_container().run_services.as_ref() {
|
||||
command.args(run_services);
|
||||
}
|
||||
|
||||
let output = self
|
||||
.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("--entrypoint");
|
||||
command.arg("/bin/sh");
|
||||
command.arg(&build_resources.image.id);
|
||||
command.arg("-c");
|
||||
|
||||
command.arg(build_resources.entrypoint_script);
|
||||
command.arg("-");
|
||||
if let Some(entrypoint_script) = build_resources.entrypoint_script {
|
||||
command.arg("--entrypoint");
|
||||
command.arg("/bin/sh");
|
||||
command.arg(&build_resources.image.id);
|
||||
command.arg("-c");
|
||||
command.arg(entrypoint_script);
|
||||
command.arg("-");
|
||||
} else {
|
||||
command.arg(&build_resources.image.id);
|
||||
}
|
||||
|
||||
Ok(command)
|
||||
}
|
||||
|
|
@ -2409,7 +2433,7 @@ struct DockerBuildResources {
|
|||
image: DockerInspect,
|
||||
additional_mounts: Vec<MountDefinition>,
|
||||
privileged: bool,
|
||||
entrypoint_script: String,
|
||||
entrypoint_script: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
@ -3166,7 +3190,7 @@ mod test {
|
|||
},
|
||||
additional_mounts: vec![],
|
||||
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);
|
||||
|
||||
|
|
@ -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]
|
||||
async fn should_find_primary_service_in_docker_compose(cx: &mut TestAppContext) {
|
||||
// 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"))]
|
||||
#[gpui::test]
|
||||
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 {
|
||||
name: None,
|
||||
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(),
|
||||
DockerComposeService {
|
||||
|
|
@ -6130,6 +6322,7 @@ FROM docker.io/hexpm/elixir:1.21-erlang-28.4.1-debian-trixie-20260316-slim AS de
|
|||
&self,
|
||||
_config_files: &Vec<PathBuf>,
|
||||
_project_name: &str,
|
||||
_services: Option<&Vec<String>>,
|
||||
) -> Result<(), DevContainerError> {
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -291,6 +291,7 @@ impl DockerClient for Docker {
|
|||
&self,
|
||||
config_files: &Vec<PathBuf>,
|
||||
project_name: &str,
|
||||
services: Option<&Vec<String>>,
|
||||
) -> Result<(), DevContainerError> {
|
||||
let mut command = Command::new(&self.docker_cli);
|
||||
if !self.is_podman() {
|
||||
|
|
@ -301,6 +302,9 @@ impl DockerClient for Docker {
|
|||
command.args(&["-f", &docker_compose_file.display().to_string()]);
|
||||
}
|
||||
command.arg("build");
|
||||
if let Some(services) = services {
|
||||
command.args(services);
|
||||
}
|
||||
|
||||
let output = command.output().await.map_err(|e| {
|
||||
log::error!("Error running docker compose up: {e}");
|
||||
|
|
@ -457,6 +461,7 @@ pub(crate) trait DockerClient {
|
|||
&self,
|
||||
config_files: &Vec<PathBuf>,
|
||||
project_name: &str,
|
||||
services: Option<&Vec<String>>,
|
||||
) -> Result<(), DevContainerError>;
|
||||
async fn run_docker_exec(
|
||||
&self,
|
||||
|
|
|
|||
|
|
@ -1550,6 +1550,8 @@ async fn go_to_diagnostic_with_severity(cx: &mut TestAppContext) {
|
|||
|
||||
// Default, should cycle through all diagnostics
|
||||
go!(GoToDiagnosticSeverityFilter::default());
|
||||
cx.assert_editor_state(indoc! {"error warning info ˇhint"});
|
||||
go!(GoToDiagnosticSeverityFilter::default());
|
||||
cx.assert_editor_state(indoc! {"ˇerror warning info hint"});
|
||||
go!(GoToDiagnosticSeverityFilter::default());
|
||||
cx.assert_editor_state(indoc! {"error ˇwarning info hint"});
|
||||
|
|
|
|||
|
|
@ -64,13 +64,19 @@ impl Render for DiagnosticIndicator {
|
|||
.message
|
||||
.split_once('\n')
|
||||
.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(
|
||||
Button::new("diagnostic_message", SharedString::new(message))
|
||||
.label_size(LabelSize::Small)
|
||||
.truncate(true)
|
||||
.tooltip(|_window, cx| {
|
||||
.tooltip(move |_window, cx| {
|
||||
Tooltip::for_action(
|
||||
"Next Diagnostic",
|
||||
tooltip,
|
||||
&editor::actions::GoToDiagnostic::default(),
|
||||
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>) {
|
||||
if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade()) {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.go_to_diagnostic_impl(
|
||||
editor.go_to_diagnostic_at_cursor(
|
||||
editor::Direction::Next,
|
||||
GoToDiagnosticSeverityFilter::default(),
|
||||
window,
|
||||
|
|
|
|||
|
|
@ -38,9 +38,9 @@ use gpui::{
|
|||
};
|
||||
use heapless::Vec as ArrayVec;
|
||||
use language::{
|
||||
Anchor, Buffer, BufferSnapshot, EditPredictionPromptFormat, EditPredictionsMode, EditPreview,
|
||||
File, OffsetRangeExt, Point, TextBufferSnapshot, ToOffset, ToPoint,
|
||||
language_settings::all_language_settings,
|
||||
Anchor, Buffer, BufferEditSource, BufferSnapshot, EditPredictionPromptFormat,
|
||||
EditPredictionsMode, EditPreview, File, OffsetRangeExt, Point, TextBufferSnapshot, ToOffset,
|
||||
ToPoint, language_settings::all_language_settings,
|
||||
};
|
||||
use project::{DisableAiSettings, Project, ProjectPath, WorktreeId};
|
||||
use release_channel::AppVersion;
|
||||
|
|
@ -324,6 +324,7 @@ struct ProjectState {
|
|||
recent_paths: VecDeque<ProjectPath>,
|
||||
registered_buffers: HashMap<gpui::EntityId, RegisteredBuffer>,
|
||||
current_prediction: Option<CurrentEditPrediction>,
|
||||
last_edit_source: Option<BufferEditSource>,
|
||||
next_pending_prediction_id: usize,
|
||||
pending_predictions: ArrayVec<PendingPrediction, 2, u8>,
|
||||
debug_tx: Option<mpsc::UnboundedSender<DebugEvent>>,
|
||||
|
|
@ -1212,6 +1213,7 @@ impl EditPredictionStore {
|
|||
debug_tx: None,
|
||||
registered_buffers: HashMap::default(),
|
||||
current_prediction: None,
|
||||
last_edit_source: None,
|
||||
cancelled_predictions: HashSet::default(),
|
||||
pending_predictions: ArrayVec::new(),
|
||||
next_pending_prediction_id: 0,
|
||||
|
|
@ -1315,6 +1317,9 @@ impl EditPredictionStore {
|
|||
}
|
||||
// TODO [zeta2] init with recent paths
|
||||
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)) => {
|
||||
let Some(project_state) = self.projects.get_mut(&project.entity_id()) else {
|
||||
return;
|
||||
|
|
@ -1332,6 +1337,15 @@ impl EditPredictionStore {
|
|||
}
|
||||
}
|
||||
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>() {
|
||||
self.refresh_prediction_from_diagnostics(
|
||||
project,
|
||||
|
|
@ -1391,11 +1405,17 @@ impl EditPredictionStore {
|
|||
cx.subscribe(buffer, {
|
||||
let project = project.downgrade();
|
||||
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 project_state = this.get_or_init_project(&project, cx);
|
||||
project_state.last_edit_source = Some(*source);
|
||||
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,
|
||||
cx: &mut Context<Self>,
|
||||
) -> 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);
|
||||
let project_state = self.projects.get(&project.entity_id()).unwrap();
|
||||
let stored_events = project_state.events(cx);
|
||||
|
|
@ -2551,11 +2580,24 @@ impl EditPredictionStore {
|
|||
EditPredictionsMode::Subtle => PredictEditsMode::Subtle,
|
||||
};
|
||||
|
||||
let is_open_source = snapshot
|
||||
.file()
|
||||
.map_or(false, |file| self.is_file_open_source(&project, file, cx))
|
||||
&& events.iter().all(|event| event.in_open_source_repo())
|
||||
&& related_files.iter().all(|file| file.in_open_source_repo);
|
||||
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()
|
||||
.map_or(false, |file| self.is_file_open_source(&project, file, cx))
|
||||
&& events.iter().all(|event| event.in_open_source_repo())
|
||||
&& related_files.iter().all(|file| file.in_open_source_repo));
|
||||
|
||||
let can_collect_data = !cfg!(test)
|
||||
&& 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::Mercury => {
|
||||
|
|
@ -3286,3 +3328,11 @@ pub fn init(cx: &mut App) {
|
|||
})
|
||||
.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 language::{
|
||||
Anchor, Buffer, Capability, CursorShape, Diagnostic, DiagnosticEntry, DiagnosticSet,
|
||||
DiagnosticSeverity, Operation, Point, Selection, SelectionGoal,
|
||||
Anchor, Buffer, BufferEditSource, Capability, CursorShape, Diagnostic, DiagnosticEntry,
|
||||
DiagnosticSet, DiagnosticSeverity, Operation, Point, Selection, SelectionGoal,
|
||||
};
|
||||
|
||||
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]
|
||||
async fn test_simple_request(cx: &mut TestAppContext) {
|
||||
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 {
|
||||
predict: mpsc::UnboundedReceiver<(
|
||||
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 http_client = FakeHttpClient::create(|_req| async move {
|
||||
Ok(gpui::http_client::Response::builder()
|
||||
.status(401)
|
||||
.body("Unauthorized".into())
|
||||
.unwrap())
|
||||
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()
|
||||
.status(401)
|
||||
.body("Unauthorized".into())
|
||||
.unwrap())
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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)
|
||||
});
|
||||
|
||||
let result = completion_task.await;
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Without authentication and without custom URL, prediction should fail"
|
||||
);
|
||||
assert!(completion_task.await.unwrap().is_none());
|
||||
assert_eq!(request_count.load(std::sync::atomic::Ordering::SeqCst), 0);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ pub fn request_prediction_with_zeta(
|
|||
Vec<crate::StoredEvent>,
|
||||
Task<Result<collections::HashMap<Arc<Path>, Entity<BufferDiff>>>>,
|
||||
)>,
|
||||
repo_url: Option<String>,
|
||||
cx: &mut Context<EditPredictionStore>,
|
||||
) -> Task<Result<Option<EditPredictionResult>>> {
|
||||
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 repo_url = if 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 repo_url = repo_url.filter(|_| can_collect_data);
|
||||
let client = store.client.clone();
|
||||
let llm_token = store.llm_token.clone();
|
||||
let organization_id = store
|
||||
|
|
|
|||
|
|
@ -323,7 +323,8 @@ pub struct SplitSelectionIntoLines {
|
|||
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)]
|
||||
#[action(namespace = editor)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
|
|
@ -332,7 +333,8 @@ pub struct GoToDiagnostic {
|
|||
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)]
|
||||
#[action(namespace = editor)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
|
|
|
|||
|
|
@ -77,7 +77,8 @@ impl Editor {
|
|||
if !self.diagnostics_enabled() {
|
||||
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(
|
||||
|
|
@ -89,10 +90,43 @@ impl Editor {
|
|||
if !self.diagnostics_enabled() {
|
||||
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,
|
||||
direction: Direction,
|
||||
severity: GoToDiagnosticSeverityFilter,
|
||||
|
|
@ -104,6 +138,71 @@ impl Editor {
|
|||
.selections
|
||||
.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;
|
||||
if let ActiveDiagnostic::Group(active_group) = &self.active_diagnostics
|
||||
&& active_group.active_range.start.to_offset(&buffer) == selection.start
|
||||
|
|
@ -111,28 +210,8 @@ impl Editor {
|
|||
active_group_id = Some(active_group.group_id);
|
||||
}
|
||||
|
||||
fn filtered<'a>(
|
||||
severity: GoToDiagnosticSeverityFilter,
|
||||
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 before = Self::diagnostics_before_cursor(&buffer, selection.start, severity);
|
||||
let after = Self::diagnostics_after_cursor(&buffer, selection.start, severity);
|
||||
|
||||
let mut found: Option<DiagnosticEntryRef<MultiBufferOffset>> = None;
|
||||
if direction == Direction::Prev {
|
||||
|
|
@ -158,31 +237,12 @@ impl Editor {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
let Some(next_diagnostic) = found else {
|
||||
return;
|
||||
};
|
||||
|
||||
let next_diagnostic_start = buffer.anchor_after(next_diagnostic.range.start);
|
||||
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);
|
||||
self.activate_diagnostic(&buffer, next_diagnostic, window, cx);
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
|
|
@ -324,32 +384,37 @@ impl Editor {
|
|||
return;
|
||||
}
|
||||
self.dismiss_diagnostics(cx);
|
||||
let snapshot = self.snapshot(window, 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
|
||||
.diagnostic_group(buffer_id, diagnostic.diagnostic.group_id)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let language_registry = self
|
||||
.project()
|
||||
.map(|project| project.read(cx).languages().clone());
|
||||
|
||||
let blocks = renderer.render_group(
|
||||
diagnostic_group,
|
||||
buffer_id,
|
||||
snapshot,
|
||||
cx.weak_entity(),
|
||||
language_registry,
|
||||
cx,
|
||||
);
|
||||
|
||||
self.display_map.update(cx, |display_map, cx| {
|
||||
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()
|
||||
};
|
||||
|
||||
let diagnostic_group = buffer
|
||||
.diagnostic_group(buffer_id, diagnostic.diagnostic.group_id)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let language_registry = self
|
||||
.project()
|
||||
.map(|project| project.read(cx).languages().clone());
|
||||
|
||||
let blocks = renderer.render_group(
|
||||
diagnostic_group,
|
||||
buffer_id,
|
||||
snapshot,
|
||||
cx.weak_entity(),
|
||||
language_registry,
|
||||
cx,
|
||||
);
|
||||
|
||||
let blocks = self.display_map.update(cx, |display_map, cx| {
|
||||
display_map.insert_blocks(blocks, cx).into_iter().collect()
|
||||
});
|
||||
self.active_diagnostics = ActiveDiagnostic::Group(ActiveDiagnosticGroup {
|
||||
active_range: buffer.anchor_before(diagnostic.range.start)
|
||||
..buffer.anchor_after(diagnostic.range.end),
|
||||
|
|
@ -516,4 +581,12 @@ impl Editor {
|
|||
self.scrollbar_marker_state.dirty = true;
|
||||
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 {
|
||||
multi_buffer::Event::Edited {
|
||||
edited_buffer,
|
||||
is_local,
|
||||
source,
|
||||
} => {
|
||||
self.scrollbar_marker_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_outline_symbols_at_cursor(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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
|
||||
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)
|
||||
.child(
|
||||
MarkdownElement::new(markdown, hover_markdown_style(window, cx))
|
||||
.scroll_handle(self.scroll_handle.clone())
|
||||
.code_block_renderer(markdown::CodeBlockRenderer::Default {
|
||||
copy_button_visibility: CopyButtonVisibility::Hidden,
|
||||
wrap_button_visibility: markdown::WrapButtonVisibility::Hidden,
|
||||
|
|
|
|||
|
|
@ -164,10 +164,12 @@ pub fn lsp_tasks(
|
|||
},
|
||||
));
|
||||
}
|
||||
lsp_tasks
|
||||
.entry(source_kind)
|
||||
.or_insert_with(Vec::new)
|
||||
.append(&mut new_lsp_tasks);
|
||||
if !new_lsp_tasks.is_empty() {
|
||||
lsp_tasks
|
||||
.entry(source_kind)
|
||||
.or_insert_with(Vec::new)
|
||||
.append(&mut new_lsp_tasks);
|
||||
}
|
||||
}
|
||||
}
|
||||
lsp_tasks.into_iter().collect()
|
||||
|
|
|
|||
|
|
@ -35,6 +35,18 @@ impl FeatureFlag for 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;
|
||||
|
||||
impl FeatureFlag for DiffReviewFeatureFlag {
|
||||
|
|
|
|||
|
|
@ -238,7 +238,11 @@ impl BlameRenderer for GitBlameRenderer {
|
|||
|
||||
let message = details
|
||||
.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());
|
||||
|
||||
let pull_request = details
|
||||
|
|
|
|||
|
|
@ -258,7 +258,11 @@ impl Render for CommitTooltip {
|
|||
.commit
|
||||
.message
|
||||
.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());
|
||||
|
||||
let pull_request = self
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ use crate::commit_view::CommitView;
|
|||
use crate::git_panel_settings::GitPanelScrollbarAccessor;
|
||||
use crate::project_diff::{self, BranchDiff, Diff, ProjectDiff};
|
||||
use crate::remote_output::{self, RemoteAction, SuccessMessage};
|
||||
use crate::solo_diff_view::SoloDiffView;
|
||||
use crate::{branch_picker, picker_prompt, render_remote_button};
|
||||
use crate::{
|
||||
git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector,
|
||||
|
|
@ -15,10 +16,7 @@ use anyhow::Context as _;
|
|||
use askpass::AskPassDelegate;
|
||||
use collections::{BTreeMap, HashMap, HashSet};
|
||||
use db::kvp::KeyValueStore;
|
||||
use editor::{
|
||||
Direction, Editor, EditorElement, EditorMode, MultiBuffer, MultiBufferOffset, SizingBehavior,
|
||||
actions::ExpandAllDiffHunks,
|
||||
};
|
||||
use editor::{Editor, EditorElement, EditorMode, MultiBuffer, MultiBufferOffset, SizingBehavior};
|
||||
use editor::{EditorStyle, RewrapOptions};
|
||||
use file_icons::FileIcons;
|
||||
use futures::StreamExt as _;
|
||||
|
|
@ -62,7 +60,7 @@ use project::{
|
|||
},
|
||||
project_settings::{GitPathStyle, ProjectSettings},
|
||||
};
|
||||
use prompt_store::{BuiltInPrompt, PromptId, PromptStore, RULES_FILE_NAMES};
|
||||
use prompt_store::RULES_FILE_NAMES;
|
||||
use proto::RpcError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsStore, StatusStyle, update_settings_file};
|
||||
|
|
@ -85,7 +83,7 @@ use workspace::SERIALIZATION_THROTTLE_TIME;
|
|||
use workspace::{
|
||||
Item, Workspace,
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
notifications::{DetachAndPromptErr, ErrorMessagePrompt, NotificationId, NotifyResultExt},
|
||||
notifications::{DetachAndPromptErr, ErrorMessagePrompt, NotificationId, NotifyTaskExt},
|
||||
};
|
||||
use zed_actions::{DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize};
|
||||
|
||||
|
|
@ -1385,63 +1383,22 @@ impl GitPanel {
|
|||
});
|
||||
}
|
||||
|
||||
fn open_file(
|
||||
fn open_solo_diff(
|
||||
&mut self,
|
||||
_: &menu::SecondaryConfirm,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
maybe!({
|
||||
let entry = self.entries.get(self.selected_entry?)?.status_entry()?;
|
||||
let active_repo = self.active_repository.as_ref()?;
|
||||
let path = active_repo
|
||||
.read(cx)
|
||||
.repo_path_to_project_path(&entry.repo_path, cx)?;
|
||||
if entry.status.is_deleted() {
|
||||
return None;
|
||||
}
|
||||
let entry = self
|
||||
.entries
|
||||
.get(self.selected_entry?)?
|
||||
.status_entry()?
|
||||
.clone();
|
||||
let repository = self.active_repository.clone()?;
|
||||
|
||||
let open_task = self
|
||||
.workspace
|
||||
.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();
|
||||
SoloDiffView::open_or_focus(entry, repository, self.workspace.clone(), window, cx)
|
||||
.detach_and_notify_err(self.workspace.clone(), window, cx);
|
||||
|
||||
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(
|
||||
prompt: &str,
|
||||
user_agents_md: Option<&str>,
|
||||
|
|
@ -2803,7 +2746,7 @@ impl GitPanel {
|
|||
.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| {
|
||||
this.commit_editor
|
||||
|
|
@ -5984,7 +5927,7 @@ impl GitPanel {
|
|||
)
|
||||
.separator()
|
||||
.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| {
|
||||
context_menu
|
||||
.separator()
|
||||
|
|
@ -6263,7 +6206,7 @@ impl GitPanel {
|
|||
this.selected_entry = Some(ix);
|
||||
cx.notify();
|
||||
if event.click_count() > 1 || event.modifiers().secondary() {
|
||||
this.open_file(&Default::default(), window, cx)
|
||||
this.open_solo_diff(&Default::default(), window, cx)
|
||||
} else {
|
||||
this.open_diff(&Default::default(), 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::close_panel))
|
||||
.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_editor))
|
||||
.on_action(cx.listener(Self::expand_commit_editor))
|
||||
|
|
@ -7147,7 +7090,11 @@ impl Component for PanelRepoFooter {
|
|||
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 no_remote_upstream = Some(UpstreamTracking::Gone);
|
||||
let ahead_of_upstream = Some(
|
||||
|
|
@ -7221,177 +7168,176 @@ impl Component for PanelRepoFooter {
|
|||
}
|
||||
|
||||
let example_width = px(340.);
|
||||
Some(
|
||||
v_flex()
|
||||
.gap_6()
|
||||
.w_full()
|
||||
.flex_none()
|
||||
.children(vec![
|
||||
example_group_with_title(
|
||||
"Action Button States",
|
||||
vec![
|
||||
single_example(
|
||||
"No Branch",
|
||||
div()
|
||||
.w(example_width)
|
||||
.overflow_hidden()
|
||||
.child(PanelRepoFooter::new_preview(active_repository(1), None))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Remote status unknown",
|
||||
div()
|
||||
.w(example_width)
|
||||
.overflow_hidden()
|
||||
.child(PanelRepoFooter::new_preview(
|
||||
active_repository(2),
|
||||
Some(branch(unknown_upstream)),
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"No Remote Upstream",
|
||||
div()
|
||||
.w(example_width)
|
||||
.overflow_hidden()
|
||||
.child(PanelRepoFooter::new_preview(
|
||||
active_repository(3),
|
||||
Some(branch(no_remote_upstream)),
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Not Ahead or Behind",
|
||||
div()
|
||||
.w(example_width)
|
||||
.overflow_hidden()
|
||||
.child(PanelRepoFooter::new_preview(
|
||||
active_repository(4),
|
||||
Some(branch(not_ahead_or_behind_upstream)),
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Behind remote",
|
||||
div()
|
||||
.w(example_width)
|
||||
.overflow_hidden()
|
||||
.child(PanelRepoFooter::new_preview(
|
||||
active_repository(5),
|
||||
Some(branch(behind_upstream)),
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Ahead of remote",
|
||||
div()
|
||||
.w(example_width)
|
||||
.overflow_hidden()
|
||||
.child(PanelRepoFooter::new_preview(
|
||||
active_repository(6),
|
||||
Some(branch(ahead_of_upstream)),
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Ahead and behind remote",
|
||||
div()
|
||||
.w(example_width)
|
||||
.overflow_hidden()
|
||||
.child(PanelRepoFooter::new_preview(
|
||||
active_repository(7),
|
||||
Some(branch(ahead_and_behind_upstream)),
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
],
|
||||
)
|
||||
.grow()
|
||||
.vertical(),
|
||||
])
|
||||
.children(vec![
|
||||
example_group_with_title(
|
||||
"Labels",
|
||||
vec![
|
||||
single_example(
|
||||
"Short Branch & Repo",
|
||||
div()
|
||||
.w(example_width)
|
||||
.overflow_hidden()
|
||||
.child(PanelRepoFooter::new_preview(
|
||||
SharedString::from("zed"),
|
||||
Some(custom("main", behind_upstream)),
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Long Branch",
|
||||
div()
|
||||
.w(example_width)
|
||||
.overflow_hidden()
|
||||
.child(PanelRepoFooter::new_preview(
|
||||
SharedString::from("zed"),
|
||||
Some(custom(
|
||||
"redesign-and-update-git-ui-list-entry-style",
|
||||
behind_upstream,
|
||||
)),
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Long Repo",
|
||||
div()
|
||||
.w(example_width)
|
||||
.overflow_hidden()
|
||||
.child(PanelRepoFooter::new_preview(
|
||||
SharedString::from("zed-industries-community-examples"),
|
||||
Some(custom("gpui", ahead_of_upstream)),
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Long Repo & Branch",
|
||||
div()
|
||||
.w(example_width)
|
||||
.overflow_hidden()
|
||||
.child(PanelRepoFooter::new_preview(
|
||||
SharedString::from("zed-industries-community-examples"),
|
||||
Some(custom(
|
||||
"redesign-and-update-git-ui-list-entry-style",
|
||||
behind_upstream,
|
||||
)),
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Uppercase Repo",
|
||||
div()
|
||||
.w(example_width)
|
||||
.overflow_hidden()
|
||||
.child(PanelRepoFooter::new_preview(
|
||||
SharedString::from("LICENSES"),
|
||||
Some(custom("main", ahead_of_upstream)),
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Uppercase Branch",
|
||||
div()
|
||||
.w(example_width)
|
||||
.overflow_hidden()
|
||||
.child(PanelRepoFooter::new_preview(
|
||||
SharedString::from("zed"),
|
||||
Some(custom("update-README", behind_upstream)),
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
],
|
||||
)
|
||||
.grow()
|
||||
.vertical(),
|
||||
])
|
||||
.into_any_element(),
|
||||
)
|
||||
|
||||
v_flex()
|
||||
.gap_6()
|
||||
.w_full()
|
||||
.flex_none()
|
||||
.children(vec![
|
||||
example_group_with_title(
|
||||
"Action Button States",
|
||||
vec![
|
||||
single_example(
|
||||
"No Branch",
|
||||
div()
|
||||
.w(example_width)
|
||||
.overflow_hidden()
|
||||
.child(PanelRepoFooter::new_preview(active_repository(1), None))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Remote status unknown",
|
||||
div()
|
||||
.w(example_width)
|
||||
.overflow_hidden()
|
||||
.child(PanelRepoFooter::new_preview(
|
||||
active_repository(2),
|
||||
Some(branch(unknown_upstream)),
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"No Remote Upstream",
|
||||
div()
|
||||
.w(example_width)
|
||||
.overflow_hidden()
|
||||
.child(PanelRepoFooter::new_preview(
|
||||
active_repository(3),
|
||||
Some(branch(no_remote_upstream)),
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Not Ahead or Behind",
|
||||
div()
|
||||
.w(example_width)
|
||||
.overflow_hidden()
|
||||
.child(PanelRepoFooter::new_preview(
|
||||
active_repository(4),
|
||||
Some(branch(not_ahead_or_behind_upstream)),
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Behind remote",
|
||||
div()
|
||||
.w(example_width)
|
||||
.overflow_hidden()
|
||||
.child(PanelRepoFooter::new_preview(
|
||||
active_repository(5),
|
||||
Some(branch(behind_upstream)),
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Ahead of remote",
|
||||
div()
|
||||
.w(example_width)
|
||||
.overflow_hidden()
|
||||
.child(PanelRepoFooter::new_preview(
|
||||
active_repository(6),
|
||||
Some(branch(ahead_of_upstream)),
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Ahead and behind remote",
|
||||
div()
|
||||
.w(example_width)
|
||||
.overflow_hidden()
|
||||
.child(PanelRepoFooter::new_preview(
|
||||
active_repository(7),
|
||||
Some(branch(ahead_and_behind_upstream)),
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
],
|
||||
)
|
||||
.grow()
|
||||
.vertical(),
|
||||
])
|
||||
.children(vec![
|
||||
example_group_with_title(
|
||||
"Labels",
|
||||
vec![
|
||||
single_example(
|
||||
"Short Branch & Repo",
|
||||
div()
|
||||
.w(example_width)
|
||||
.overflow_hidden()
|
||||
.child(PanelRepoFooter::new_preview(
|
||||
SharedString::from("zed"),
|
||||
Some(custom("main", behind_upstream)),
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Long Branch",
|
||||
div()
|
||||
.w(example_width)
|
||||
.overflow_hidden()
|
||||
.child(PanelRepoFooter::new_preview(
|
||||
SharedString::from("zed"),
|
||||
Some(custom(
|
||||
"redesign-and-update-git-ui-list-entry-style",
|
||||
behind_upstream,
|
||||
)),
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Long Repo",
|
||||
div()
|
||||
.w(example_width)
|
||||
.overflow_hidden()
|
||||
.child(PanelRepoFooter::new_preview(
|
||||
SharedString::from("zed-industries-community-examples"),
|
||||
Some(custom("gpui", ahead_of_upstream)),
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Long Repo & Branch",
|
||||
div()
|
||||
.w(example_width)
|
||||
.overflow_hidden()
|
||||
.child(PanelRepoFooter::new_preview(
|
||||
SharedString::from("zed-industries-community-examples"),
|
||||
Some(custom(
|
||||
"redesign-and-update-git-ui-list-entry-style",
|
||||
behind_upstream,
|
||||
)),
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Uppercase Repo",
|
||||
div()
|
||||
.w(example_width)
|
||||
.overflow_hidden()
|
||||
.child(PanelRepoFooter::new_preview(
|
||||
SharedString::from("LICENSES"),
|
||||
Some(custom("main", ahead_of_upstream)),
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Uppercase Branch",
|
||||
div()
|
||||
.w(example_width)
|
||||
.overflow_hidden()
|
||||
.child(PanelRepoFooter::new_preview(
|
||||
SharedString::from("zed"),
|
||||
Some(custom("update-README", behind_upstream)),
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
],
|
||||
)
|
||||
.grow()
|
||||
.vertical(),
|
||||
])
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ pub mod picker_prompt;
|
|||
pub mod project_diff;
|
||||
pub(crate) mod remote_output;
|
||||
pub mod repository_selector;
|
||||
pub mod solo_diff_view;
|
||||
pub mod stash_picker;
|
||||
pub mod text_diff_view;
|
||||
pub mod worktree_names;
|
||||
|
|
@ -1038,7 +1039,12 @@ impl Component for GitStatusIcon {
|
|||
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 {
|
||||
FileStatus::Tracked(git::status::TrackedStatus {
|
||||
index_status: code,
|
||||
|
|
@ -1055,20 +1061,18 @@ impl Component for GitStatusIcon {
|
|||
}
|
||||
.into();
|
||||
|
||||
Some(
|
||||
v_flex()
|
||||
.gap_6()
|
||||
.children(vec![example_group(vec![
|
||||
single_example("Modified", GitStatusIcon::new(modified).into_any_element()),
|
||||
single_example("Added", GitStatusIcon::new(added).into_any_element()),
|
||||
single_example("Deleted", GitStatusIcon::new(deleted).into_any_element()),
|
||||
single_example(
|
||||
"Conflicted",
|
||||
GitStatusIcon::new(conflict).into_any_element(),
|
||||
),
|
||||
])])
|
||||
.into_any_element(),
|
||||
)
|
||||
v_flex()
|
||||
.gap_6()
|
||||
.children(vec![example_group(vec![
|
||||
single_example("Modified", GitStatusIcon::new(modified).into_any_element()),
|
||||
single_example("Added", GitStatusIcon::new(added).into_any_element()),
|
||||
single_example("Deleted", GitStatusIcon::new(deleted).into_any_element()),
|
||||
single_example(
|
||||
"Conflicted",
|
||||
GitStatusIcon::new(conflict).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::git_store::RepositoryEvent;
|
||||
use ui::{
|
||||
Button, Divider, HighlightedLabel, IconButton, KeyBinding, ListItem, ListItemSpacing, Tooltip,
|
||||
prelude::*,
|
||||
Button, CommonAnimationExt as _, Divider, HighlightedLabel, IconButton, KeyBinding, ListItem,
|
||||
ListItemSpacing, Tooltip, prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use util::paths::PathExt;
|
||||
|
|
@ -116,6 +116,7 @@ impl WorktreePicker {
|
|||
show_footer,
|
||||
modifiers: Modifiers::default(),
|
||||
hovered_delete_index: None,
|
||||
deleting_worktree_paths: HashSet::default(),
|
||||
};
|
||||
|
||||
let picker = cx.new(|cx| {
|
||||
|
|
@ -313,6 +314,7 @@ struct WorktreePickerDelegate {
|
|||
show_footer: bool,
|
||||
modifiers: Modifiers,
|
||||
hovered_delete_index: Option<usize>,
|
||||
deleting_worktree_paths: HashSet<PathBuf>,
|
||||
}
|
||||
|
||||
fn remove_worktree_command(path: &Path, force: bool) -> String {
|
||||
|
|
@ -420,18 +422,18 @@ impl WorktreePickerDelegate {
|
|||
fn build_fixed_entries(&self) -> Vec<WorktreeEntry> {
|
||||
let mut entries = Vec::new();
|
||||
|
||||
if !self.has_multiple_repositories {
|
||||
if let Some(ref default_branch) = self.default_branch {
|
||||
let is_different = self
|
||||
.current_branch_name
|
||||
.as_ref()
|
||||
.is_none_or(|current| current != &default_branch.branch_name);
|
||||
entries.push(WorktreeEntry::CreateFromDefaultBranch {
|
||||
default_branch: default_branch.clone(),
|
||||
});
|
||||
if is_different {
|
||||
entries.push(WorktreeEntry::CreateFromCurrentBranch);
|
||||
}
|
||||
if self.has_multiple_repositories {
|
||||
entries.push(WorktreeEntry::CreateFromCurrentBranch);
|
||||
} else if let Some(ref default_branch) = self.default_branch {
|
||||
let is_different = self
|
||||
.current_branch_name
|
||||
.as_ref()
|
||||
.is_none_or(|current| current != &default_branch.branch_name);
|
||||
entries.push(WorktreeEntry::CreateFromDefaultBranch {
|
||||
default_branch: default_branch.clone(),
|
||||
});
|
||||
if is_different {
|
||||
entries.push(WorktreeEntry::CreateFromCurrentBranch);
|
||||
}
|
||||
} else {
|
||||
entries.push(WorktreeEntry::CreateFromCurrentBranch);
|
||||
|
|
@ -464,7 +466,7 @@ impl WorktreePickerDelegate {
|
|||
}
|
||||
|
||||
fn delete_worktree(
|
||||
&self,
|
||||
&mut self,
|
||||
ix: usize,
|
||||
force: bool,
|
||||
window: &mut Window,
|
||||
|
|
@ -476,7 +478,9 @@ impl WorktreePickerDelegate {
|
|||
let WorktreeEntry::Worktree { worktree, .. } = entry else {
|
||||
return;
|
||||
};
|
||||
if !self.can_delete_worktree(worktree) {
|
||||
if !self.can_delete_worktree(worktree)
|
||||
|| self.deleting_worktree_paths.contains(&worktree.path)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -493,10 +497,27 @@ impl WorktreePickerDelegate {
|
|||
);
|
||||
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| {
|
||||
let initial_result = repo
|
||||
let initial_result = match repo
|
||||
.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 {
|
||||
Ok(()) => (Ok(()), force),
|
||||
|
|
@ -510,6 +531,12 @@ impl WorktreePickerDelegate {
|
|||
.flatten();
|
||||
|
||||
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| {
|
||||
window.prompt(
|
||||
PromptLevel::Warning,
|
||||
|
|
@ -524,9 +551,39 @@ impl WorktreePickerDelegate {
|
|||
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))
|
||||
.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 {
|
||||
log::error!("Failed to force remove worktree: {error}");
|
||||
|
|
@ -540,6 +597,12 @@ impl WorktreePickerDelegate {
|
|||
};
|
||||
|
||||
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() {
|
||||
cx.update(|_window, cx| {
|
||||
show_error_toast(
|
||||
|
|
@ -555,6 +618,7 @@ impl WorktreePickerDelegate {
|
|||
}
|
||||
|
||||
picker.update_in(cx, |picker, _window, cx| {
|
||||
picker.delegate.deleting_worktree_paths.remove(&path);
|
||||
picker.delegate.matches.retain(|e| {
|
||||
!matches!(e, WorktreeEntry::Worktree { worktree, .. } if worktree.path == path)
|
||||
});
|
||||
|
|
@ -814,6 +878,10 @@ impl PickerDelegate for WorktreePickerDelegate {
|
|||
}
|
||||
}
|
||||
WorktreeEntry::Worktree { worktree, .. } => {
|
||||
if self.deleting_worktree_paths.contains(&worktree.path) {
|
||||
return;
|
||||
}
|
||||
|
||||
let is_current = self.project_worktree_paths.contains(&worktree.path);
|
||||
|
||||
if !is_current {
|
||||
|
|
@ -956,6 +1024,7 @@ impl PickerDelegate for WorktreePickerDelegate {
|
|||
let sha = worktree.sha.chars().take(7).collect::<String>();
|
||||
|
||||
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 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 =
|
||||
IconButton::new(("open-new-window", ix), IconName::ArrowUpRight)
|
||||
.icon_size(IconSize::Small)
|
||||
|
|
@ -1045,6 +1131,13 @@ impl PickerDelegate for WorktreePickerDelegate {
|
|||
return;
|
||||
};
|
||||
if let WorktreeEntry::Worktree { worktree, .. } = entry {
|
||||
if picker
|
||||
.delegate
|
||||
.deleting_worktree_paths
|
||||
.contains(&worktree.path)
|
||||
{
|
||||
return;
|
||||
}
|
||||
window.dispatch_action(
|
||||
Box::new(OpenWorktreeInNewWindow {
|
||||
path: worktree.path.clone(),
|
||||
|
|
@ -1083,12 +1176,8 @@ impl PickerDelegate for WorktreePickerDelegate {
|
|||
.into()
|
||||
})
|
||||
.on_click(cx.listener(move |picker, _, window, cx| {
|
||||
picker.delegate.delete_worktree(
|
||||
ix,
|
||||
picker.delegate.modifiers.alt,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let force = picker.delegate.modifiers.alt;
|
||||
picker.delegate.delete_worktree(ix, force, window, cx);
|
||||
})),
|
||||
);
|
||||
|
||||
|
|
@ -1162,6 +1251,10 @@ impl PickerDelegate for WorktreePickerDelegate {
|
|||
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()
|
||||
.w_full()
|
||||
.p_1p5()
|
||||
|
|
@ -1188,7 +1281,14 @@ impl PickerDelegate for WorktreePickerDelegate {
|
|||
} else if is_existing_worktree {
|
||||
Some(
|
||||
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();
|
||||
this.child(
|
||||
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();
|
||||
this.child(
|
||||
Button::new("open-in-new-window", "Open in New Window")
|
||||
|
|
@ -1218,16 +1318,18 @@ impl PickerDelegate for WorktreePickerDelegate {
|
|||
}),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
Button::new("open-worktree", "Open")
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx)
|
||||
.map(|kb| kb.size(rems_from_px(12.))),
|
||||
)
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(menu::Confirm.boxed_clone(), cx)
|
||||
}),
|
||||
)
|
||||
.when(!is_deleting, |this| {
|
||||
this.child(
|
||||
Button::new("open-worktree", "Open")
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx)
|
||||
.map(|kb| kb.size(rems_from_px(12.))),
|
||||
)
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(menu::Confirm.boxed_clone(), cx)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.into_any(),
|
||||
)
|
||||
} 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(
|
||||
repository: &Entity<project::git_store::Repository>,
|
||||
worktree_path: &Path,
|
||||
|
|
@ -1497,6 +1626,54 @@ mod tests {
|
|||
.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]
|
||||
async fn test_remote_default_branch_is_preferred_create_target(cx: &mut TestAppContext) {
|
||||
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]
|
||||
async fn test_delete_dirty_worktree_prompts_for_force_delete(cx: &mut TestAppContext) {
|
||||
let (fs, worktree_picker, repository, worktree_path, mut cx) =
|
||||
|
|
@ -1557,19 +1765,96 @@ mod tests {
|
|||
picker.delegate.delete_worktree(index, false, window, cx);
|
||||
})
|
||||
});
|
||||
assert!(deleting_worktree_paths(&worktree_picker, &mut cx).contains(&worktree_path));
|
||||
|
||||
cx.run_until_parked();
|
||||
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.run_until_parked();
|
||||
|
||||
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!(
|
||||
!repo_contains_worktree(&repository, &worktree_path, &mut cx).await,
|
||||
"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]
|
||||
async fn test_force_delete_worktree_deletes_without_prompt(cx: &mut TestAppContext) {
|
||||
let (fs, worktree_picker, repository, worktree_path, mut cx) =
|
||||
|
|
@ -1589,9 +1874,17 @@ mod tests {
|
|||
picker.delegate.delete_worktree(index, true, window, cx);
|
||||
})
|
||||
});
|
||||
assert!(deleting_worktree_paths(&worktree_picker, &mut cx).contains(&worktree_path));
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
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!(
|
||||
!repo_contains_worktree(&repository, &worktree_path, &mut cx).await,
|
||||
"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.
|
||||
pub fn with_assets(self, asset_source: impl AssetSource) -> Self {
|
||||
let mut context_lock = self.0.borrow_mut();
|
||||
|
|
@ -666,6 +681,9 @@ pub struct App {
|
|||
pub(crate) window_update_stack: Vec<WindowId>,
|
||||
pub(crate) mode: GpuiMode,
|
||||
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,
|
||||
pending_updates: usize,
|
||||
quit_mode: QuitMode,
|
||||
|
|
@ -755,6 +773,7 @@ impl App {
|
|||
quit_mode: QuitMode::default(),
|
||||
quitting: false,
|
||||
cursor_hide_mode: CursorHideMode::default(),
|
||||
accessibility_force_disabled: false,
|
||||
|
||||
#[cfg(any(test, feature = "test-support", debug_assertions))]
|
||||
name: None,
|
||||
|
|
|
|||
|
|
@ -336,6 +336,20 @@ impl TestAppContext {
|
|||
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.
|
||||
#[track_caller]
|
||||
pub fn simulate_prompt_answer(&self, button: &str) {
|
||||
|
|
@ -1098,3 +1112,54 @@ impl AnyWindowHandle {
|
|||
.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> {
|
||||
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 {
|
||||
|
|
@ -1644,6 +1682,114 @@ mod test {
|
|||
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]
|
||||
fn test_measure_all_after_width_change(cx: &mut TestAppContext) {
|
||||
let cx = cx.add_empty_window();
|
||||
|
|
|
|||
|
|
@ -6,9 +6,7 @@ use scheduler::Instant;
|
|||
use scheduler::Scheduler;
|
||||
use std::{future::Future, marker::PhantomData, mem, pin::Pin, rc::Rc, sync::Arc, time::Duration};
|
||||
|
||||
pub use scheduler::{
|
||||
FallibleTask, ForegroundExecutor as SchedulerForegroundExecutor, Priority, Task,
|
||||
};
|
||||
pub use scheduler::{FallibleTask, LocalExecutor as SchedulerLocalExecutor, Priority, Task};
|
||||
|
||||
/// A pointer to the executor that is currently running,
|
||||
/// for spawning background tasks.
|
||||
|
|
@ -22,7 +20,7 @@ pub struct BackgroundExecutor {
|
|||
/// for spawning tasks on the main thread.
|
||||
#[derive(Clone)]
|
||||
pub struct ForegroundExecutor {
|
||||
inner: scheduler::ForegroundExecutor,
|
||||
inner: scheduler::LocalExecutor,
|
||||
dispatcher: Arc<dyn PlatformDispatcher>,
|
||||
not_send: PhantomData<Rc<()>>,
|
||||
}
|
||||
|
|
@ -280,18 +278,29 @@ impl ForegroundExecutor {
|
|||
)
|
||||
} else {
|
||||
let platform_scheduler = Arc::new(PlatformScheduler::new(dispatcher.clone()));
|
||||
let session_id = platform_scheduler.allocate_session_id();
|
||||
(platform_scheduler, session_id)
|
||||
let inner = platform_scheduler.foreground_executor();
|
||||
return Self {
|
||||
inner,
|
||||
dispatcher,
|
||||
not_send: PhantomData,
|
||||
};
|
||||
};
|
||||
|
||||
#[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 session_id = platform_scheduler.allocate_session_id();
|
||||
(platform_scheduler, session_id)
|
||||
platform_scheduler.foreground_executor()
|
||||
};
|
||||
|
||||
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 {
|
||||
inner,
|
||||
|
|
@ -366,7 +375,7 @@ impl ForegroundExecutor {
|
|||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn scheduler_executor(&self) -> SchedulerForegroundExecutor {
|
||||
pub fn scheduler_executor(&self) -> SchedulerLocalExecutor {
|
||||
self.inner.clone()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -139,8 +139,7 @@ impl PlatformDispatcher for TestDispatcher {
|
|||
}
|
||||
|
||||
fn dispatch_on_main_thread(&self, runnable: RunnableVariant, _priority: Priority) {
|
||||
self.scheduler
|
||||
.schedule_foreground(self.session_id, runnable);
|
||||
self.scheduler.schedule_local(self.session_id, runnable);
|
||||
}
|
||||
|
||||
fn dispatch_after(&self, _duration: Duration, _runnable: RunnableVariant) {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
use crate::{
|
||||
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
|
||||
DummyKeyboardMapper, ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay,
|
||||
PlatformHeadlessRenderer, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem,
|
||||
PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, SourceMetadata,
|
||||
Task, TestDisplay, TestWindow, ThermalState, WindowAppearance, WindowParams, size,
|
||||
DummyKeyboardMapper, ForegroundExecutor, Keymap, NoopTextSystem, PathPromptOptions, Platform,
|
||||
PlatformDisplay, PlatformHeadlessRenderer, PlatformKeyboardLayout, PlatformKeyboardMapper,
|
||||
PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream,
|
||||
SourceMetadata, Task, TestDisplay, TestWindow, ThermalState, WindowAppearance, WindowParams,
|
||||
size,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use collections::VecDeque;
|
||||
|
|
@ -85,6 +86,10 @@ struct TestPrompt {
|
|||
pub(crate) struct TestPrompts {
|
||||
multiple_choice: VecDeque<TestPrompt>,
|
||||
new_path: VecDeque<(PathBuf, oneshot::Sender<Result<Option<PathBuf>>>)>,
|
||||
paths: VecDeque<(
|
||||
PathPromptOptions,
|
||||
oneshot::Sender<Result<Option<Vec<PathBuf>>>>,
|
||||
)>,
|
||||
}
|
||||
|
||||
impl TestPlatform {
|
||||
|
|
@ -147,6 +152,33 @@ impl TestPlatform {
|
|||
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]
|
||||
pub(crate) fn simulate_prompt_answer(&self, response: &str) {
|
||||
let prompt = self
|
||||
|
|
@ -348,9 +380,11 @@ impl Platform for TestPlatform {
|
|||
|
||||
fn prompt_for_paths(
|
||||
&self,
|
||||
_options: crate::PathPromptOptions,
|
||||
options: crate::PathPromptOptions,
|
||||
) -> 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(
|
||||
|
|
|
|||
|
|
@ -3,10 +3,14 @@ use async_task::Runnable;
|
|||
use chrono::{DateTime, Utc};
|
||||
use futures::channel::oneshot;
|
||||
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"))]
|
||||
use std::task::{Context, Poll};
|
||||
use std::{
|
||||
any::Any,
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
.dispatch_on_main_thread(runnable, Priority::default());
|
||||
}
|
||||
|
|
@ -133,6 +147,21 @@ impl Scheduler for PlatformScheduler {
|
|||
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> {
|
||||
None
|
||||
}
|
||||
|
|
@ -152,3 +181,261 @@ impl Clock for PlatformClock {
|
|||
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(_) => {}
|
||||
}
|
||||
|
||||
let accessibility_force_disabled = cx.accessibility_force_disabled;
|
||||
let a11y_active_flag = Arc::new(AtomicBool::new(false));
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
{
|
||||
if !accessibility_force_disabled {
|
||||
let initial_tree = accesskit::TreeUpdate {
|
||||
nodes: vec![(ROOT_NODE_ID, accesskit::Node::new(accesskit::Role::Window))],
|
||||
tree: Some(accesskit::Tree::new(ROOT_NODE_ID)),
|
||||
|
|
@ -1717,7 +1718,7 @@ impl Window {
|
|||
captured_hitbox: None,
|
||||
#[cfg(any(feature = "inspector", debug_assertions))]
|
||||
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
|
||||
/// needed to dispatch incoming action requests back to the right elements.
|
||||
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.
|
||||
///
|
||||
/// Updated by AccessKit using callbacks provided to the adapter. Can change
|
||||
|
|
@ -131,8 +135,9 @@ pub(crate) struct 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 {
|
||||
force_disabled,
|
||||
active_flag,
|
||||
active_this_frame: false,
|
||||
nodes: A11yNodeBuilder::new(),
|
||||
|
|
@ -147,7 +152,7 @@ impl A11y {
|
|||
/// See the docs for [`Self::active_flag`] and [`Self::active_this_frame`]
|
||||
/// for more commentary.
|
||||
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 {
|
||||
|
|
@ -164,7 +169,21 @@ impl A11y {
|
|||
|
||||
/// Finalize the tree and produce a [`TreeUpdate`] for the platform adapter.
|
||||
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]
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
tempfile.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
|
|
|
|||
|
|
@ -449,45 +449,48 @@ pub fn all_schema_file_associations(
|
|||
.flat_map(|(_, glob_strings)| glob_strings)
|
||||
.cloned();
|
||||
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!([
|
||||
{
|
||||
"fileMatch": [
|
||||
schema_file_match(paths::settings_file()),
|
||||
],
|
||||
"fileMatch": settings_file_matches,
|
||||
"url": format!("{SCHEMA_URI_PREFIX}settings"),
|
||||
},
|
||||
{
|
||||
"fileMatch": [
|
||||
paths::local_settings_file_relative_path()],
|
||||
paths::local_settings_file_relative_path()
|
||||
],
|
||||
"url": format!("{SCHEMA_URI_PREFIX}project_settings"),
|
||||
},
|
||||
{
|
||||
"fileMatch": [schema_file_match(paths::keymap_file())],
|
||||
"fileMatch": keymap_file_matches,
|
||||
"url": format!("{SCHEMA_URI_PREFIX}keymap"),
|
||||
},
|
||||
{
|
||||
"fileMatch": [
|
||||
schema_file_match(paths::tasks_file()),
|
||||
paths::local_tasks_file_relative_path()
|
||||
],
|
||||
"fileMatch": tasks_file_matches,
|
||||
"url": format!("{SCHEMA_URI_PREFIX}tasks"),
|
||||
},
|
||||
{
|
||||
"fileMatch": [
|
||||
schema_file_match(paths::debug_scenarios_file()),
|
||||
paths::local_debug_file_relative_path()
|
||||
],
|
||||
"fileMatch": debug_file_matches,
|
||||
"url": format!("{SCHEMA_URI_PREFIX}debug_tasks"),
|
||||
},
|
||||
{
|
||||
"fileMatch": [
|
||||
schema_file_match(
|
||||
paths::snippets_dir()
|
||||
.join("*.json")
|
||||
.as_path()
|
||||
)
|
||||
],
|
||||
"fileMatch": snippet_file_matches,
|
||||
"url": format!("{SCHEMA_URI_PREFIX}snippets"),
|
||||
},
|
||||
{
|
||||
|
|
@ -619,11 +622,80 @@ fn root_schema_from_action_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]
|
||||
fn schema_file_match(path: &std::path::Path) -> String {
|
||||
path.strip_prefix(path.parent().unwrap().parent().unwrap())
|
||||
.unwrap()
|
||||
fn stripped_match(path: &std::path::Path) -> String {
|
||||
let parent = path.parent().and_then(|p| p.parent()).unwrap_or(path);
|
||||
path.strip_prefix(parent)
|
||||
.unwrap_or(path)
|
||||
.display()
|
||||
.to_string()
|
||||
.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.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum BufferEvent {
|
||||
|
|
@ -307,7 +320,7 @@ pub enum BufferEvent {
|
|||
is_local: bool,
|
||||
},
|
||||
/// The buffer was edited.
|
||||
Edited { is_local: bool },
|
||||
Edited { source: BufferEditSource },
|
||||
/// The buffer's `dirty` bit changed.
|
||||
DirtyChanged,
|
||||
/// The buffer was saved.
|
||||
|
|
@ -2433,6 +2446,14 @@ impl Buffer {
|
|||
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
|
||||
/// that occur within a short period of time will be grouped together. This
|
||||
/// is controlled by the buffer's undo grouping duration.
|
||||
|
|
@ -2440,6 +2461,15 @@ impl Buffer {
|
|||
&mut self,
|
||||
now: Instant,
|
||||
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> {
|
||||
assert!(self.transaction_depth > 0);
|
||||
self.transaction_depth -= 1;
|
||||
|
|
@ -2449,7 +2479,7 @@ impl Buffer {
|
|||
false
|
||||
};
|
||||
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)
|
||||
} else {
|
||||
None
|
||||
|
|
@ -2844,7 +2874,7 @@ impl Buffer {
|
|||
&mut self,
|
||||
old_version: &clock::Global,
|
||||
was_dirty: bool,
|
||||
is_local: bool,
|
||||
source: BufferEditSource,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.was_changed();
|
||||
|
|
@ -2854,7 +2884,7 @@ impl Buffer {
|
|||
}
|
||||
|
||||
self.reparse(cx, true);
|
||||
cx.emit(BufferEvent::Edited { is_local });
|
||||
cx.emit(BufferEvent::Edited { source });
|
||||
let is_dirty = self.is_dirty();
|
||||
if was_dirty != is_dirty {
|
||||
cx.emit(BufferEvent::DirtyChanged);
|
||||
|
|
@ -2976,7 +3006,7 @@ impl Buffer {
|
|||
self.text.apply_ops(buffer_ops);
|
||||
self.deferred_ops.insert(deferred_ops);
|
||||
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
|
||||
// selection update.
|
||||
cx.notify();
|
||||
|
|
@ -3131,7 +3161,7 @@ impl Buffer {
|
|||
|
||||
if let Some((transaction_id, operation)) = self.text.undo() {
|
||||
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);
|
||||
Some(transaction_id)
|
||||
} else {
|
||||
|
|
@ -3149,7 +3179,7 @@ impl Buffer {
|
|||
let old_version = self.version.clone();
|
||||
if let Some(operation) = self.text.undo_transaction(transaction_id) {
|
||||
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
|
||||
} else {
|
||||
false
|
||||
|
|
@ -3171,7 +3201,7 @@ impl Buffer {
|
|||
self.send_operation(Operation::Buffer(operation), true, cx);
|
||||
}
|
||||
if undone {
|
||||
self.did_edit(&old_version, was_dirty, true, cx)
|
||||
self.did_edit(&old_version, was_dirty, BufferEditSource::User, cx)
|
||||
}
|
||||
undone
|
||||
}
|
||||
|
|
@ -3181,7 +3211,7 @@ impl Buffer {
|
|||
let operation = self.text.undo_operations(counts);
|
||||
let old_version = self.version.clone();
|
||||
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.
|
||||
|
|
@ -3191,7 +3221,7 @@ impl Buffer {
|
|||
|
||||
if let Some((transaction_id, operation)) = self.text.redo() {
|
||||
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);
|
||||
Some(transaction_id)
|
||||
} else {
|
||||
|
|
@ -3232,7 +3262,7 @@ impl Buffer {
|
|||
self.send_operation(Operation::Buffer(operation), true, cx);
|
||||
}
|
||||
if redone {
|
||||
self.did_edit(&old_version, was_dirty, true, cx)
|
||||
self.did_edit(&old_version, was_dirty, BufferEditSource::User, cx)
|
||||
}
|
||||
redone
|
||||
}
|
||||
|
|
@ -3342,7 +3372,7 @@ impl Buffer {
|
|||
if !ops.is_empty() {
|
||||
for op in ops {
|
||||
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!(
|
||||
mem::take(&mut *buffer_1_events.lock()),
|
||||
vec![
|
||||
BufferEvent::Edited { is_local: true },
|
||||
BufferEvent::Edited {
|
||||
source: BufferEditSource::User
|
||||
},
|
||||
BufferEvent::DirtyChanged,
|
||||
BufferEvent::Edited { is_local: true },
|
||||
BufferEvent::Edited { is_local: true },
|
||||
BufferEvent::Edited {
|
||||
source: BufferEditSource::User
|
||||
},
|
||||
BufferEvent::Edited {
|
||||
source: BufferEditSource::User
|
||||
},
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
mem::take(&mut *buffer_2_events.lock()),
|
||||
vec![
|
||||
BufferEvent::Edited { is_local: false },
|
||||
BufferEvent::Edited {
|
||||
source: BufferEditSource::Remote
|
||||
},
|
||||
BufferEvent::DirtyChanged
|
||||
]
|
||||
);
|
||||
|
|
@ -487,14 +495,18 @@ fn test_edit_events(cx: &mut gpui::App) {
|
|||
assert_eq!(
|
||||
mem::take(&mut *buffer_1_events.lock()),
|
||||
vec![
|
||||
BufferEvent::Edited { is_local: true },
|
||||
BufferEvent::Edited {
|
||||
source: BufferEditSource::User
|
||||
},
|
||||
BufferEvent::DirtyChanged,
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
mem::take(&mut *buffer_2_events.lock()),
|
||||
vec![
|
||||
BufferEvent::Edited { is_local: false },
|
||||
BufferEvent::Edited {
|
||||
source: BufferEditSource::Remote
|
||||
},
|
||||
BufferEvent::DirtyChanged
|
||||
]
|
||||
);
|
||||
|
|
|
|||
|
|
@ -670,12 +670,22 @@ impl LanguageModel for BedrockModel {
|
|||
value: "high".into(),
|
||||
is_default: true,
|
||||
},
|
||||
language_model::LanguageModelEffortLevel {
|
||||
name: "XHigh".into(),
|
||||
value: "xhigh".into(),
|
||||
is_default: false,
|
||||
},
|
||||
language_model::LanguageModelEffortLevel {
|
||||
name: "Max".into(),
|
||||
value: "max".into(),
|
||||
is_default: false,
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
.filter(|effort_level| {
|
||||
effort_level.value != "xhigh" || self.model.supports_xhigh_adaptive_thinking()
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
|
|
@ -1128,6 +1138,7 @@ pub fn into_bedrock(
|
|||
"low" => Some(bedrock::BedrockAdaptiveThinkingEffort::Low),
|
||||
"medium" => Some(bedrock::BedrockAdaptiveThinkingEffort::Medium),
|
||||
"high" => Some(bedrock::BedrockAdaptiveThinkingEffort::High),
|
||||
"xhigh" => Some(bedrock::BedrockAdaptiveThinkingEffort::XHigh),
|
||||
"max" => Some(bedrock::BedrockAdaptiveThinkingEffort::Max),
|
||||
_ => None,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -721,7 +721,13 @@ impl Component for ZedAiConfiguration {
|
|||
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 {
|
||||
plan: Option<Plan>,
|
||||
is_connected: bool,
|
||||
|
|
@ -741,94 +747,92 @@ impl Component for ZedAiConfiguration {
|
|||
.into_any_element()
|
||||
};
|
||||
|
||||
Some(
|
||||
v_flex()
|
||||
.p_4()
|
||||
.gap_4()
|
||||
.children(vec![
|
||||
single_example(
|
||||
"Not connected",
|
||||
configuration(PreviewConfiguration {
|
||||
plan: None,
|
||||
is_connected: false,
|
||||
is_zed_model_provider_enabled: true,
|
||||
eligible_for_trial: false,
|
||||
}),
|
||||
),
|
||||
single_example(
|
||||
"Accept Terms of Service",
|
||||
configuration(PreviewConfiguration {
|
||||
plan: None,
|
||||
is_connected: true,
|
||||
is_zed_model_provider_enabled: true,
|
||||
eligible_for_trial: true,
|
||||
}),
|
||||
),
|
||||
single_example(
|
||||
"No Plan - Not eligible for trial",
|
||||
configuration(PreviewConfiguration {
|
||||
plan: None,
|
||||
is_connected: true,
|
||||
is_zed_model_provider_enabled: true,
|
||||
eligible_for_trial: false,
|
||||
}),
|
||||
),
|
||||
single_example(
|
||||
"No Plan - Eligible for trial",
|
||||
configuration(PreviewConfiguration {
|
||||
plan: None,
|
||||
is_connected: true,
|
||||
is_zed_model_provider_enabled: true,
|
||||
eligible_for_trial: true,
|
||||
}),
|
||||
),
|
||||
single_example(
|
||||
"Free Plan",
|
||||
configuration(PreviewConfiguration {
|
||||
plan: Some(Plan::ZedFree),
|
||||
is_connected: true,
|
||||
is_zed_model_provider_enabled: true,
|
||||
eligible_for_trial: true,
|
||||
}),
|
||||
),
|
||||
single_example(
|
||||
"Zed Pro Trial Plan",
|
||||
configuration(PreviewConfiguration {
|
||||
plan: Some(Plan::ZedProTrial),
|
||||
is_connected: true,
|
||||
is_zed_model_provider_enabled: true,
|
||||
eligible_for_trial: true,
|
||||
}),
|
||||
),
|
||||
single_example(
|
||||
"Zed Pro Plan",
|
||||
configuration(PreviewConfiguration {
|
||||
plan: Some(Plan::ZedPro),
|
||||
is_connected: true,
|
||||
is_zed_model_provider_enabled: true,
|
||||
eligible_for_trial: true,
|
||||
}),
|
||||
),
|
||||
single_example(
|
||||
"Business Plan - Zed models enabled",
|
||||
configuration(PreviewConfiguration {
|
||||
plan: Some(Plan::ZedBusiness),
|
||||
is_connected: true,
|
||||
is_zed_model_provider_enabled: true,
|
||||
eligible_for_trial: false,
|
||||
}),
|
||||
),
|
||||
single_example(
|
||||
"Business Plan - Zed models disabled",
|
||||
configuration(PreviewConfiguration {
|
||||
plan: Some(Plan::ZedBusiness),
|
||||
is_connected: true,
|
||||
is_zed_model_provider_enabled: false,
|
||||
eligible_for_trial: false,
|
||||
}),
|
||||
),
|
||||
])
|
||||
.into_any_element(),
|
||||
)
|
||||
v_flex()
|
||||
.p_4()
|
||||
.gap_4()
|
||||
.children(vec![
|
||||
single_example(
|
||||
"Not connected",
|
||||
configuration(PreviewConfiguration {
|
||||
plan: None,
|
||||
is_connected: false,
|
||||
is_zed_model_provider_enabled: true,
|
||||
eligible_for_trial: false,
|
||||
}),
|
||||
),
|
||||
single_example(
|
||||
"Accept Terms of Service",
|
||||
configuration(PreviewConfiguration {
|
||||
plan: None,
|
||||
is_connected: true,
|
||||
is_zed_model_provider_enabled: true,
|
||||
eligible_for_trial: true,
|
||||
}),
|
||||
),
|
||||
single_example(
|
||||
"No Plan - Not eligible for trial",
|
||||
configuration(PreviewConfiguration {
|
||||
plan: None,
|
||||
is_connected: true,
|
||||
is_zed_model_provider_enabled: true,
|
||||
eligible_for_trial: false,
|
||||
}),
|
||||
),
|
||||
single_example(
|
||||
"No Plan - Eligible for trial",
|
||||
configuration(PreviewConfiguration {
|
||||
plan: None,
|
||||
is_connected: true,
|
||||
is_zed_model_provider_enabled: true,
|
||||
eligible_for_trial: true,
|
||||
}),
|
||||
),
|
||||
single_example(
|
||||
"Free Plan",
|
||||
configuration(PreviewConfiguration {
|
||||
plan: Some(Plan::ZedFree),
|
||||
is_connected: true,
|
||||
is_zed_model_provider_enabled: true,
|
||||
eligible_for_trial: true,
|
||||
}),
|
||||
),
|
||||
single_example(
|
||||
"Zed Pro Trial Plan",
|
||||
configuration(PreviewConfiguration {
|
||||
plan: Some(Plan::ZedProTrial),
|
||||
is_connected: true,
|
||||
is_zed_model_provider_enabled: true,
|
||||
eligible_for_trial: true,
|
||||
}),
|
||||
),
|
||||
single_example(
|
||||
"Zed Pro Plan",
|
||||
configuration(PreviewConfiguration {
|
||||
plan: Some(Plan::ZedPro),
|
||||
is_connected: true,
|
||||
is_zed_model_provider_enabled: true,
|
||||
eligible_for_trial: true,
|
||||
}),
|
||||
),
|
||||
single_example(
|
||||
"Business Plan - Zed models enabled",
|
||||
configuration(PreviewConfiguration {
|
||||
plan: Some(Plan::ZedBusiness),
|
||||
is_connected: true,
|
||||
is_zed_model_provider_enabled: true,
|
||||
eligible_for_trial: false,
|
||||
}),
|
||||
),
|
||||
single_example(
|
||||
"Business Plan - Zed models disabled",
|
||||
configuration(PreviewConfiguration {
|
||||
plan: Some(Plan::ZedBusiness),
|
||||
is_connected: true,
|
||||
is_zed_model_provider_enabled: false,
|
||||
eligible_for_trial: false,
|
||||
}),
|
||||
),
|
||||
])
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -773,7 +773,6 @@ impl CopilotResponsesEventMapper {
|
|||
|
||||
copilot_responses::StreamEvent::Completed { response } => {
|
||||
let mut events = Vec::new();
|
||||
events.extend(self.capture_reasoning_items_from_output(&response.output));
|
||||
if let Some(usage) = response.usage {
|
||||
events.push(Ok(LanguageModelCompletionEvent::UsageUpdate(TokenUsage {
|
||||
input_tokens: usage.input_tokens.unwrap_or(0),
|
||||
|
|
@ -805,7 +804,6 @@ impl CopilotResponsesEventMapper {
|
|||
};
|
||||
|
||||
let mut events = Vec::new();
|
||||
events.extend(self.capture_reasoning_items_from_output(&response.output));
|
||||
if let Some(usage) = response.usage {
|
||||
events.push(Ok(LanguageModelCompletionEvent::UsageUpdate(TokenUsage {
|
||||
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(
|
||||
&mut self,
|
||||
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]
|
||||
fn into_copilot_responses_replays_reasoning_details() {
|
||||
let model = test_responses_model();
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ fn reasoning_effort_display(effort: ReasoningEffort) -> (&'static str, &'static
|
|||
ReasoningEffort::Low => ("Low", "low"),
|
||||
ReasoningEffort::Medium => ("Medium", "medium"),
|
||||
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 language_model::{
|
||||
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
|
||||
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
|
||||
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
||||
LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolSchemaFormat, RateLimiter,
|
||||
env_var,
|
||||
LanguageModelCompletionEvent, LanguageModelEffortLevel, LanguageModelId, LanguageModelName,
|
||||
LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
|
||||
LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice,
|
||||
LanguageModelToolSchemaFormat, RateLimiter, env_var,
|
||||
};
|
||||
use open_ai::ResponseStreamEvent;
|
||||
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 {
|
||||
fn id(&self) -> LanguageModelId {
|
||||
self.id.clone()
|
||||
|
|
@ -291,6 +360,15 @@ impl LanguageModel for XAiLanguageModel {
|
|||
| 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 {
|
||||
if self.model.requires_json_schema_subset() {
|
||||
LanguageModelToolSchemaFormat::JsonSchemaSubset
|
||||
|
|
@ -329,13 +407,14 @@ impl LanguageModel for XAiLanguageModel {
|
|||
LanguageModelCompletionError,
|
||||
>,
|
||||
> {
|
||||
let reasoning_effort = reasoning_effort_for_request(&request, &self.model);
|
||||
let request = crate::provider::open_ai::into_open_ai(
|
||||
request,
|
||||
self.model.id(),
|
||||
self.model.supports_parallel_tool_calls(),
|
||||
self.model.supports_prompt_cache_key(),
|
||||
self.max_output_tokens(),
|
||||
None,
|
||||
reasoning_effort,
|
||||
false,
|
||||
);
|
||||
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 {
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -49,8 +49,27 @@ pub(crate) struct AudioStack {
|
|||
|
||||
impl AudioStack {
|
||||
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(
|
||||
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()));
|
||||
Self {
|
||||
|
|
|
|||
|
|
@ -40,7 +40,8 @@ use gpui::{
|
|||
use language::{CharClassifier, Language, LanguageRegistry, Rope};
|
||||
use parser::CodeBlockMetadata;
|
||||
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 sum_tree::TreeMap;
|
||||
|
|
@ -350,6 +351,7 @@ pub struct MarkdownOptions {
|
|||
pub parse_html: bool,
|
||||
pub render_mermaid_diagrams: bool,
|
||||
pub parse_heading_slugs: bool,
|
||||
pub render_metadata_blocks: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
|
|
@ -847,6 +849,7 @@ impl Markdown {
|
|||
let should_parse_html = self.options.parse_html;
|
||||
let should_render_mermaid_diagrams = self.options.render_mermaid_diagrams;
|
||||
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 fallback = self.fallback_code_block_language.clone();
|
||||
|
||||
|
|
@ -860,6 +863,7 @@ impl Markdown {
|
|||
languages_by_path: TreeMap::default(),
|
||||
root_block_starts: Arc::default(),
|
||||
html_blocks: BTreeMap::default(),
|
||||
metadata_blocks: BTreeMap::default(),
|
||||
mermaid_diagrams: BTreeMap::default(),
|
||||
heading_slugs: HashMap::default(),
|
||||
footnote_definitions: HashMap::default(),
|
||||
|
|
@ -868,13 +872,18 @@ impl Markdown {
|
|||
);
|
||||
}
|
||||
|
||||
let parsed =
|
||||
parse_markdown_with_options(&source, should_parse_html, should_parse_heading_slugs);
|
||||
let parsed = parse_markdown_with_options(
|
||||
&source,
|
||||
should_parse_html,
|
||||
should_parse_heading_slugs,
|
||||
should_parse_metadata_blocks,
|
||||
);
|
||||
let events = parsed.events;
|
||||
let language_names = parsed.language_names;
|
||||
let paths = parsed.language_paths;
|
||||
let root_block_starts = parsed.root_block_starts;
|
||||
let html_blocks = parsed.html_blocks;
|
||||
let metadata_blocks = parsed.metadata_blocks;
|
||||
let heading_slugs = parsed.heading_slugs;
|
||||
let footnote_definitions = parsed.footnote_definitions;
|
||||
let mermaid_diagrams = if should_render_mermaid_diagrams {
|
||||
|
|
@ -942,6 +951,7 @@ impl Markdown {
|
|||
languages_by_path,
|
||||
root_block_starts: Arc::from(root_block_starts),
|
||||
html_blocks,
|
||||
metadata_blocks,
|
||||
mermaid_diagrams,
|
||||
heading_slugs,
|
||||
footnote_definitions,
|
||||
|
|
@ -1070,6 +1080,7 @@ pub struct ParsedMarkdown {
|
|||
pub languages_by_path: TreeMap<Arc<str>, Arc<Language>>,
|
||||
pub root_block_starts: Arc<[usize]>,
|
||||
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 heading_slugs: HashMap<SharedString, usize>,
|
||||
pub footnote_definitions: HashMap<SharedString, usize>,
|
||||
|
|
@ -1398,6 +1409,114 @@ impl MarkdownElement {
|
|||
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(
|
||||
&self,
|
||||
builder: &mut MarkdownElementBuilder,
|
||||
|
|
@ -1809,6 +1928,7 @@ impl Element for MarkdownElement {
|
|||
let mut current_img_block_range: Option<Range<usize>> = None;
|
||||
let mut handled_html_block = false;
|
||||
let mut rendered_mermaid_block = false;
|
||||
let mut rendered_metadata_block = false;
|
||||
for (index, (range, event)) in parsed_markdown.events.iter().enumerate() {
|
||||
// Skip alt text for images that rendered
|
||||
if let Some(current_img_block_range) = ¤t_img_block_range
|
||||
|
|
@ -1832,6 +1952,13 @@ impl Element for MarkdownElement {
|
|||
continue;
|
||||
}
|
||||
|
||||
if rendered_metadata_block {
|
||||
if matches!(event, MarkdownEvent::End(MarkdownTagEnd::MetadataBlock(_))) {
|
||||
rendered_metadata_block = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
match event {
|
||||
MarkdownEvent::RootStart => {
|
||||
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);
|
||||
}
|
||||
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) => {
|
||||
builder.table.start(alignments.clone());
|
||||
|
||||
|
|
@ -2359,6 +2499,7 @@ impl Element for MarkdownElement {
|
|||
builder.pop_div();
|
||||
builder.pop_div();
|
||||
}
|
||||
MarkdownTagEnd::MetadataBlock(_) => {}
|
||||
_ => log::debug!("unsupported markdown tag end: {:?}", tag),
|
||||
},
|
||||
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 {
|
||||
div_stack: Vec<AnyDiv>,
|
||||
rendered_lines: Vec<RenderedLine>,
|
||||
|
|
@ -3586,6 +3732,34 @@ mod tests {
|
|||
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(
|
||||
markdown: &str,
|
||||
callback: impl Fn(&str, &App) -> Option<SharedString> + 'static,
|
||||
|
|
@ -3873,7 +4047,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_table_checkbox_detection() {
|
||||
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 cell_texts: Vec<String> = Vec::new();
|
||||
|
|
@ -3915,7 +4089,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_table_checkbox_marker_source_range() {
|
||||
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 pending_text = String::new();
|
||||
|
|
@ -4192,7 +4366,7 @@ mod tests {
|
|||
}
|
||||
|
||||
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
|
||||
.events
|
||||
.iter()
|
||||
|
|
|
|||
|
|
@ -686,7 +686,8 @@ mod tests {
|
|||
#[test]
|
||||
fn test_extract_mermaid_diagrams_parses_scale() {
|
||||
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);
|
||||
|
||||
assert_eq!(diagrams.len(), 1);
|
||||
|
|
@ -702,7 +703,8 @@ mod tests {
|
|||
"```mermaid\nblock-beta\n```\n\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);
|
||||
assert_eq!(
|
||||
diagrams.len(),
|
||||
|
|
|
|||
|
|
@ -37,10 +37,23 @@ pub(crate) struct ParsedMarkdownData {
|
|||
pub language_paths: HashSet<Arc<str>>,
|
||||
pub root_block_starts: Vec<usize>,
|
||||
pub html_blocks: BTreeMap<usize, html::html_parser::ParsedHtmlBlock>,
|
||||
pub metadata_blocks: BTreeMap<usize, ParsedMetadataBlock>,
|
||||
pub heading_slugs: 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 {
|
||||
fn push_event(&mut self, range: Range<usize>, event: MarkdownEvent) {
|
||||
match &event {
|
||||
|
|
@ -149,27 +162,83 @@ fn build_heading_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(
|
||||
text: &str,
|
||||
parse_html: bool,
|
||||
parse_heading_slugs: bool,
|
||||
parse_metadata_blocks: bool,
|
||||
) -> ParsedMarkdownData {
|
||||
let mut state = ParseState::default();
|
||||
let mut language_names = HashSet::default();
|
||||
let mut language_paths = HashSet::default();
|
||||
let mut html_blocks = BTreeMap::default();
|
||||
let mut metadata_blocks = BTreeMap::default();
|
||||
let mut within_link = false;
|
||||
let mut within_code_block = 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()
|
||||
.peekable();
|
||||
while let Some((pulldown_event, range)) = parser.next() {
|
||||
if within_metadata {
|
||||
if let pulldown_cmark::Event::End(pulldown_cmark::TagEnd::MetadataBlock { .. }) =
|
||||
if within_metadata && !parse_metadata_blocks {
|
||||
if let pulldown_cmark::Event::End(pulldown_cmark::TagEnd::MetadataBlock(_)) =
|
||||
pulldown_event
|
||||
{
|
||||
within_metadata = false;
|
||||
current_metadata_block_start = None;
|
||||
metadata_block_content_range = None;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
|
@ -216,9 +285,14 @@ pub(crate) fn parse_markdown_with_options(
|
|||
id: SharedString::from(id.into_string()),
|
||||
}
|
||||
}
|
||||
pulldown_cmark::Tag::MetadataBlock(_kind) => {
|
||||
pulldown_cmark::Tag::MetadataBlock(kind) => {
|
||||
within_metadata = true;
|
||||
continue;
|
||||
current_metadata_block_start = Some(range.start);
|
||||
metadata_block_content_range = None;
|
||||
if !parse_metadata_blocks {
|
||||
continue;
|
||||
}
|
||||
MarkdownTag::MetadataBlock(kind)
|
||||
}
|
||||
pulldown_cmark::Tag::CodeBlock(pulldown_cmark::CodeBlockKind::Indented) => {
|
||||
within_code_block = true;
|
||||
|
|
@ -347,6 +421,25 @@ pub(crate) fn parse_markdown_with_options(
|
|||
within_link = false;
|
||||
} else if let pulldown_cmark::TagEnd::CodeBlock = tag {
|
||||
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));
|
||||
}
|
||||
|
|
@ -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 {
|
||||
let (range, event) = event_for(text, range, &parsed);
|
||||
state.push_event(range, event);
|
||||
|
|
@ -541,6 +646,7 @@ pub(crate) fn parse_markdown_with_options(
|
|||
language_paths,
|
||||
root_block_starts: state.root_block_starts,
|
||||
html_blocks,
|
||||
metadata_blocks,
|
||||
heading_slugs,
|
||||
footnote_definitions,
|
||||
}
|
||||
|
|
@ -798,8 +904,8 @@ mod tests {
|
|||
use super::MarkdownTag::*;
|
||||
use super::*;
|
||||
|
||||
const UNWANTED_OPTIONS: Options = Options::ENABLE_YAML_STYLE_METADATA_BLOCKS
|
||||
.union(Options::ENABLE_MATH)
|
||||
const CONDITIONAL_OPTIONS: Options = Options::ENABLE_YAML_STYLE_METADATA_BLOCKS;
|
||||
const UNWANTED_OPTIONS: Options = Options::ENABLE_MATH
|
||||
.union(Options::ENABLE_DEFINITION_LIST)
|
||||
.union(Options::ENABLE_WIKILINKS);
|
||||
|
||||
|
|
@ -807,21 +913,174 @@ mod tests {
|
|||
fn all_options_considered() {
|
||||
// The purpose of this is to fail when new options are added to pulldown_cmark, so that they
|
||||
// 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]
|
||||
fn wanted_and_unwanted_options_disjoint() {
|
||||
assert_eq!(
|
||||
PARSE_OPTIONS.intersection(UNWANTED_OPTIONS),
|
||||
PARSE_OPTIONS
|
||||
.union(CONDITIONAL_OPTIONS)
|
||||
.intersection(UNWANTED_OPTIONS),
|
||||
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]
|
||||
fn test_html_comments() {
|
||||
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 {
|
||||
events: vec
|
||||
.events,
|
||||
vec![
|
||||
|
|
@ -925,6 +1186,7 @@ mod tests {
|
|||
"-- --- ... \"double quoted\" 'single quoted' ----------",
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
ParsedMarkdownData {
|
||||
events: vec![
|
||||
|
|
@ -957,7 +1219,12 @@ mod tests {
|
|||
#[test]
|
||||
fn test_code_block_metadata() {
|
||||
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 {
|
||||
events: vec![
|
||||
(0..37, RootStart),
|
||||
|
|
@ -986,7 +1253,7 @@ mod tests {
|
|||
}
|
||||
);
|
||||
assert_eq!(
|
||||
parse_markdown_with_options(" fn main() {}", false, false),
|
||||
parse_markdown_with_options(" fn main() {}", false, false, false),
|
||||
ParsedMarkdownData {
|
||||
events: vec![
|
||||
(4..16, RootStart),
|
||||
|
|
@ -1012,7 +1279,7 @@ mod tests {
|
|||
}
|
||||
|
||||
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_count = 0;
|
||||
let mut saw_text_inside_code_block = false;
|
||||
|
|
@ -1064,9 +1331,54 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_metadata_blocks_do_not_affect_root_blocks() {
|
||||
fn test_metadata_blocks_are_root_blocks() {
|
||||
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 {
|
||||
events: vec![
|
||||
(27..36, RootStart),
|
||||
|
|
@ -1088,7 +1400,7 @@ mod tests {
|
|||
|------|---------|
|
||||
| [x] | Fix bug |
|
||||
| [ ] | 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 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.",
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
assert_eq!(
|
||||
parsed.events,
|
||||
|
|
@ -1194,6 +1507,7 @@ mod tests {
|
|||
"Text[^a] and[^b].\n\n[^a]: First.\n\n[^b]: Second.",
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
assert_eq!(parsed.footnote_definitions.len(), 2);
|
||||
assert!(parsed.footnote_definitions.contains_key("a"));
|
||||
|
|
@ -1211,6 +1525,7 @@ mod tests {
|
|||
"https:/\\/example.com is equivalent to https://example.com!",
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.events,
|
||||
vec![
|
||||
|
|
@ -1253,6 +1568,7 @@ mod tests {
|
|||
"Visit https://example.com/cat\\/é‍☕ for coffee!",
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.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",
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
assert_eq!(parsed.heading_slugs.len(), 5);
|
||||
assert!(parsed.heading_slugs.contains_key("hello-world"));
|
||||
|
|
@ -1301,6 +1618,7 @@ mod tests {
|
|||
"# Duplicate\n\nText\n\n## Duplicate\n\nMore text",
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
let first = parsed.heading_slugs.get("duplicate").copied();
|
||||
let second = parsed.heading_slugs.get("duplicate-1").copied();
|
||||
|
|
@ -1311,7 +1629,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
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!(parsed.heading_slugs.contains_key("foo"));
|
||||
assert!(parsed.heading_slugs.contains_key("foo-1"));
|
||||
|
|
@ -1323,7 +1641,7 @@ mod tests {
|
|||
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 parsed = parse_markdown_with_options(markdown, false, false);
|
||||
let parsed = parse_markdown_with_options(markdown, false, false, false);
|
||||
|
||||
let block_quote_kinds: Vec<_> = parsed
|
||||
.events
|
||||
|
|
|
|||
|
|
@ -223,6 +223,7 @@ impl MarkdownPreviewView {
|
|||
parse_html: true,
|
||||
render_mermaid_diagrams: true,
|
||||
parse_heading_slugs: true,
|
||||
render_metadata_blocks: true,
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
|
|
|
|||
|
|
@ -20,11 +20,11 @@ use futures_lite::future::yield_now;
|
|||
use gpui::{App, Context, Entity, EventEmitter};
|
||||
use itertools::Itertools;
|
||||
use language::{
|
||||
AutoindentMode, Buffer, BufferChunks, BufferRow, BufferSnapshot, Capability, CharClassifier,
|
||||
CharKind, CharScopeContext, Chunk, CursorShape, DiagnosticEntryRef, File, IndentGuideSettings,
|
||||
IndentSize, Language, LanguageAwareStyling, LanguageScope, OffsetRangeExt, OffsetUtf16,
|
||||
Outline, OutlineItem, Point, PointUtf16, Selection, TextDimension, TextObject, ToOffset as _,
|
||||
ToPoint as _, TransactionId, TreeSitterOptions, Unclipped,
|
||||
AutoindentMode, Buffer, BufferChunks, BufferEditSource, BufferRow, BufferSnapshot, Capability,
|
||||
CharClassifier, CharKind, CharScopeContext, Chunk, CursorShape, DiagnosticEntryRef, File,
|
||||
IndentGuideSettings, IndentSize, Language, LanguageAwareStyling, LanguageScope, OffsetRangeExt,
|
||||
OffsetUtf16, Outline, OutlineItem, Point, PointUtf16, Selection, TextDimension, TextObject,
|
||||
ToOffset as _, ToPoint as _, TransactionId, TreeSitterOptions, Unclipped,
|
||||
language_settings::{AllLanguageSettings, LanguageSettings},
|
||||
};
|
||||
|
||||
|
|
@ -110,7 +110,7 @@ pub enum Event {
|
|||
DiffHunksToggled,
|
||||
Edited {
|
||||
edited_buffer: Option<Entity<Buffer>>,
|
||||
is_local: bool,
|
||||
source: BufferEditSource,
|
||||
},
|
||||
TransactionUndone {
|
||||
transaction_id: TransactionId,
|
||||
|
|
@ -1828,7 +1828,7 @@ impl MultiBuffer {
|
|||
}
|
||||
cx.emit(Event::Edited {
|
||||
edited_buffer: None,
|
||||
is_local: true,
|
||||
source: BufferEditSource::User,
|
||||
});
|
||||
cx.emit(Event::BuffersRemoved { removed_buffer_ids });
|
||||
cx.notify();
|
||||
|
|
@ -1952,9 +1952,9 @@ impl MultiBuffer {
|
|||
use language::BufferEvent;
|
||||
let buffer_id = buffer.read(cx).remote_id();
|
||||
cx.emit(match event {
|
||||
&BufferEvent::Edited { is_local } => Event::Edited {
|
||||
&BufferEvent::Edited { source } => Event::Edited {
|
||||
edited_buffer: Some(buffer),
|
||||
is_local,
|
||||
source,
|
||||
},
|
||||
BufferEvent::DirtyChanged => Event::DirtyChanged,
|
||||
BufferEvent::Saved => Event::Saved,
|
||||
|
|
@ -2044,7 +2044,7 @@ impl MultiBuffer {
|
|||
}
|
||||
cx.emit(Event::Edited {
|
||||
edited_buffer: None,
|
||||
is_local: true,
|
||||
source: BufferEditSource::User,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -2090,7 +2090,7 @@ impl MultiBuffer {
|
|||
}
|
||||
cx.emit(Event::Edited {
|
||||
edited_buffer: None,
|
||||
is_local: true,
|
||||
source: BufferEditSource::User,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -2313,7 +2313,7 @@ impl MultiBuffer {
|
|||
cx.emit(Event::DiffHunksToggled);
|
||||
cx.emit(Event::Edited {
|
||||
edited_buffer: None,
|
||||
is_local: true,
|
||||
source: BufferEditSource::User,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -2449,7 +2449,7 @@ impl MultiBuffer {
|
|||
cx.emit(Event::DiffHunksToggled);
|
||||
cx.emit(Event::Edited {
|
||||
edited_buffer: None,
|
||||
is_local: true,
|
||||
source: BufferEditSource::User,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -3102,7 +3102,7 @@ impl MultiBuffer {
|
|||
cx.emit(Event::DiffHunksToggled);
|
||||
cx.emit(Event::Edited {
|
||||
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