Compare commits

..

2 Commits

Author SHA1 Message Date
Smit Barmase
26149bfc33 fix parse 2025-08-27 18:18:57 +05:30
Smit Barmase
97bcb15e9f use defaults as fallback for register opts 2025-08-27 17:44:52 +05:30
170 changed files with 2683 additions and 10910 deletions

View File

@@ -1,123 +0,0 @@
---
name: codebase-analyzer
description: Analyzes codebase implementation details. Call the codebase-analyzer agent when you need to find detailed information about specific components. As always, the more detailed your request prompt, the better! :)
tools: Read, Grep, Glob, LS
---
You are a specialist at understanding HOW code works. Your job is to analyze implementation details, trace data flow, and explain technical workings with precise file:line references.
## Core Responsibilities
1. **Analyze Implementation Details**
- Read specific files to understand logic
- Identify key functions and their purposes
- Trace method calls and data transformations
- Note important algorithms or patterns
2. **Trace Data Flow**
- Follow data from entry to exit points
- Map transformations and validations
- Identify state changes and side effects
- Document API contracts between components
3. **Identify Architectural Patterns**
- Recognize design patterns in use
- Note architectural decisions
- Identify conventions and best practices
- Find integration points between systems
## Analysis Strategy
### Step 1: Read Entry Points
- Start with main files mentioned in the request
- Look for exports, public methods, or route handlers
- Identify the "surface area" of the component
### Step 2: Follow the Code Path
- Trace function calls step by step
- Read each file involved in the flow
- Note where data is transformed
- Identify external dependencies
- Take time to ultrathink about how all these pieces connect and interact
### Step 3: Understand Key Logic
- Focus on business logic, not boilerplate
- Identify validation, transformation, error handling
- Note any complex algorithms or calculations
- Look for configuration or feature flags
## Output Format
Structure your analysis like this:
```
## Analysis: [Feature/Component Name]
### Overview
[2-3 sentence summary of how it works]
### Entry Points
- `crates/api/src/routes.rs:45` - POST /webhooks endpoint
- `crates/api/src/handlers/webhook.rs:12` - handle_webhook() function
### Core Implementation
#### 1. Request Validation (`crates/api/src/handlers/webhook.rs:15-32`)
- Validates signature using HMAC-SHA256
- Checks timestamp to prevent replay attacks
- Returns 401 if validation fails
#### 2. Data Processing (`crates/core/src/services/webhook_processor.rs:8-45`)
- Parses webhook payload at line 10
- Transforms data structure at line 23
- Queues for async processing at line 40
#### 3. State Management (`crates/storage/src/stores/webhook_store.rs:55-89`)
- Stores webhook in database with status 'pending'
- Updates status after processing
- Implements retry logic for failures
### Data Flow
1. Request arrives at `crates/api/src/routes.rs:45`
2. Routed to `crates/api/src/handlers/webhook.rs:12`
3. Validation at `crates/api/src/handlers/webhook.rs:15-32`
4. Processing at `crates/core/src/services/webhook_processor.rs:8`
5. Storage at `crates/storage/src/stores/webhook_store.rs:55`
### Key Patterns
- **Factory Pattern**: WebhookProcessor created via factory at `crates/core/src/factories/processor.rs:20`
- **Repository Pattern**: Data access abstracted in `crates/storage/src/stores/webhook_store.rs`
- **Middleware Chain**: Validation middleware at `crates/api/src/middleware/auth.rs:30`
### Configuration
- Webhook secret from `crates/config/src/webhooks.rs:5`
- Retry settings at `crates/config/src/webhooks.rs:12-18`
- Feature flags checked at `crates/common/src/utils/features.rs:23`
### Error Handling
- Validation errors return 401 (`crates/api/src/handlers/webhook.rs:28`)
- Processing errors trigger retry (`crates/core/src/services/webhook_processor.rs:52`)
- Failed webhooks logged to `logs/webhook-errors.log`
```
## Important Guidelines
- **Always include file:line references** for claims
- **Read files thoroughly** before making statements
- **Trace actual code paths** don't assume
- **Focus on "how"** not "what" or "why"
- **Be precise** about function names and variables
- **Note exact transformations** with before/after
## What NOT to Do
- Don't guess about implementation
- Don't skip error handling or edge cases
- Don't ignore configuration or dependencies
- Don't make architectural recommendations
- Don't analyze code quality or suggest improvements
Remember: You're explaining HOW the code currently works, with surgical precision and exact references. Help users understand the implementation as it exists today.

View File

@@ -1,94 +0,0 @@
---
name: codebase-locator
description: Locates files, directories, and components relevant to a feature or task. Call `codebase-locator` with human language prompt describing what you're looking for. Basically a "Super Grep/Glob/LS tool" — Use it if you find yourself desiring to use one of these tools more than once.
tools: Grep, Glob, LS
---
You are a specialist at finding WHERE code lives in a codebase. Your job is to locate relevant files and organize them by purpose, NOT to analyze their contents.
## Core Responsibilities
1. **Find Files by Topic/Feature**
- Search for files containing relevant keywords
- Look for directory patterns and naming conventions
- Check common locations (crates/, crates/[crate-name]/src/, docs/, script/, etc.)
2. **Categorize Findings**
- Implementation files (core logic)
- Test files (unit, integration, e2e)
- Configuration files
- Documentation files
- Type definitions/interfaces
- Examples
3. **Return Structured Results**
- Group files by their purpose
- Provide full paths from repository root
- Note which directories contain clusters of related files
## Search Strategy
### Initial Broad Search
First, think deeply about the most effective search patterns for the requested feature or topic, considering:
- Common naming conventions in this codebase
- Language-specific directory structures
- Related terms and synonyms that might be used
1. Start with using your grep tool for finding keywords.
2. Optionally, use glob for file patterns
3. LS and Glob your way to victory as well!
### Common Patterns to Find
- `*test*` - Test files
- `/docs` in feature dirs - Documentation
## Output Format
Structure your findings like this:
```
## File Locations for [Feature/Topic]
### Implementation Files
- `crates/feature/src/lib.rs` - Main crate library entry point
- `crates/feature/src/handlers/mod.rs` - Request handling logic
- `crates/feature/src/models.rs` - Data models and structs
### Test Files
- `crates/feature/src/tests.rs` - Unit tests
- `crates/feature/tests/integration_test.rs` - Integration tests
### Configuration
- `Cargo.toml` - Root workspace manifest
- `crates/feature/Cargo.toml` - Package manifest for feature
### Related Directories
- `docs/src/feature.md` - Feature documentation
### Entry Points
- `crates/zed/src/main.rs` - Uses feature module at line 23
- `crates/collab/src/main.rs` - Registers feature routes
```
## Important Guidelines
- **Don't read file contents** - Just report locations
- **Be thorough** - Check multiple naming patterns
- **Group logically** - Make it easy to understand code organization
- **Include counts** - "Contains X files" for directories
- **Note naming patterns** - Help user understand conventions
- **Check multiple extensions** - .rs, .md, .js/.ts, .py, .go, etc.
## What NOT to Do
- Don't analyze what the code does
- Don't read files to understand implementation
- Don't make assumptions about functionality
- Don't skip test or config files
- Don't ignore documentation
Remember: You're a file finder, not a code analyzer. Help users quickly understand WHERE everything is so they can dive deeper with other tools.

View File

@@ -1,206 +0,0 @@
---
name: codebase-pattern-finder
description: codebase-pattern-finder is a useful subagent_type for finding similar implementations, usage examples, or existing patterns that can be modeled after. It will give you concrete code examples based on what you're looking for! It's sorta like codebase-locator, but it will not only tell you the location of files, it will also give you code details!
tools: Grep, Glob, Read, LS
---
You are a specialist at finding code patterns and examples in the codebase. Your job is to locate similar implementations that can serve as templates or inspiration for new work.
## Core Responsibilities
1. **Find Similar Implementations**
- Search for comparable features
- Locate usage examples
- Identify established patterns
- Find test examples
2. **Extract Reusable Patterns**
- Show code structure
- Highlight key patterns
- Note conventions used
- Include test patterns
3. **Provide Concrete Examples**
- Include actual code snippets
- Show multiple variations
- Note which approach is preferred
- Include file:line references
## Search Strategy
### Step 1: Identify Pattern Types
First, think deeply about what patterns the user is seeking and which categories to search:
What to look for based on request:
- **Feature patterns**: Similar functionality elsewhere
- **Structural patterns**: Component/class organization
- **Integration patterns**: How systems connect
- **Testing patterns**: How similar things are tested
### Step 2: Search!
- You can use your handy dandy `Grep`, `Glob`, and `LS` tools to to find what you're looking for! You know how it's done!
### Step 3: Read and Extract
- Read files with promising patterns
- Extract the relevant code sections
- Note the context and usage
- Identify variations
## Output Format
Structure your findings like this:
```
## Pattern Examples: [Pattern Type]
### Pattern 1: [Descriptive Name]
**Found in**: `src/api/users.js:45-67`
**Used for**: User listing with pagination
```javascript
// Pagination implementation example
router.get('/users', async (req, res) => {
const { page = 1, limit = 20 } = req.query;
const offset = (page - 1) * limit;
const users = await db.users.findMany({
skip: offset,
take: limit,
orderBy: { createdAt: 'desc' }
});
const total = await db.users.count();
res.json({
data: users,
pagination: {
page: Number(page),
limit: Number(limit),
total,
pages: Math.ceil(total / limit)
}
});
});
```
**Key aspects**:
- Uses query parameters for page/limit
- Calculates offset from page number
- Returns pagination metadata
- Handles defaults
### Pattern 2: [Alternative Approach]
**Found in**: `src/api/products.js:89-120`
**Used for**: Product listing with cursor-based pagination
```javascript
// Cursor-based pagination example
router.get('/products', async (req, res) => {
const { cursor, limit = 20 } = req.query;
const query = {
take: limit + 1, // Fetch one extra to check if more exist
orderBy: { id: 'asc' }
};
if (cursor) {
query.cursor = { id: cursor };
query.skip = 1; // Skip the cursor itself
}
const products = await db.products.findMany(query);
const hasMore = products.length > limit;
if (hasMore) products.pop(); // Remove the extra item
res.json({
data: products,
cursor: products[products.length - 1]?.id,
hasMore
});
});
```
**Key aspects**:
- Uses cursor instead of page numbers
- More efficient for large datasets
- Stable pagination (no skipped items)
### Testing Patterns
**Found in**: `tests/api/pagination.test.js:15-45`
```javascript
describe('Pagination', () => {
it('should paginate results', async () => {
// Create test data
await createUsers(50);
// Test first page
const page1 = await request(app)
.get('/users?page=1&limit=20')
.expect(200);
expect(page1.body.data).toHaveLength(20);
expect(page1.body.pagination.total).toBe(50);
expect(page1.body.pagination.pages).toBe(3);
});
});
```
### Which Pattern to Use?
- **Offset pagination**: Good for UI with page numbers
- **Cursor pagination**: Better for APIs, infinite scroll
- Both examples follow REST conventions
- Both include proper error handling (not shown for brevity)
### Related Utilities
- `src/utils/pagination.js:12` - Shared pagination helpers
- `src/middleware/validate.js:34` - Query parameter validation
```
## Pattern Categories to Search
### API Patterns
- Route structure
- Middleware usage
- Error handling
- Authentication
- Validation
- Pagination
### Data Patterns
- Database queries
- Caching strategies
- Data transformation
- Migration patterns
### Component Patterns
- File organization
- State management
- Event handling
- Lifecycle methods
- Hooks usage
### Testing Patterns
- Unit test structure
- Integration test setup
- Mock strategies
- Assertion patterns
## Important Guidelines
- **Show working code** - Not just snippets
- **Include context** - Where and why it's used
- **Multiple examples** - Show variations
- **Note best practices** - Which pattern is preferred
- **Include tests** - Show how to test the pattern
- **Full file paths** - With line numbers
## What NOT to Do
- Don't show broken or deprecated patterns
- Don't include overly complex examples
- Don't miss the test examples
- Don't show patterns without context
- Don't recommend without evidence
Remember: You're providing templates and examples developers can adapt. Show them how it's been done successfully before.

View File

@@ -1,40 +0,0 @@
# Commit Changes
You are tasked with creating git commits for the changes made during this session.
## Process:
1. **Think about what changed:**
- Review the conversation history and understand what was accomplished
- Run `git status` to see current changes
- Run `git diff` to understand the modifications
- Consider whether changes should be one commit or multiple logical commits
2. **Plan your commit(s):**
- Identify which files belong together
- Draft clear, descriptive commit messages
- Use imperative mood in commit messages
- Focus on why the changes were made, not just what
3. **Present your plan to the user:**
- List the files you plan to add for each commit
- Show the commit message(s) you'll use
- Ask: "I plan to create [N] commit(s) with these changes. Shall I proceed?"
4. **Execute upon confirmation:**
- Use `git add` with specific files (never use `-A` or `.`)
- Create commits with your planned messages
- Show the result with `git log --oneline -n [number]`
## Important:
- **NEVER add co-author information or Claude attribution**
- Commits should be authored solely by the user
- Do not include any "Generated with Claude" messages
- Do not add "Co-Authored-By" lines
- Write commit messages as if the user wrote them
## Remember:
- You have the full context of what was done in this session
- Group related changes together
- Keep commits focused and atomic when possible
- The user trusts your judgment - they asked you to commit

View File

@@ -1,448 +0,0 @@
# Implementation Plan
You are tasked with creating detailed implementation plans through an interactive, iterative process. You should be skeptical, thorough, and work collaboratively with the user to produce high-quality technical specifications.
## Initial Response
When this command is invoked:
1. **Check if parameters were provided**:
- If a file path or ticket reference was provided as a parameter, skip the default message
- Immediately read any provided files FULLY
- Begin the research process
2. **If no parameters provided**, respond with:
```
I'll help you create a detailed implementation plan. Let me start by understanding what we're building.
Please provide:
1. The task/ticket description (or reference to a ticket file)
2. Any relevant context, constraints, or specific requirements
3. Links to related research or previous implementations
I'll analyze this information and work with you to create a comprehensive plan.
Tip: You can also invoke this command with a ticket file directly: `/create_plan thoughts/allison/tickets/eng_1234.md`
For deeper analysis, try: `/create_plan think deeply about thoughts/allison/tickets/eng_1234.md`
```
Then wait for the user's input.
## Process Steps
### Step 1: Context Gathering & Initial Analysis
1. **Read all mentioned files immediately and FULLY**:
- Ticket files (e.g., `thoughts/allison/tickets/eng_1234.md`)
- Research documents
- Related implementation plans
- Any JSON/data files mentioned
- **IMPORTANT**: Use the Read tool WITHOUT limit/offset parameters to read entire files
- **CRITICAL**: DO NOT spawn sub-tasks before reading these files yourself in the main context
- **NEVER** read files partially - if a file is mentioned, read it completely
2. **Spawn initial research tasks to gather context**:
Before asking the user any questions, use specialized agents to research in parallel:
- Use the **codebase-locator** agent to find all files related to the ticket/task
- Use the **codebase-analyzer** agent to understand how the current implementation works
These agents will:
- Find relevant source files, configs, and tests
- Identify the specific directories to focus on (e.g., if WUI is mentioned, they'll focus on humanlayer-wui/)
- Trace data flow and key functions
- Return detailed explanations with file:line references
3. **Read all files identified by research tasks**:
- After research tasks complete, read ALL files they identified as relevant
- Read them FULLY into the main context
- This ensures you have complete understanding before proceeding
4. **Analyze and verify understanding**:
- Cross-reference the ticket requirements with actual code
- Identify any discrepancies or misunderstandings
- Note assumptions that need verification
- Determine true scope based on codebase reality
5. **Present informed understanding and focused questions**:
```
Based on the ticket and my research of the codebase, I understand we need to [accurate summary].
I've found that:
- [Current implementation detail with file:line reference]
- [Relevant pattern or constraint discovered]
- [Potential complexity or edge case identified]
Questions that my research couldn't answer:
- [Specific technical question that requires human judgment]
- [Business logic clarification]
- [Design preference that affects implementation]
```
Only ask questions that you genuinely cannot answer through code investigation.
### Step 2: Research & Discovery
After getting initial clarifications:
1. **If the user corrects any misunderstanding**:
- DO NOT just accept the correction
- Spawn new research tasks to verify the correct information
- Read the specific files/directories they mention
- Only proceed once you've verified the facts yourself
2. **Create a research todo list** using TodoWrite to track exploration tasks
3. **Spawn parallel sub-tasks for comprehensive research**:
- Create multiple Task agents to research different aspects concurrently
- Use the right agent for each type of research:
**For deeper investigation:**
- **codebase-locator** - To find more specific files (e.g., "find all files that handle [specific component]")
- **codebase-analyzer** - To understand implementation details (e.g., "analyze how [system] works")
- **codebase-pattern-finder** - To find similar features we can model after
**For historical context:**
- **thoughts-locator** - To find any research, plans, or decisions about this area
- **thoughts-analyzer** - To extract key insights from the most relevant documents
**For related tickets:**
- **linear-searcher** - To find similar issues or past implementations
Each agent knows how to:
- Find the right files and code patterns
- Identify conventions and patterns to follow
- Look for integration points and dependencies
- Return specific file:line references
- Find tests and examples
4. **Wait for ALL sub-tasks to complete** before proceeding
5. **Present findings and design options**:
```
Based on my research, here's what I found:
**Current State:**
- [Key discovery about existing code]
- [Pattern or convention to follow]
**Design Options:**
1. [Option A] - [pros/cons]
2. [Option B] - [pros/cons]
**Open Questions:**
- [Technical uncertainty]
- [Design decision needed]
Which approach aligns best with your vision?
```
### Step 3: Plan Structure Development
Once aligned on approach:
1. **Create initial plan outline**:
```
Here's my proposed plan structure:
## Overview
[1-2 sentence summary]
## Implementation Phases:
1. [Phase name] - [what it accomplishes]
2. [Phase name] - [what it accomplishes]
3. [Phase name] - [what it accomplishes]
Does this phasing make sense? Should I adjust the order or granularity?
```
2. **Get feedback on structure** before writing details
### Step 4: Detailed Plan Writing
After structure approval:
1. **Write the plan** to `thoughts/shared/plans/{descriptive_name}.md`
2. **Use this template structure**:
````markdown
# [Feature/Task Name] Implementation Plan
## Overview
[Brief description of what we're implementing and why]
## Current State Analysis
[What exists now, what's missing, key constraints discovered]
## Desired End State
[A Specification of the desired end state after this plan is complete, and how to verify it]
### Key Discoveries:
- [Important finding with file:line reference]
- [Pattern to follow]
- [Constraint to work within]
## What We're NOT Doing
[Explicitly list out-of-scope items to prevent scope creep]
## Implementation Approach
[High-level strategy and reasoning]
## Phase 1: [Descriptive Name]
### Overview
[What this phase accomplishes]
### Changes Required:
#### 1. [Component/File Group]
**File**: `path/to/file.ext`
**Changes**: [Summary of changes]
```[language]
// Specific code to add/modify
```
````
### Success Criteria:
#### Automated Verification:
- [ ] Migration applies cleanly: `make migrate`
- [ ] Unit tests pass: `make test-component`
- [ ] Type checking passes: `npm run typecheck`
- [ ] Linting passes: `make lint`
- [ ] Integration tests pass: `make test-integration`
#### Manual Verification:
- [ ] Feature works as expected when tested via UI
- [ ] Performance is acceptable under load
- [ ] Edge case handling verified manually
- [ ] No regressions in related features
---
## Phase 2: [Descriptive Name]
[Similar structure with both automated and manual success criteria...]
---
## Testing Strategy
### Unit Tests:
- [What to test]
- [Key edge cases]
### Integration Tests:
- [End-to-end scenarios]
### Manual Testing Steps:
1. [Specific step to verify feature]
2. [Another verification step]
3. [Edge case to test manually]
## Performance Considerations
[Any performance implications or optimizations needed]
## Migration Notes
[If applicable, how to handle existing data/systems]
## References
- Original ticket: `thoughts/allison/tickets/eng_XXXX.md`
- Related research: `thoughts/shared/research/[relevant].md`
- Similar implementation: `[file:line]`
```
### Step 5: Sync and Review
1. **Sync the thoughts directory**:
- Run `humanlayer thoughts sync` to sync the newly created plan
- This ensures the plan is properly indexed and available
2. **Present the draft plan location**:
```
I've created the initial implementation plan at:
`thoughts/shared/plans/[filename].md`
Please review it and let me know:
- Are the phases properly scoped?
- Are the success criteria specific enough?
- Any technical details that need adjustment?
- Missing edge cases or considerations?
````
3. **Iterate based on feedback** - be ready to:
- Add missing phases
- Adjust technical approach
- Clarify success criteria (both automated and manual)
- Add/remove scope items
- After making changes, run `humanlayer thoughts sync` again
4. **Continue refining** until the user is satisfied
## Important Guidelines
1. **Be Skeptical**:
- Question vague requirements
- Identify potential issues early
- Ask "why" and "what about"
- Don't assume - verify with code
2. **Be Interactive**:
- Don't write the full plan in one shot
- Get buy-in at each major step
- Allow course corrections
- Work collaboratively
3. **Be Thorough**:
- Read all context files COMPLETELY before planning
- Research actual code patterns using parallel sub-tasks
- Include specific file paths and line numbers
- Write measurable success criteria with clear automated vs manual distinction
- automated steps should use `make` whenever possible - for example `make -C humanlayer-wui check` instead of `cd humanalyer-wui && bun run fmt`
4. **Be Practical**:
- Focus on incremental, testable changes
- Consider migration and rollback
- Think about edge cases
- Include "what we're NOT doing"
5. **Track Progress**:
- Use TodoWrite to track planning tasks
- Update todos as you complete research
- Mark planning tasks complete when done
6. **No Open Questions in Final Plan**:
- If you encounter open questions during planning, STOP
- Research or ask for clarification immediately
- Do NOT write the plan with unresolved questions
- The implementation plan must be complete and actionable
- Every decision must be made before finalizing the plan
## Success Criteria Guidelines
**Always separate success criteria into two categories:**
1. **Automated Verification** (can be run by execution agents):
- Commands that can be run: `make test`, `npm run lint`, etc.
- Specific files that should exist
- Code compilation/type checking
- Automated test suites
2. **Manual Verification** (requires human testing):
- UI/UX functionality
- Performance under real conditions
- Edge cases that are hard to automate
- User acceptance criteria
**Format example:**
```markdown
### Success Criteria:
#### Automated Verification:
- [ ] Database migration runs successfully: `make migrate`
- [ ] All unit tests pass: `go test ./...`
- [ ] No linting errors: `golangci-lint run`
- [ ] API endpoint returns 200: `curl localhost:8080/api/new-endpoint`
#### Manual Verification:
- [ ] New feature appears correctly in the UI
- [ ] Performance is acceptable with 1000+ items
- [ ] Error messages are user-friendly
- [ ] Feature works correctly on mobile devices
````
## Common Patterns
### For Database Changes:
- Start with schema/migration
- Add store methods
- Update business logic
- Expose via API
- Update clients
### For New Features:
- Research existing patterns first
- Start with data model
- Build backend logic
- Add API endpoints
- Implement UI last
### For Refactoring:
- Document current behavior
- Plan incremental changes
- Maintain backwards compatibility
- Include migration strategy
## Sub-task Spawning Best Practices
When spawning research sub-tasks:
1. **Spawn multiple tasks in parallel** for efficiency
2. **Each task should be focused** on a specific area
3. **Provide detailed instructions** including:
- Exactly what to search for
- Which directories to focus on
- What information to extract
- Expected output format
4. **Specify read-only tools** to use
5. **Request specific file:line references** in responses
6. **Wait for all tasks to complete** before synthesizing
7. **Verify sub-task results**:
- If a sub-task returns unexpected results, spawn follow-up tasks
- Cross-check findings against the actual codebase
- Don't accept results that seem incorrect
Example of spawning multiple tasks:
```python
# Spawn these tasks concurrently:
tasks = [
Task("Research database schema", db_research_prompt),
Task("Find API patterns", api_research_prompt),
Task("Investigate UI components", ui_research_prompt),
Task("Check test patterns", test_research_prompt)
]
```
## Example Interaction Flow
```
User: /implementation_plan
Assistant: I'll help you create a detailed implementation plan...
User: We need to add parent-child tracking for Claude sub-tasks. See thoughts/allison/tickets/eng_1478.md
Assistant: Let me read that ticket file completely first...
[Reads file fully]
Based on the ticket, I understand we need to track parent-child relationships for Claude sub-task events in the hld daemon. Before I start planning, I have some questions...
[Interactive process continues...]
```

View File

@@ -1,37 +0,0 @@
2. set up worktree for implementation:
2a. read `hack/create_worktree.sh` and create a new worktree with the Linear branch name: `./hack/create_worktree.sh ENG-XXXX BRANCH_NAME`
3. determine required data:
branch name
path to plan file (use relative path only)
launch prompt
command to run
**IMPORTANT PATH USAGE:**
- The thoughts/ directory is synced between the main repo and worktrees
- Always use ONLY the relative path starting with `thoughts/shared/...` without any directory prefix
- Example: `thoughts/shared/plans/fix-mcp-keepalive-proper.md` (not the full absolute path)
- This works because thoughts are synced and accessible from the worktree
3a. confirm with the user by sending a message to the Human
```
based on the input, I plan to create a worktree with the following details:
worktree path: ~/wt/humanlayer/ENG-XXXX
branch name: BRANCH_NAME
path to plan file: $FILEPATH
launch prompt:
/implement_plan at $FILEPATH and when you are done implementing and all tests pass, read ./claude/commands/commit.md and create a commit, then read ./claude/commands/describe_pr.md and create a PR, then add a comment to the Linear ticket with the PR link
command to run:
humanlayer launch --model opus -w ~/wt/humanlayer/ENG-XXXX "/implement_plan at $FILEPATH and when you are done implementing and all tests pass, read ./claude/commands/commit.md and create a commit, then read ./claude/commands/describe_pr.md and create a PR, then add a comment to the Linear ticket with the PR link"
```
incorporate any user feedback then:
4. launch implementation session: `humanlayer launch --model opus -w ~/wt/humanlayer/ENG-XXXX "/implement_plan at $FILEPATH and when you are done implementing and all tests pass, read ./claude/commands/commit.md and create a commit, then read ./claude/commands/describe_pr.md and create a PR, then add a comment to the Linear ticket with the PR link"`

View File

@@ -1,196 +0,0 @@
# Debug
You are tasked with helping debug issues during manual testing or implementation. This command allows you to investigate problems by examining logs, database state, and git history without editing files. Think of this as a way to bootstrap a debugging session without using the primary window's context.
## Initial Response
When invoked WITH a plan/ticket file:
```
I'll help debug issues with [file name]. Let me understand the current state.
What specific problem are you encountering?
- What were you trying to test/implement?
- What went wrong?
- Any error messages?
I'll investigate the logs, database, and git state to help figure out what's happening.
```
When invoked WITHOUT parameters:
```
I'll help debug your current issue.
Please describe what's going wrong:
- What are you working on?
- What specific problem occurred?
- When did it last work?
I can investigate logs, database state, and recent changes to help identify the issue.
```
## Environment Information
You have access to these key locations and tools:
**Logs** (automatically created by `make daemon` and `make wui`):
- MCP logs: `~/.humanlayer/logs/mcp-claude-approvals-*.log`
- Combined WUI/Daemon logs: `~/.humanlayer/logs/wui-${BRANCH_NAME}/codelayer.log`
- First line shows: `[timestamp] starting [service] in [directory]`
**Database**:
- Location: `~/.humanlayer/daemon-{BRANCH_NAME}.db`
- SQLite database with sessions, events, approvals, etc.
- Can query directly with `sqlite3`
**Git State**:
- Check current branch, recent commits, uncommitted changes
- Similar to how `commit` and `describe_pr` commands work
**Service Status**:
- Check if daemon is running: `ps aux | grep hld`
- Check if WUI is running: `ps aux | grep wui`
- Socket exists: `~/.humanlayer/daemon.sock`
## Process Steps
### Step 1: Understand the Problem
After the user describes the issue:
1. **Read any provided context** (plan or ticket file):
- Understand what they're implementing/testing
- Note which phase or step they're on
- Identify expected vs actual behavior
2. **Quick state check**:
- Current git branch and recent commits
- Any uncommitted changes
- When the issue started occurring
### Step 2: Investigate the Issue
Spawn parallel Task agents for efficient investigation:
```
Task 1 - Check Recent Logs:
Find and analyze the most recent logs for errors:
1. Find latest daemon log: ls -t ~/.humanlayer/logs/daemon-*.log | head -1
2. Find latest WUI log: ls -t ~/.humanlayer/logs/wui-*.log | head -1
3. Search for errors, warnings, or issues around the problem timeframe
4. Note the working directory (first line of log)
5. Look for stack traces or repeated errors
Return: Key errors/warnings with timestamps
```
```
Task 2 - Database State:
Check the current database state:
1. Connect to database: sqlite3 ~/.humanlayer/daemon.db
2. Check schema: .tables and .schema for relevant tables
3. Query recent data:
- SELECT * FROM sessions ORDER BY created_at DESC LIMIT 5;
- SELECT * FROM conversation_events WHERE created_at > datetime('now', '-1 hour');
- Other queries based on the issue
4. Look for stuck states or anomalies
Return: Relevant database findings
```
```
Task 3 - Git and File State:
Understand what changed recently:
1. Check git status and current branch
2. Look at recent commits: git log --oneline -10
3. Check uncommitted changes: git diff
4. Verify expected files exist
5. Look for any file permission issues
Return: Git state and any file issues
```
### Step 3: Present Findings
Based on the investigation, present a focused debug report:
```markdown
## Debug Report
### What's Wrong
[Clear statement of the issue based on evidence]
### Evidence Found
**From Logs** (`~/.humanlayer/logs/`):
- [Error/warning with timestamp]
- [Pattern or repeated issue]
**From Database**:
```sql
-- Relevant query and result
[Finding from database]
```
**From Git/Files**:
- [Recent changes that might be related]
- [File state issues]
### Root Cause
[Most likely explanation based on evidence]
### Next Steps
1. **Try This First**:
```bash
[Specific command or action]
```
2. **If That Doesn't Work**:
- Restart services: `make daemon` and `make wui`
- Check browser console for WUI errors
- Run with debug: `HUMANLAYER_DEBUG=true make daemon`
### Can't Access?
Some issues might be outside my reach:
- Browser console errors (F12 in browser)
- MCP server internal state
- System-level issues
Would you like me to investigate something specific further?
```
## Important Notes
- **Focus on manual testing scenarios** - This is for debugging during implementation
- **Always require problem description** - Can't debug without knowing what's wrong
- **Read files completely** - No limit/offset when reading context
- **Think like `commit` or `describe_pr`** - Understand git state and changes
- **Guide back to user** - Some issues (browser console, MCP internals) are outside reach
- **No file editing** - Pure investigation only
## Quick Reference
**Find Latest Logs**:
```bash
ls -t ~/.humanlayer/logs/daemon-*.log | head -1
ls -t ~/.humanlayer/logs/wui-*.log | head -1
```
**Database Queries**:
```bash
sqlite3 ~/.humanlayer/daemon.db ".tables"
sqlite3 ~/.humanlayer/daemon.db ".schema sessions"
sqlite3 ~/.humanlayer/daemon.db "SELECT * FROM sessions ORDER BY created_at DESC LIMIT 5;"
```
**Service Check**:
```bash
ps aux | grep hld # Is daemon running?
ps aux | grep wui # Is WUI running?
```
**Git State**:
```bash
git status
git log --oneline -10
git diff
```
Remember: This command helps you investigate without burning the primary window's context. Perfect for when you hit an issue during manual testing and need to dig into logs, database, or git state.

View File

@@ -1,71 +0,0 @@
# Generate PR Description
You are tasked with generating a comprehensive pull request description following the repository's standard template.
## Steps to follow:
1. **Read the PR description template:**
- First, check if `thoughts/shared/pr_description.md` exists
- If it doesn't exist, inform the user that their `humanlayer thoughts` setup is incomplete and they need to create a PR description template at `thoughts/shared/pr_description.md`
- Read the template carefully to understand all sections and requirements
2. **Identify the PR to describe:**
- Check if the current branch has an associated PR: `gh pr view --json url,number,title,state 2>/dev/null`
- If no PR exists for the current branch, or if on main/master, list open PRs: `gh pr list --limit 10 --json number,title,headRefName,author`
- Ask the user which PR they want to describe
3. **Check for existing description:**
- Check if `thoughts/shared/prs/{number}_description.md` already exists
- If it exists, read it and inform the user you'll be updating it
- Consider what has changed since the last description was written
4. **Gather comprehensive PR information:**
- Get the full PR diff: `gh pr diff {number}`
- If you get an error about no default remote repository, instruct the user to run `gh repo set-default` and select the appropriate repository
- Get commit history: `gh pr view {number} --json commits`
- Review the base branch: `gh pr view {number} --json baseRefName`
- Get PR metadata: `gh pr view {number} --json url,title,number,state`
5. **Analyze the changes thoroughly:** (ultrathink about the code changes, their architectural implications, and potential impacts)
- Read through the entire diff carefully
- For context, read any files that are referenced but not shown in the diff
- Understand the purpose and impact of each change
- Identify user-facing changes vs internal implementation details
- Look for breaking changes or migration requirements
6. **Handle verification requirements:**
- Look for any checklist items in the "How to verify it" section of the template
- For each verification step:
- If it's a command you can run (like `make check test`, `npm test`, etc.), run it
- If it passes, mark the checkbox as checked: `- [x]`
- If it fails, keep it unchecked and note what failed: `- [ ]` with explanation
- If it requires manual testing (UI interactions, external services), leave unchecked and note for user
- Document any verification steps you couldn't complete
7. **Generate the description:**
- Fill out each section from the template thoroughly:
- Answer each question/section based on your analysis
- Be specific about problems solved and changes made
- Focus on user impact where relevant
- Include technical details in appropriate sections
- Write a concise changelog entry
- Ensure all checklist items are addressed (checked or explained)
8. **Save and sync the description:**
- Write the completed description to `thoughts/shared/prs/{number}_description.md`
- Run `humanlayer thoughts sync` to sync the thoughts directory
- Show the user the generated description
9. **Update the PR:**
- Update the PR description directly: `gh pr edit {number} --body-file thoughts/shared/prs/{number}_description.md`
- Confirm the update was successful
- If any verification steps remain unchecked, remind the user to complete them before merging
## Important notes:
- This command works across different repositories - always read the local template
- Be thorough but concise - descriptions should be scannable
- Focus on the "why" as much as the "what"
- Include any breaking changes or migration notes prominently
- If the PR touches multiple components, organize the description accordingly
- Always attempt to run verification commands when possible
- Clearly communicate which verification steps need manual testing

View File

@@ -1,65 +0,0 @@
# Implement Plan
You are tasked with implementing an approved technical plan from `thoughts/shared/plans/`. These plans contain phases with specific changes and success criteria.
## Getting Started
When given a plan path:
- Read the plan completely and check for any existing checkmarks (- [x])
- Read the original ticket and all files mentioned in the plan
- **Read files fully** - never use limit/offset parameters, you need complete context
- Think deeply about how the pieces fit together
- Create a todo list to track your progress
- Start implementing if you understand what needs to be done
If no plan path provided, ask for one.
## Implementation Philosophy
Plans are carefully designed, but reality can be messy. Your job is to:
- Follow the plan's intent while adapting to what you find
- Implement each phase fully before moving to the next
- Verify your work makes sense in the broader codebase context
- Update checkboxes in the plan as you complete sections
When things don't match the plan exactly, think about why and communicate clearly. The plan is your guide, but your judgment matters too.
If you encounter a mismatch:
- STOP and think deeply about why the plan can't be followed
- Present the issue clearly:
```
Issue in Phase [N]:
Expected: [what the plan says]
Found: [actual situation]
Why this matters: [explanation]
How should I proceed?
```
## Verification Approach
After implementing a phase:
- Run the success criteria checks (usually `cargo test -p [crate_name]` covers everything)
- Fix any issues before proceeding
- Update your progress in both the plan and your todos
- Check off completed items in the plan file itself using Edit
Don't let verification interrupt your flow - batch it at natural stopping points.
## If You Get Stuck
When something isn't working as expected:
- First, make sure you've read and understood all the relevant code
- Consider if the codebase has evolved since the plan was written
- Present the mismatch clearly and ask for guidance
Use sub-tasks sparingly - mainly for targeted debugging or exploring unfamiliar territory.
## Resuming Work
If the plan has existing checkmarks:
- Trust that completed work is done
- Pick up from the first unchecked item
- Verify previous work only if something seems off
Remember: You're implementing a solution, not just checking boxes. Keep the end goal in mind and maintain forward momentum.

View File

@@ -1,44 +0,0 @@
# Local Review
You are tasked with setting up a local review environment for a colleague's branch. This involves creating a worktree, setting up dependencies, and launching a new Claude Code session.
## Process
When invoked with a parameter like `gh_username:branchName`:
1. **Parse the input**:
- Extract GitHub username and branch name from the format `username:branchname`
- If no parameter provided, ask for it in the format: `gh_username:branchName`
2. **Extract ticket information**:
- Look for ticket numbers in the branch name (e.g., `eng-1696`, `ENG-1696`)
- Use this to create a short worktree directory name
- If no ticket found, use a sanitized version of the branch name
3. **Set up the remote and worktree**:
- Check if the remote already exists using `git remote -v`
- If not, add it: `git remote add USERNAME git@github.com:USERNAME/humanlayer`
- Fetch from the remote: `git fetch USERNAME`
- Create worktree: `git worktree add -b BRANCHNAME ~/wt/humanlayer/SHORT_NAME USERNAME/BRANCHNAME`
4. **Configure the worktree**:
- Copy Claude settings: `cp .claude/settings.local.json WORKTREE/.claude/`
- Run setup: `make -C WORKTREE setup`
- Initialize thoughts: `cd WORKTREE && npx humanlayer thoughts init --directory humanlayer`
## Error Handling
- If worktree already exists, inform the user they need to remove it first
- If remote fetch fails, check if the username/repo exists
- If setup fails, provide the error but continue with the launch
## Example Usage
```
/local_review samdickson22:sam/eng-1696-hotkey-for-yolo-mode
```
This will:
- Add 'samdickson22' as a remote
- Create worktree at `~/wt/humanlayer/eng-1696`
- Set up the environment

View File

@@ -1,28 +0,0 @@
## PART I - IF A TICKET IS MENTIONED
0c. use `linear` cli to fetch the selected item into thoughts with the ticket number - ./thoughts/shared/tickets/ENG-xxxx.md
0d. read the ticket and all comments to understand the implementation plan and any concerns
## PART I - IF NO TICKET IS MENTIOND
0. read .claude/commands/linear.md
0a. fetch the top 10 priority items from linear in status "ready for dev" using the MCP tools, noting all items in the `links` section
0b. select the highest priority SMALL or XS issue from the list (if no SMALL or XS issues exist, EXIT IMMEDIATELY and inform the user)
0c. use `linear` cli to fetch the selected item into thoughts with the ticket number - ./thoughts/shared/tickets/ENG-xxxx.md
0d. read the ticket and all comments to understand the implementation plan and any concerns
## PART II - NEXT STEPS
think deeply
1. move the item to "in dev" using the MCP tools
1a. identify the linked implementation plan document from the `links` section
1b. if no plan exists, move the ticket back to "ready for spec" and EXIT with an explanation
think deeply about the implementation
2. set up worktree for implementation:
2a. read `hack/create_worktree.sh` and create a new worktree with the Linear branch name: `./hack/create_worktree.sh ENG-XXXX BRANCH_NAME`
2b. launch implementation session: `npx humanlayer launch --model opus -w ~/wt/humanlayer/ENG-XXXX "/implement_plan and when you are done implementing and all tests pass, read ./claude/commands/commit.md and create a commit, then read ./claude/commands/describe_pr.md and create a PR, then add a comment to the Linear ticket with the PR link"`
think deeply, use TodoWrite to track your tasks. When fetching from linear, get the top 10 items by priority but only work on ONE item - specifically the highest priority SMALL or XS sized issue.

View File

@@ -1,30 +0,0 @@
## PART I - IF A TICKET IS MENTIONED
0c. use `linear` cli to fetch the selected item into thoughts with the ticket number - ./thoughts/shared/tickets/ENG-xxxx.md
0d. read the ticket and all comments to learn about past implementations and research, and any questions or concerns about them
### PART I - IF NO TICKET IS MENTIONED
0. read .claude/commands/linear.md
0a. fetch the top 10 priority items from linear in status "ready for spec" using the MCP tools, noting all items in the `links` section
0b. select the highest priority SMALL or XS issue from the list (if no SMALL or XS issues exist, EXIT IMMEDIATELY and inform the user)
0c. use `linear` cli to fetch the selected item into thoughts with the ticket number - ./thoughts/shared/tickets/ENG-xxxx.md
0d. read the ticket and all comments to learn about past implementations and research, and any questions or concerns about them
### PART II - NEXT STEPS
think deeply
1. move the item to "plan in progress" using the MCP tools
1a. read ./claude/commands/create_plan.md
1b. determine if the item has a linked implementation plan document based on the `links` section
1d. if the plan exists, you're done, respond with a link to the ticket
1e. if the research is insufficient or has unaswered questions, create a new plan document following the instructions in ./claude/commands/create_plan.md
think deeply
2. when the plan is complete, `humanlayer thoughts sync` and attach the doc to the ticket using the MCP tools and create a terse comment with a link to it (re-read .claude/commands/linear.md if needed)
2a. move the item to "plan in review" using the MCP tools
think deeply, use TodoWrite to track your tasks. When fetching from linear, get the top 10 items by priority but only work on ONE item - specifically the highest priority SMALL or XS sized issue.

View File

@@ -1,46 +0,0 @@
## PART I - IF A LINEAR TICKET IS MENTIONED
0c. use `linear` cli to fetch the selected item into thoughts with the ticket number - ./thoughts/shared/tickets/ENG-xxxx.md
0d. read the ticket and all comments to understand what research is needed and any previous attempts
## PART I - IF NO TICKET IS MENTIONED
0. read .claude/commands/linear.md
0a. fetch the top 10 priority items from linear in status "research needed" using the MCP tools, noting all items in the `links` section
0b. select the highest priority SMALL or XS issue from the list (if no SMALL or XS issues exist, EXIT IMMEDIATELY and inform the user)
0c. use `linear` cli to fetch the selected item into thoughts with the ticket number - ./thoughts/shared/tickets/ENG-xxxx.md
0d. read the ticket and all comments to understand what research is needed and any previous attempts
## PART II - NEXT STEPS
think deeply
1. move the item to "research in progress" using the MCP tools
1a. read any linked documents in the `links` section to understand context
1b. if insufficient information to conduct research, add a comment asking for clarification and move back to "research needed"
think deeply about the research needs
2. conduct the research:
2a. read .claude/commands/research_codebase.md for guidance on effective codebase research
2b. if the linear comments suggest web research is needed, use WebSearch to research external solutions, APIs, or best practices
2c. search the codebase for relevant implementations and patterns
2d. examine existing similar features or related code
2e. identify technical constraints and opportunities
2f. Be unbiased - don't think too much about an ideal implementation plan, just document all related files and how the systems work today
2g. document findings in a new thoughts document: `thoughts/shared/research/ENG-XXXX_research.md`
think deeply about the findings
3. synthesize research into actionable insights:
3a. summarize key findings and technical decisions
3b. identify potential implementation approaches
3c. note any risks or concerns discovered
3d. run `humanlayer thoughts sync` to save the research
4. update the ticket:
4a. attach the research document to the ticket using the MCP tools with proper link formatting
4b. add a comment summarizing the research outcomes
4c. move the item to "research in review" using the MCP tools
think deeply, use TodoWrite to track your tasks. When fetching from linear, get the top 10 items by priority but only work on ONE item - specifically the highest priority issue.

View File

@@ -1,172 +0,0 @@
# Research Codebase
You are tasked with conducting comprehensive research across the codebase to answer user questions by spawning parallel sub-agents and synthesizing their findings.
## Initial Setup:
When this command is invoked, respond with:
```
I'm ready to research the codebase. Please provide your research question or area of interest, and I'll analyze it thoroughly by exploring relevant components and connections.
```
Then wait for the user's research query.
## Steps to follow after receiving the research query:
1. **Read any directly mentioned files first:**
- If the user mentions specific files (crates, docs, JSON), read them FULLY first
- **IMPORTANT**: Use the Read tool WITHOUT limit/offset parameters to read entire files
- **CRITICAL**: Read these files yourself in the main context before spawning any sub-tasks
- This ensures you have full context before decomposing the research
2. **Analyze and decompose the research question:**
- Break down the user's query into composable research areas
- Take time to ultrathink about the underlying patterns, connections, and architectural implications the user might be seeking
- Identify specific components, patterns, or concepts to investigate
- Create a research plan using TodoWrite to track all subtasks
- Consider which directories, files, or architectural patterns are relevant
3. **Spawn parallel sub-agent tasks for comprehensive research:**
- Create multiple Task agents to research different aspects concurrently
- We now have specialized agents that know how to do specific research tasks:
**For codebase research:**
- Use the **codebase-locator** agent to find WHERE files and components live
- Use the **codebase-analyzer** agent to understand HOW specific code works
The key is to use these agents intelligently:
- Start with locator agents to find what exists
- Then use analyzer agents on the most promising findings
- Run multiple agents in parallel when they're searching for different things
- Each agent knows its job - just tell it what you're looking for
- Don't write detailed prompts about HOW to search - the agents already know
4. **Wait for all sub-agents to complete and synthesize findings:**
- IMPORTANT: Wait for ALL sub-agent tasks to complete before proceeding
- Compile all sub-agent results (both codebase and thoughts findings)
- Prioritize live codebase findings as primary source of truth
- Use thoughts/ findings as supplementary historical context
- Connect findings across different components
- Include specific file paths and line numbers for reference
- Verify all thoughts/ paths are correct (e.g., thoughts/allison/ not thoughts/shared/ for personal files)
- Highlight patterns, connections, and architectural decisions
- Answer the user's specific questions with concrete evidence
5. **Gather metadata for the research document:**
- Run the `zed/script/spec_metadata.sh` script to generate all relevant metadata
- Filename: `thoughts/shared/research/YYYY-MM-DD_HH-MM-SS_topic.md`
6. **Generate research document:**
- Use the metadata gathered in step 4
- Structure the document with YAML frontmatter followed by content:
```markdown
---
date: [Current date and time with timezone in ISO format]
researcher: [Researcher name from thoughts status]
git_commit: [Current commit hash]
branch: [Current branch name]
repository: [Repository name]
topic: "[User's Question/Topic]"
tags: [research, codebase, relevant-component-names]
status: complete
last_updated: [Current date in YYYY-MM-DD format]
last_updated_by: [Researcher name]
---
# Research: [User's Question/Topic]
**Date**: [Current date and time with timezone from step 4]
**Researcher**: [Researcher name from thoughts status]
**Git Commit**: [Current commit hash from step 4]
**Branch**: [Current branch name from step 4]
**Repository**: [Repository name]
## Research Question
[Original user query]
## Summary
[High-level findings answering the user's question]
## Detailed Findings
### [Component/Area 1]
- Finding with reference ([file.ext:line](link))
- Connection to other components
- Implementation details
### [Component/Area 2]
...
## Code References
- `path/to/file.py:123` - Description of what's there
- `another/file.ts:45-67` - Description of the code block
## Architecture Insights
[Patterns, conventions, and design decisions discovered]
## Historical Context (from thoughts/)
[Relevant insights from thoughts/ directory with references]
- `thoughts/shared/something.md` - Historical decision about X
- `thoughts/local/notes.md` - Past exploration of Y
Note: Paths exclude "searchable/" even if found there
## Related Research
[Links to other research documents in thoughts/shared/research/]
## Open Questions
[Any areas that need further investigation]
```
7. **Add GitHub permalinks (if applicable):**
- Check if on main branch or if commit is pushed: `git branch --show-current` and `git status`
- If on main/master or pushed, generate GitHub permalinks:
- Get repo info: `gh repo view --json owner,name`
- Create permalinks: `https://github.com/{owner}/{repo}/blob/{commit}/{file}#L{line}`
- Replace local file references with permalinks in the document
8. **Handle follow-up questions:**
- If the user has follow-up questions, append to the same research document
- Update the frontmatter fields `last_updated` and `last_updated_by` to reflect the update
- Add `last_updated_note: "Added follow-up research for [brief description]"` to frontmatter
- Add a new section: `## Follow-up Research [timestamp]`
- Spawn new sub-agents as needed for additional investigation
- Continue updating the document and syncing
## Important notes:
- Always use parallel Task agents to maximize efficiency and minimize context usage
- Always run fresh codebase research - never rely solely on existing research documents
- The thoughts/ directory provides historical context to supplement live findings
- Focus on finding concrete file paths and line numbers for developer reference
- Research documents should be self-contained with all necessary context
- Each sub-agent prompt should be specific and focused on read-only operations
- Consider cross-component connections and architectural patterns
- Include temporal context (when the research was conducted)
- Link to GitHub when possible for permanent references
- Keep the main agent focused on synthesis, not deep file reading
- Encourage sub-agents to find examples and usage patterns, not just definitions
- Explore all of thoughts/ directory, not just research subdirectory
- **File reading**: Always read mentioned files FULLY (no limit/offset) before spawning sub-tasks
- **Critical ordering**: Follow the numbered steps exactly
- ALWAYS read mentioned files first before spawning sub-tasks (step 1)
- ALWAYS wait for all sub-agents to complete before synthesizing (step 4)
- ALWAYS gather metadata before writing the document (step 5 before step 6)
- NEVER write the research document with placeholder values
- **Frontmatter consistency**:
- Always include frontmatter at the beginning of research documents
- Keep frontmatter fields consistent across all research documents
- Update frontmatter when adding follow-up research
- Use snake_case for multi-word field names (e.g., `last_updated`, `git_commit`)
- Tags should be relevant to the research topic and components studied

View File

@@ -1,162 +0,0 @@
# Validate Plan
You are tasked with validating that an implementation plan was correctly executed, verifying all success criteria and identifying any deviations or issues.
## Initial Setup
When invoked:
1. **Determine context** - Are you in an existing conversation or starting fresh?
- If existing: Review what was implemented in this session
- If fresh: Need to discover what was done through git and codebase analysis
2. **Locate the plan**:
- If plan path provided, use it
- Otherwise, search recent commits for plan references or ask user
3. **Gather implementation evidence**:
```bash
# Check recent commits
git log --oneline -n 20
git diff HEAD~N..HEAD # Where N covers implementation commits
# Run comprehensive checks
cd $(git rev-parse --show-toplevel) && make check test
```
## Validation Process
### Step 1: Context Discovery
If starting fresh or need more context:
1. **Read the implementation plan** completely
2. **Identify what should have changed**:
- List all files that should be modified
- Note all success criteria (automated and manual)
- Identify key functionality to verify
3. **Spawn parallel research tasks** to discover implementation:
```
Task 1 - Verify database changes:
Research if migration [N] was added and schema changes match plan.
Check: migration files, schema version, table structure
Return: What was implemented vs what plan specified
Task 2 - Verify code changes:
Find all modified files related to [feature].
Compare actual changes to plan specifications.
Return: File-by-file comparison of planned vs actual
Task 3 - Verify test coverage:
Check if tests were added/modified as specified.
Run test commands and capture results.
Return: Test status and any missing coverage
```
### Step 2: Systematic Validation
For each phase in the plan:
1. **Check completion status**:
- Look for checkmarks in the plan (- [x])
- Verify the actual code matches claimed completion
2. **Run automated verification**:
- Execute each command from "Automated Verification"
- Document pass/fail status
- If failures, investigate root cause
3. **Assess manual criteria**:
- List what needs manual testing
- Provide clear steps for user verification
4. **Think deeply about edge cases**:
- Were error conditions handled?
- Are there missing validations?
- Could the implementation break existing functionality?
### Step 3: Generate Validation Report
Create comprehensive validation summary:
```markdown
## Validation Report: [Plan Name]
### Implementation Status
✓ Phase 1: [Name] - Fully implemented
✓ Phase 2: [Name] - Fully implemented
⚠️ Phase 3: [Name] - Partially implemented (see issues)
### Automated Verification Results
✓ Build passes: `make build`
✓ Tests pass: `make test`
✗ Linting issues: `make lint` (3 warnings)
### Code Review Findings
#### Matches Plan:
- Database migration correctly adds [table]
- API endpoints implement specified methods
- Error handling follows plan
#### Deviations from Plan:
- Used different variable names in [file:line]
- Added extra validation in [file:line] (improvement)
#### Potential Issues:
- Missing index on foreign key could impact performance
- No rollback handling in migration
### Manual Testing Required:
1. UI functionality:
- [ ] Verify [feature] appears correctly
- [ ] Test error states with invalid input
2. Integration:
- [ ] Confirm works with existing [component]
- [ ] Check performance with large datasets
### Recommendations:
- Address linting warnings before merge
- Consider adding integration test for [scenario]
- Document new API endpoints
```
## Working with Existing Context
If you were part of the implementation:
- Review the conversation history
- Check your todo list for what was completed
- Focus validation on work done in this session
- Be honest about any shortcuts or incomplete items
## Important Guidelines
1. **Be thorough but practical** - Focus on what matters
2. **Run all automated checks** - Don't skip verification commands
3. **Document everything** - Both successes and issues
4. **Think critically** - Question if the implementation truly solves the problem
5. **Consider maintenance** - Will this be maintainable long-term?
## Validation Checklist
Always verify:
- [ ] All phases marked complete are actually done
- [ ] Automated tests pass
- [ ] Code follows existing patterns
- [ ] No regressions introduced
- [ ] Error handling is robust
- [ ] Documentation updated if needed
- [ ] Manual test steps are clear
## Relationship to Other Commands
Recommended workflow:
1. `/implement_plan` - Execute the implementation
2. `/commit` - Create atomic commits for changes
3. `/validate_plan` - Verify implementation correctness
4. `/describe_pr` - Generate PR description
The validation works best after commits are made, as it can analyze the git history to understand what was implemented.
Remember: Good validation catches issues before they reach production. Be constructive but thorough in identifying gaps or improvements.

View File

@@ -1,10 +0,0 @@
{
"permissions": {
"allow": [
// "Bash(./hack/spec_metadata.sh)",
// "Bash(hack/spec_metadata.sh)",
// "Bash(bash hack/spec_metadata.sh)"
]
},
"enableAllProjectMcpServers": false
}

View File

@@ -1,31 +0,0 @@
{
"permissions": {
"allow": [
"Read(/Users/mikaylamaki/projects/zed-work/zed-monorepo-real/**)",
"Read(/Users/nathan/src/agent-client-protocol/rust/**)",
"Read(/Users/nathan/src/agent-client-protocol/rust/**)",
"Read(/Users/nathan/src/agent-client-protocol/rust/**)",
"Read(/Users/nathan/src/agent-client-protocol/rust/**)",
"Bash(git add:*)",
"Read(/Users/nathan/src/agent-client-protocol/rust/**)",
"Bash(./script/spec_metadata.sh:*)",
"Bash(npm run generate:*)",
"Bash(npm run typecheck:*)",
"Bash(npm run:*)",
"Bash(npm install)",
"Bash(grep:*)",
"Bash(find:*)",
"Bash(node:*)",
"Bash(cargo check:*)",
"Bash(cargo test)",
"Bash(npx tsc:*)"
],
"additionalDirectories": [
"/Users/mikaylamaki/projects/zed-work/zed-monorepo-real/claude-code-acp/",
"/Users/mikaylamaki/projects/zed-work/zed-monorepo-real/agentic-coding-protocol/",
"/Users/nathan/src/agent",
"/Users/nathan/src/agent-client-protocol/",
"/Users/nathan/src/claude-code-acp"
]
}
}

8
Cargo.lock generated
View File

@@ -191,7 +191,9 @@ dependencies = [
[[package]]
name = "agent-client-protocol"
version = "0.2.0-alpha.0"
version = "0.0.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "289eb34ee17213dadcca47eedadd386a5e7678094095414e475965d1bcca2860"
dependencies = [
"anyhow",
"async-broadcast",
@@ -304,7 +306,6 @@ dependencies = [
"libc",
"log",
"nix 0.29.0",
"node_runtime",
"paths",
"project",
"rand 0.8.5",
@@ -9212,7 +9213,6 @@ dependencies = [
"language",
"lsp",
"project",
"proto",
"release_channel",
"serde_json",
"settings",
@@ -20589,7 +20589,7 @@ dependencies = [
[[package]]
name = "zed_html"
version = "0.2.2"
version = "0.2.1"
dependencies = [
"zed_extension_api 0.1.0",
]

View File

@@ -426,7 +426,7 @@ zlog_settings = { path = "crates/zlog_settings" }
# External crates
#
agent-client-protocol = { path = "../agent-client-protocol" }
agent-client-protocol = "0.0.31"
aho-corasick = "1.1"
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
any_vec = "0.14"

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-list-filter-icon lucide-list-filter"><path d="M3 6h18"/><path d="M7 12h10"/><path d="M10 18h4"/></svg>

Before

Width:  |  Height:  |  Size: 305 B

View File

@@ -435,7 +435,7 @@
"g b": "vim::WindowBottom",
"shift-r": "editor::Paste",
"x": "vim::HelixSelectLine",
"x": "editor::SelectLine",
"shift-x": "editor::SelectLine",
"%": "editor::SelectAll",
// Window mode

View File

@@ -363,8 +363,6 @@
// Whether to show code action buttons in the editor toolbar.
"code_actions": false
},
// Whether to allow windows to tab together based on the users tabbing preference (macOS only).
"use_system_window_tabs": false,
// Titlebar related settings
"title_bar": {
// Whether to show the branch icon beside branch switcher in the titlebar.

View File

@@ -789,12 +789,11 @@ pub enum ThreadStatus {
#[derive(Debug, Clone)]
pub enum LoadError {
NotInstalled,
Unsupported {
command: SharedString,
current_version: SharedString,
minimum_version: SharedString,
},
FailedToInstall(SharedString),
Exited {
status: ExitStatus,
},
@@ -804,19 +803,15 @@ pub enum LoadError {
impl Display for LoadError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
LoadError::NotInstalled => write!(f, "not installed"),
LoadError::Unsupported {
command: path,
current_version,
minimum_version,
} => {
write!(
f,
"version {current_version} from {path} is not supported (need at least {minimum_version})"
)
write!(f, "version {current_version} from {path} is not supported")
}
LoadError::FailedToInstall(msg) => write!(f, "Failed to install: {msg}"),
LoadError::Exited { status } => write!(f, "Server exited with status {status}"),
LoadError::Other(msg) => write!(f, "{msg}"),
LoadError::Other(msg) => write!(f, "{}", msg),
}
}
}
@@ -868,11 +863,6 @@ impl AcpThread {
&self.connection
}
/// Returns true if the agent supports custom slash commands.
pub fn supports_custom_commands(&self) -> bool {
self.prompt_capabilities.supports_custom_commands
}
pub fn action_log(&self) -> &Entity<ActionLog> {
&self.action_log
}
@@ -2643,7 +2633,6 @@ mod tests {
image: true,
audio: true,
embedded_context: true,
supports_custom_commands: false,
}),
cx,
)

View File

@@ -76,9 +76,6 @@ pub trait AgentConnection {
None
}
fn list_commands(&self, session_id: &acp::SessionId, cx: &mut App) -> Task<Result<acp::ListCommandsResponse>>;
fn run_command(&self, request: acp::RunCommandRequest, cx: &mut App) -> Task<Result<()>>;
fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
}
@@ -341,7 +338,6 @@ mod test_support {
image: true,
audio: true,
embedded_context: true,
supports_custom_commands: false,
}),
cx,
)
@@ -444,14 +440,6 @@ mod test_support {
Some(Rc::new(StubAgentSessionEditor))
}
fn list_commands(&self, _session_id: &acp::SessionId, _cx: &mut App) -> Task<Result<acp::ListCommandsResponse>> {
Task::ready(Ok(acp::ListCommandsResponse { commands: vec![] }))
}
fn run_command(&self, _request: acp::RunCommandRequest, _cx: &mut App) -> Task<Result<()>> {
Task::ready(Ok(()))
}
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
self
}

View File

@@ -93,7 +93,7 @@ impl LanguageModels {
let mut recommended = Vec::new();
for provider in &providers {
for model in provider.recommended_models(cx) {
recommended_models.insert((model.provider_id(), model.id()));
recommended_models.insert(model.id());
recommended.push(Self::map_language_model_to_info(&model, provider));
}
}
@@ -110,7 +110,7 @@ impl LanguageModels {
for model in provider.provided_models(cx) {
let model_info = Self::map_language_model_to_info(&model, &provider);
let model_id = model_info.id.clone();
if !recommended_models.contains(&(model.provider_id(), model.id())) {
if !recommended_models.contains(&model.id()) {
provider_models.push(model_info);
}
models.insert(model_id, model);
@@ -1027,19 +1027,6 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
Some(Rc::new(self.clone()) as Rc<dyn acp_thread::AgentTelemetry>)
}
fn list_commands(&self, session_id: &acp::SessionId, _cx: &mut App) -> Task<Result<acp::ListCommandsResponse>> {
// Native agent doesn't support custom commands yet
let _session_id = session_id.clone();
Task::ready(Ok(acp::ListCommandsResponse {
commands: vec![],
}))
}
fn run_command(&self, _request: acp::RunCommandRequest, _cx: &mut App) -> Task<Result<()>> {
// Native agent doesn't support custom commands yet
Task::ready(Err(anyhow!("Custom commands not supported")))
}
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
self
}

View File

@@ -1,9 +1,10 @@
use std::{any::Any, path::Path, rc::Rc, sync::Arc};
use agent_servers::{AgentServer, AgentServerDelegate};
use agent_servers::AgentServer;
use anyhow::Result;
use fs::Fs;
use gpui::{App, Entity, SharedString, Task};
use project::Project;
use prompt_store::PromptStore;
use crate::{HistoryStore, NativeAgent, NativeAgentConnection, templates::Templates};
@@ -29,21 +30,33 @@ impl AgentServer for NativeAgentServer {
"Zed Agent".into()
}
fn empty_state_headline(&self) -> SharedString {
self.name()
}
fn empty_state_message(&self) -> SharedString {
"".into()
}
fn logo(&self) -> ui::IconName {
ui::IconName::ZedAgent
}
fn install_command(&self) -> Option<&'static str> {
None
}
fn connect(
&self,
_root_dir: &Path,
delegate: AgentServerDelegate,
project: &Entity<Project>,
cx: &mut App,
) -> Task<Result<Rc<dyn acp_thread::AgentConnection>>> {
log::debug!(
"NativeAgentServer::connect called for path: {:?}",
_root_dir
);
let project = delegate.project().clone();
let project = project.clone();
let fs = self.fs.clone();
let history = self.history.clone();
let prompt_store = PromptStore::global(cx);

View File

@@ -588,7 +588,6 @@ impl Thread {
image,
audio: false,
embedded_context: true,
supports_custom_commands: false,
}
}

View File

@@ -2,7 +2,7 @@ use agent_client_protocol as acp;
use anyhow::Result;
use futures::{FutureExt as _, future::Shared};
use gpui::{App, AppContext, Entity, SharedString, Task};
use project::Project;
use project::{Project, terminals::TerminalKind};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{
@@ -144,14 +144,14 @@ impl AgentTool for TerminalTool {
let terminal = self
.project
.update(cx, |project, cx| {
project.create_terminal_task(
task::SpawnInTerminal {
project.create_terminal(
TerminalKind::Task(task::SpawnInTerminal {
command: Some(program),
args,
cwd: working_dir.clone(),
env,
..Default::default()
},
}),
cx,
)
})?

View File

@@ -6,7 +6,7 @@ publish.workspace = true
license = "GPL-3.0-or-later"
[features]
test-support = ["acp_thread/test-support", "gpui/test-support", "project/test-support", "dep:env_logger", "client/test-support", "dep:gpui_tokio", "reqwest_client/test-support"]
test-support = ["acp_thread/test-support", "gpui/test-support", "project/test-support", "dep:env_logger", "fs", "client/test-support", "dep:gpui_tokio", "reqwest_client/test-support"]
e2e = []
[lints]
@@ -27,7 +27,7 @@ client = { workspace = true, optional = true }
collections.workspace = true
context_server.workspace = true
env_logger = { workspace = true, optional = true }
fs.workspace = true
fs = { workspace = true, optional = true }
futures.workspace = true
gpui.workspace = true
gpui_tokio = { workspace = true, optional = true }
@@ -37,7 +37,6 @@ language.workspace = true
language_model.workspace = true
language_models.workspace = true
log.workspace = true
node_runtime.workspace = true
paths.workspace = true
project.workspace = true
rand.workspace = true

View File

@@ -3,7 +3,6 @@ use acp_thread::AgentConnection;
use acp_tools::AcpConnectionRegistry;
use action_log::ActionLog;
use agent_client_protocol::{self as acp, Agent as _, ErrorCode};
use agent_settings::AgentSettings;
use anyhow::anyhow;
use collections::HashMap;
use futures::AsyncBufReadExt as _;
@@ -11,7 +10,6 @@ use futures::channel::oneshot;
use futures::io::BufReader;
use project::Project;
use serde::Deserialize;
use settings::Settings as _;
use std::{any::Any, cell::RefCell};
use std::{path::Path, rc::Rc};
use thiserror::Error;
@@ -32,8 +30,6 @@ pub struct AcpConnection {
auth_methods: Vec<acp::AuthMethod>,
prompt_capabilities: acp::PromptCapabilities,
_io_task: Task<Result<()>>,
_wait_task: Task<Result<()>>,
_stderr_task: Task<Result<()>>,
}
pub struct AcpSession {
@@ -90,7 +86,7 @@ impl AcpConnection {
let io_task = cx.background_spawn(io_task);
let stderr_task = cx.background_spawn(async move {
cx.background_spawn(async move {
let mut stderr = BufReader::new(stderr);
let mut line = String::new();
while let Ok(n) = stderr.read_line(&mut line).await
@@ -99,10 +95,10 @@ impl AcpConnection {
log::warn!("agent stderr: {}", &line);
line.clear();
}
Ok(())
});
})
.detach();
let wait_task = cx.spawn({
cx.spawn({
let sessions = sessions.clone();
async move |cx| {
let status = child.status().await?;
@@ -118,7 +114,8 @@ impl AcpConnection {
anyhow::Ok(())
}
});
})
.detach();
let connection = Rc::new(connection);
@@ -136,7 +133,6 @@ impl AcpConnection {
read_text_file: true,
write_text_file: true,
},
terminal: true,
},
})
.await?;
@@ -152,8 +148,6 @@ impl AcpConnection {
sessions,
prompt_capabilities: response.agent_capabilities.prompt_capabilities,
_io_task: io_task,
_wait_task: wait_task,
_stderr_task: stderr_task,
})
}
@@ -329,23 +323,6 @@ impl AgentConnection for AcpConnection {
.detach();
}
fn list_commands(&self, session_id: &acp::SessionId, cx: &mut App) -> Task<Result<acp::ListCommandsResponse>> {
let conn = self.connection.clone();
let session_id = session_id.clone();
cx.foreground_executor().spawn(async move {
conn.list_commands(acp::ListCommandsRequest { session_id }).await
.map_err(Into::into)
})
}
fn run_command(&self, request: acp::RunCommandRequest, cx: &mut App) -> Task<Result<()>> {
let conn = self.connection.clone();
cx.foreground_executor().spawn(async move {
conn.run_command(request).await
.map_err(Into::into)
})
}
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
self
}
@@ -362,28 +339,6 @@ impl acp::Client for ClientDelegate {
arguments: acp::RequestPermissionRequest,
) -> Result<acp::RequestPermissionResponse, acp::Error> {
let cx = &mut self.cx.clone();
// If always_allow_tool_actions is enabled, then auto-choose the first "Allow" button
if AgentSettings::try_read_global(cx, |settings| settings.always_allow_tool_actions)
.unwrap_or(false)
{
// Don't use AllowAlways, because then if you were to turn off always_allow_tool_actions,
// some tools would (incorrectly) continue to auto-accept.
if let Some(allow_once_option) = arguments.options.iter().find_map(|option| {
if matches!(option.kind, acp::PermissionOptionKind::AllowOnce) {
Some(option.id.clone())
} else {
None
}
}) {
return Ok(acp::RequestPermissionResponse {
outcome: acp::RequestPermissionOutcome::Selected {
option_id: allow_once_option,
},
});
}
}
let rx = self
.sessions
.borrow()

View File

@@ -13,19 +13,12 @@ pub use gemini::*;
pub use settings::*;
use acp_thread::AgentConnection;
use acp_thread::LoadError;
use anyhow::Result;
use anyhow::anyhow;
use anyhow::bail;
use collections::HashMap;
use gpui::AppContext as _;
use gpui::{App, AsyncApp, Entity, SharedString, Task};
use node_runtime::VersionStrategy;
use project::Project;
use schemars::JsonSchema;
use semver::Version;
use serde::{Deserialize, Serialize};
use std::str::FromStr as _;
use std::{
any::Any,
path::{Path, PathBuf},
@@ -38,118 +31,23 @@ pub fn init(cx: &mut App) {
settings::init(cx);
}
pub struct AgentServerDelegate {
project: Entity<Project>,
status_tx: watch::Sender<SharedString>,
}
impl AgentServerDelegate {
pub fn new(project: Entity<Project>, status_tx: watch::Sender<SharedString>) -> Self {
Self { project, status_tx }
}
pub fn project(&self) -> &Entity<Project> {
&self.project
}
fn get_or_npm_install_builtin_agent(
self,
binary_name: SharedString,
package_name: SharedString,
entrypoint_path: PathBuf,
settings: Option<BuiltinAgentServerSettings>,
minimum_version: Option<Version>,
cx: &mut App,
) -> Task<Result<AgentServerCommand>> {
if let Some(settings) = &settings
&& let Some(command) = settings.clone().custom_command()
{
return Task::ready(Ok(command));
}
let project = self.project;
let fs = project.read(cx).fs().clone();
let Some(node_runtime) = project.read(cx).node_runtime().cloned() else {
return Task::ready(Err(anyhow!("Missing node runtime")));
};
let mut status_tx = self.status_tx;
cx.spawn(async move |cx| {
if let Some(settings) = settings && !settings.ignore_system_version.unwrap_or(true) {
if let Some(bin) = find_bin_in_path(binary_name.clone(), &project, cx).await {
return Ok(AgentServerCommand { path: bin, args: Vec::new(), env: Default::default() })
}
}
cx.background_spawn(async move {
let node_path = node_runtime.binary_path().await?;
let dir = paths::data_dir().join("external_agents").join(binary_name.as_str());
fs.create_dir(&dir).await?;
let local_executable_path = dir.join(entrypoint_path);
let command = AgentServerCommand {
path: node_path,
args: vec![local_executable_path.to_string_lossy().to_string()],
env: Default::default(),
};
let installed_version = node_runtime
.npm_package_installed_version(&dir, &package_name)
.await?
.filter(|version| {
Version::from_str(&version)
.is_ok_and(|version| Some(version) >= minimum_version)
});
status_tx.send("Checking for latest version…".into())?;
let latest_version = match node_runtime.npm_package_latest_version(&package_name).await
{
Ok(latest_version) => latest_version,
Err(e) => {
if let Some(installed_version) = installed_version {
log::error!("{e}");
log::warn!("failed to fetch latest version of {package_name}, falling back to cached version {installed_version}");
return Ok(command);
} else {
bail!(e);
}
}
};
let should_install = node_runtime
.should_install_npm_package(
&package_name,
&local_executable_path,
&dir,
VersionStrategy::Latest(&latest_version),
)
.await;
if should_install {
status_tx.send("Installing latest version…".into())?;
node_runtime
.npm_install_packages(&dir, &[(&package_name, &latest_version)])
.await?;
}
Ok(command)
}).await.map_err(|e| LoadError::FailedToInstall(e.to_string().into()).into())
})
}
}
pub trait AgentServer: Send {
fn logo(&self) -> ui::IconName;
fn name(&self) -> SharedString;
fn empty_state_headline(&self) -> SharedString;
fn empty_state_message(&self) -> SharedString;
fn telemetry_id(&self) -> &'static str;
fn connect(
&self,
root_dir: &Path,
delegate: AgentServerDelegate,
project: &Entity<Project>,
cx: &mut App,
) -> Task<Result<Rc<dyn AgentConnection>>>;
fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
fn install_command(&self) -> Option<&'static str>;
}
impl dyn AgentServer {
@@ -183,6 +81,15 @@ impl std::fmt::Debug for AgentServerCommand {
}
}
pub enum AgentServerVersion {
Supported,
Unsupported {
error_message: SharedString,
upgrade_message: SharedString,
upgrade_command: String,
},
}
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)]
pub struct AgentServerCommand {
#[serde(rename = "command")]
@@ -197,16 +104,23 @@ impl AgentServerCommand {
path_bin_name: &'static str,
extra_args: &[&'static str],
fallback_path: Option<&Path>,
settings: Option<BuiltinAgentServerSettings>,
settings: Option<AgentServerSettings>,
project: &Entity<Project>,
cx: &mut AsyncApp,
) -> Option<Self> {
if let Some(settings) = settings
&& let Some(command) = settings.custom_command()
{
Some(command)
if let Some(agent_settings) = settings {
Some(Self {
path: agent_settings.command.path,
args: agent_settings
.command
.args
.into_iter()
.chain(extra_args.iter().map(|arg| arg.to_string()))
.collect(),
env: agent_settings.command.env,
})
} else {
match find_bin_in_path(path_bin_name.into(), project, cx).await {
match find_bin_in_path(path_bin_name, project, cx).await {
Some(path) => Some(Self {
path,
args: extra_args.iter().map(|arg| arg.to_string()).collect(),
@@ -229,7 +143,7 @@ impl AgentServerCommand {
}
async fn find_bin_in_path(
bin_name: SharedString,
bin_name: &'static str,
project: &Entity<Project>,
cx: &mut AsyncApp,
) -> Option<PathBuf> {
@@ -259,11 +173,11 @@ async fn find_bin_in_path(
cx.background_executor()
.spawn(async move {
let which_result = if cfg!(windows) {
which::which(bin_name.as_str())
which::which(bin_name)
} else {
let env = env_task.await.unwrap_or_default();
let shell_path = env.get("PATH").cloned();
which::which_in(bin_name.as_str(), shell_path.as_ref(), root_dir.as_ref())
which::which_in(bin_name, shell_path.as_ref(), root_dir.as_ref())
};
if let Err(which::Error::CannotFindBinaryPath) = which_result {

View File

@@ -36,7 +36,7 @@ use util::{ResultExt, debug_panic};
use crate::claude::mcp_server::{ClaudeZedMcpServer, McpConfig};
use crate::claude::tools::ClaudeTool;
use crate::{AgentServer, AgentServerCommand, AgentServerDelegate, AllAgentServersSettings};
use crate::{AgentServer, AgentServerCommand, AllAgentServersSettings};
use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError, MentionUri};
#[derive(Clone)]
@@ -51,14 +51,26 @@ impl AgentServer for ClaudeCode {
"Claude Code".into()
}
fn empty_state_headline(&self) -> SharedString {
self.name()
}
fn empty_state_message(&self) -> SharedString {
"How can I help you today?".into()
}
fn logo(&self) -> ui::IconName {
ui::IconName::AiClaude
}
fn install_command(&self) -> Option<&'static str> {
Some("npm install -g @anthropic-ai/claude-code@latest")
}
fn connect(
&self,
_root_dir: &Path,
_delegate: AgentServerDelegate,
_project: &Entity<Project>,
_cx: &mut App,
) -> Task<Result<Rc<dyn AgentConnection>>> {
let connection = ClaudeAgentConnection {
@@ -100,7 +112,7 @@ impl AgentConnection for ClaudeAgentConnection {
)
.await
else {
return Err(anyhow!("Failed to find Claude Code binary"));
return Err(LoadError::NotInstalled.into());
};
let api_key =
@@ -220,7 +232,6 @@ impl AgentConnection for ClaudeAgentConnection {
LoadError::Unsupported {
command: command.path.to_string_lossy().to_string().into(),
current_version: version.to_string().into(),
minimum_version: "1.0.0".into(),
}
} else {
LoadError::Exited { status }
@@ -244,7 +255,6 @@ impl AgentConnection for ClaudeAgentConnection {
image: true,
audio: false,
embedded_context: true,
supports_custom_commands: false,
}),
cx,
)
@@ -340,19 +350,6 @@ impl AgentConnection for ClaudeAgentConnection {
.log_err();
}
fn list_commands(&self, session_id: &acp::SessionId, _cx: &mut App) -> Task<Result<acp::ListCommandsResponse>> {
// Claude agent doesn't support custom commands yet
let _session_id = session_id.clone();
Task::ready(Ok(acp::ListCommandsResponse {
commands: vec![],
}))
}
fn run_command(&self, _request: acp::RunCommandRequest, _cx: &mut App) -> Task<Result<()>> {
// Claude agent doesn't support custom commands yet
Task::ready(Err(anyhow!("Custom commands not supported")))
}
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
self
}

View File

@@ -1,8 +1,8 @@
use crate::{AgentServerCommand, AgentServerDelegate};
use crate::{AgentServerCommand, AgentServerSettings};
use acp_thread::AgentConnection;
use anyhow::Result;
use gpui::{App, SharedString, Task};
use language_models::provider::anthropic::AnthropicLanguageModelProvider;
use gpui::{App, Entity, SharedString, Task};
use project::Project;
use std::{path::Path, rc::Rc};
use ui::IconName;
@@ -13,8 +13,11 @@ pub struct CustomAgentServer {
}
impl CustomAgentServer {
pub fn new(name: SharedString, command: AgentServerCommand) -> Self {
Self { name, command }
pub fn new(name: SharedString, settings: &AgentServerSettings) -> Self {
Self {
name,
command: settings.command.clone(),
}
}
}
@@ -31,33 +34,33 @@ impl crate::AgentServer for CustomAgentServer {
IconName::Terminal
}
fn empty_state_headline(&self) -> SharedString {
"No conversations yet".into()
}
fn empty_state_message(&self) -> SharedString {
format!("Start a conversation with {}", self.name).into()
}
fn connect(
&self,
root_dir: &Path,
_delegate: AgentServerDelegate,
_project: &Entity<Project>,
cx: &mut App,
) -> Task<Result<Rc<dyn AgentConnection>>> {
let server_name = self.name();
let mut command = self.command.clone();
let command = self.command.clone();
let root_dir = root_dir.to_path_buf();
// TODO: Remove this once we have Claude properly
cx.spawn(async move |mut cx| {
if let Some(api_key) = cx
.update(AnthropicLanguageModelProvider::api_key)?
.await
.ok()
{
command
.env
.get_or_insert_default()
.insert("ANTHROPIC_API_KEY".to_owned(), api_key.key);
}
crate::acp::connect(server_name, command, &root_dir, &mut cx).await
})
}
fn install_command(&self) -> Option<&'static str> {
None
}
fn into_any(self: Rc<Self>) -> Rc<dyn std::any::Any> {
self
}

View File

@@ -1,4 +1,4 @@
use crate::{AgentServer, AgentServerDelegate};
use crate::AgentServer;
use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus};
use agent_client_protocol as acp;
use futures::{FutureExt, StreamExt, channel::mpsc, select};
@@ -471,8 +471,12 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
#[cfg(test)]
crate::AllAgentServersSettings::override_global(
crate::AllAgentServersSettings {
claude: Some(crate::claude::tests::local_command().into()),
gemini: Some(crate::gemini::tests::local_command().into()),
claude: Some(crate::AgentServerSettings {
command: crate::claude::tests::local_command(),
}),
gemini: Some(crate::AgentServerSettings {
command: crate::gemini::tests::local_command(),
}),
custom: collections::HashMap::default(),
},
cx,
@@ -490,10 +494,8 @@ pub async fn new_test_thread(
current_dir: impl AsRef<Path>,
cx: &mut TestAppContext,
) -> Entity<AcpThread> {
let delegate = AgentServerDelegate::new(project.clone(), watch::channel("".into()).0);
let connection = cx
.update(|cx| server.connect(current_dir.as_ref(), delegate, cx))
.update(|cx| server.connect(current_dir.as_ref(), &project, cx))
.await
.unwrap();

View File

@@ -2,11 +2,12 @@ use std::rc::Rc;
use std::{any::Any, path::Path};
use crate::acp::AcpConnection;
use crate::{AgentServer, AgentServerDelegate};
use crate::{AgentServer, AgentServerCommand};
use acp_thread::{AgentConnection, LoadError};
use anyhow::Result;
use gpui::{App, SharedString, Task};
use gpui::{App, Entity, SharedString, Task};
use language_models::provider::google::GoogleLanguageModelProvider;
use project::Project;
use settings::SettingsStore;
use crate::AllAgentServersSettings;
@@ -25,16 +26,29 @@ impl AgentServer for Gemini {
"Gemini CLI".into()
}
fn empty_state_headline(&self) -> SharedString {
self.name()
}
fn empty_state_message(&self) -> SharedString {
"Ask questions, edit files, run commands".into()
}
fn logo(&self) -> ui::IconName {
ui::IconName::AiGemini
}
fn install_command(&self) -> Option<&'static str> {
Some("npm install --engine-strict -g @google/gemini-cli@latest")
}
fn connect(
&self,
root_dir: &Path,
delegate: AgentServerDelegate,
project: &Entity<Project>,
cx: &mut App,
) -> Task<Result<Rc<dyn AgentConnection>>> {
let project = project.clone();
let root_dir = root_dir.to_path_buf();
let server_name = self.name();
cx.spawn(async move |cx| {
@@ -42,19 +56,12 @@ impl AgentServer for Gemini {
settings.get::<AllAgentServersSettings>(None).gemini.clone()
})?;
let mut command = cx
.update(|cx| {
delegate.get_or_npm_install_builtin_agent(
Self::BINARY_NAME.into(),
Self::PACKAGE_NAME.into(),
format!("node_modules/{}/dist/index.js", Self::PACKAGE_NAME).into(),
settings,
Some("0.2.1".parse().unwrap()),
cx,
)
})?
.await?;
command.args.push("--experimental-acp".into());
let Some(mut command) =
AgentServerCommand::resolve("gemini", &[ACP_ARG], None, settings, &project, cx)
.await
else {
return Err(LoadError::NotInstalled.into());
};
if let Some(api_key) = cx.update(GoogleLanguageModelProvider::api_key)?.await.ok() {
command
@@ -80,8 +87,12 @@ impl AgentServer for Gemini {
if !connection.prompt_capabilities().image {
return Err(LoadError::Unsupported {
current_version: current_version.into(),
command: command.path.to_string_lossy().to_string().into(),
minimum_version: Self::MINIMUM_VERSION.into(),
command: format!(
"{} {}",
command.path.to_string_lossy(),
command.args.join(" ")
)
.into(),
}
.into());
}
@@ -103,16 +114,13 @@ impl AgentServer for Gemini {
let (version_output, help_output) =
futures::future::join(version_fut, help_fut).await;
let current_version = std::str::from_utf8(&version_output?.stdout)?
.trim()
.to_string();
let current_version = String::from_utf8(version_output?.stdout)?;
let supported = String::from_utf8(help_output?.stdout)?.contains(ACP_ARG);
if !supported {
return Err(LoadError::Unsupported {
current_version: current_version.into(),
command: command.path.to_string_lossy().to_string().into(),
minimum_version: Self::MINIMUM_VERSION.into(),
}
.into());
}
@@ -128,11 +136,17 @@ impl AgentServer for Gemini {
}
impl Gemini {
const PACKAGE_NAME: &str = "@google/gemini-cli";
pub fn binary_name() -> &'static str {
"gemini"
}
const MINIMUM_VERSION: &str = "0.2.1";
pub fn install_command() -> &'static str {
"npm install --engine-strict -g @google/gemini-cli@latest"
}
const BINARY_NAME: &str = "gemini";
pub fn upgrade_command() -> &'static str {
"npm install -g @google/gemini-cli@latest"
}
}
#[cfg(test)]

View File

@@ -1,5 +1,3 @@
use std::path::PathBuf;
use crate::AgentServerCommand;
use anyhow::Result;
use collections::HashMap;
@@ -14,62 +12,16 @@ pub fn init(cx: &mut App) {
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug)]
pub struct AllAgentServersSettings {
pub gemini: Option<BuiltinAgentServerSettings>,
pub claude: Option<BuiltinAgentServerSettings>,
pub gemini: Option<AgentServerSettings>,
pub claude: Option<AgentServerSettings>,
/// Custom agent servers configured by the user
#[serde(flatten)]
pub custom: HashMap<SharedString, CustomAgentServerSettings>,
}
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
pub struct BuiltinAgentServerSettings {
/// Absolute path to a binary to be used when launching this agent.
///
/// This can be used to run a specific binary without automatic downloads or searching `$PATH`.
#[serde(rename = "command")]
pub path: Option<PathBuf>,
/// If a binary is specified in `command`, it will be passed these arguments.
pub args: Option<Vec<String>>,
/// If a binary is specified in `command`, it will be passed these environment variables.
pub env: Option<HashMap<String, String>>,
/// Whether to skip searching `$PATH` for an agent server binary when
/// launching this agent.
///
/// This has no effect if a `command` is specified. Otherwise, when this is
/// `false`, Zed will search `$PATH` for an agent server binary and, if one
/// is found, use it for threads with this agent. If no agent binary is
/// found on `$PATH`, Zed will automatically install and use its own binary.
/// When this is `true`, Zed will not search `$PATH`, and will always use
/// its own binary.
///
/// Default: true
pub ignore_system_version: Option<bool>,
}
impl BuiltinAgentServerSettings {
pub(crate) fn custom_command(self) -> Option<AgentServerCommand> {
self.path.map(|path| AgentServerCommand {
path,
args: self.args.unwrap_or_default(),
env: self.env,
})
}
}
impl From<AgentServerCommand> for BuiltinAgentServerSettings {
fn from(value: AgentServerCommand) -> Self {
BuiltinAgentServerSettings {
path: Some(value.path),
args: Some(value.args),
env: value.env,
..Default::default()
}
}
pub custom: HashMap<SharedString, AgentServerSettings>,
}
#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
pub struct CustomAgentServerSettings {
pub struct AgentServerSettings {
#[serde(flatten)]
pub command: AgentServerCommand,
}

View File

@@ -1,10 +1,10 @@
use std::cell::{Cell, RefCell};
use std::cell::Cell;
use std::ops::Range;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use acp_thread::{AcpThread, MentionUri};
use acp_thread::MentionUri;
use agent_client_protocol as acp;
use agent2::{HistoryEntry, HistoryStore};
use anyhow::Result;
@@ -15,7 +15,6 @@ use language::{Buffer, CodeLabel, HighlightId};
use lsp::CompletionContext;
use project::{
Completion, CompletionIntent, CompletionResponse, Project, ProjectPath, Symbol, WorktreeId,
lsp_store::CompletionDocumentation,
};
use prompt_store::PromptStore;
use rope::Point;
@@ -33,12 +32,6 @@ use crate::context_picker::{
ContextPickerAction, ContextPickerEntry, ContextPickerMode, selection_ranges,
};
#[derive(Debug)]
enum CompletionType {
Mention(MentionCompletion),
SlashCommand(SlashCommandCompletion),
}
pub(crate) enum Match {
File(FileMatch),
Symbol(SymbolMatch),
@@ -54,69 +47,6 @@ pub struct EntryMatch {
entry: ContextPickerEntry,
}
#[derive(Debug, Clone)]
pub struct SlashCommandCompletion {
pub source_range: Range<usize>,
pub command_name: String,
pub argument: Option<String>,
}
impl SlashCommandCompletion {
fn try_parse(line: &str, offset_to_line: usize) -> Option<Self> {
let last_slash_start = line.rfind('/')?;
if last_slash_start >= line.len() {
return Some(Self {
source_range: last_slash_start + offset_to_line..last_slash_start + 1 + offset_to_line,
command_name: String::new(),
argument: None,
});
}
// Check if slash is at word boundary (not preceded by alphanumeric)
if last_slash_start > 0
&& line
.chars()
.nth(last_slash_start - 1)
.is_some_and(|c| c.is_alphanumeric())
{
return None;
}
let rest_of_line = &line[last_slash_start + 1..];
let mut command_name = String::new();
let mut argument = None;
let mut parts = rest_of_line.split_whitespace();
let mut end = last_slash_start + 1;
if let Some(cmd_text) = parts.next() {
end += cmd_text.len();
command_name = cmd_text.to_string();
// Check for arguments after command name
match rest_of_line[cmd_text.len()..].find(|c: char| !c.is_whitespace()) {
Some(whitespace_count) => {
if let Some(arg_text) = parts.next() {
argument = Some(arg_text.to_string());
end += whitespace_count + arg_text.len();
}
}
None => {
// Rest of line is entirely whitespace
end += rest_of_line.len() - cmd_text.len();
}
}
}
Some(Self {
source_range: last_slash_start + offset_to_line..end + offset_to_line,
command_name,
argument,
})
}
}
impl Match {
pub fn score(&self) -> f64 {
match self {
@@ -137,7 +67,6 @@ pub struct ContextPickerCompletionProvider {
history_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
thread: Rc<RefCell<Option<WeakEntity<AcpThread>>>>,
}
impl ContextPickerCompletionProvider {
@@ -154,15 +83,9 @@ impl ContextPickerCompletionProvider {
history_store,
prompt_store,
prompt_capabilities,
thread: Rc::new(RefCell::new(None)),
}
}
/// Set the ACP thread for slash command support
pub fn set_thread(&self, thread: WeakEntity<AcpThread>) {
*self.thread.borrow_mut() = Some(thread);
}
fn completion_for_entry(
entry: ContextPickerEntry,
source_range: Range<Anchor>,
@@ -722,123 +645,22 @@ impl CompletionProvider for ContextPickerCompletionProvider {
_window: &mut Window,
cx: &mut Context<Editor>,
) -> Task<Result<Vec<CompletionResponse>>> {
// Get the buffer state first
let (line, offset_to_line) = buffer.update(cx, |buffer, _cx| {
let state = buffer.update(cx, |buffer, _cx| {
let position = buffer_position.to_point(buffer);
let line_start = Point::new(position.row, 0);
let offset_to_line = buffer.point_to_offset(line_start);
let mut lines = buffer.text_for_range(line_start..position).lines();
let line = lines.next().unwrap_or("");
(line.to_string(), offset_to_line)
});
// Then check for completions outside of the buffer update
let completion_state = {
// First try mention completion
if let Some(mention) = MentionCompletion::try_parse(
self.prompt_capabilities.get().embedded_context,
&line,
offset_to_line,
) {
Some(CompletionType::Mention(mention))
} else if let Some(thread) = self.thread.borrow().as_ref().cloned() {
// Then try slash command completion (only if thread supports commands)
if let Ok(supports_commands) = thread.read_with(cx, |thread, _| {
thread.supports_custom_commands()
}) {
if supports_commands {
if let Some(slash) = SlashCommandCompletion::try_parse(&line, offset_to_line) {
Some(CompletionType::SlashCommand(slash))
} else {
None
}
} else {
None
}
} else {
None
}
} else {
None
}
};
let Some(completion_type) = completion_state else {
return Task::ready(Ok(Vec::new()));
};
match completion_type {
CompletionType::Mention(state) => self.complete_mentions(state, buffer.clone(), buffer_position, cx),
CompletionType::SlashCommand(state) => self.complete_slash_commands(state, buffer.clone(), buffer_position, cx),
}
}
fn is_completion_trigger(
&self,
buffer: &Entity<language::Buffer>,
position: language::Anchor,
_text: &str,
_trigger_in_words: bool,
_menu_is_open: bool,
cx: &mut Context<Editor>,
) -> bool {
let buffer = buffer.read(cx);
let position = position.to_point(buffer);
let line_start = Point::new(position.row, 0);
let offset_to_line = buffer.point_to_offset(line_start);
let mut lines = buffer.text_for_range(line_start..position).lines();
if let Some(line) = lines.next() {
// Check for @ mention completions
if let Some(completion) = MentionCompletion::try_parse(
let line = lines.next()?;
MentionCompletion::try_parse(
self.prompt_capabilities.get().embedded_context,
line,
offset_to_line,
) {
let in_range = completion.source_range.start <= offset_to_line + position.column as usize
&& completion.source_range.end >= offset_to_line + position.column as usize;
if in_range {
return true;
}
}
// Check for slash command completions (only if thread supports commands)
if let Some(thread) = self.thread.borrow().as_ref().cloned() {
if let Ok(supports_commands) = thread.read_with(cx, |thread, _| {
thread.supports_custom_commands()
}) {
if supports_commands {
if let Some(completion) = SlashCommandCompletion::try_parse(line, offset_to_line) {
let in_range = completion.source_range.start <= offset_to_line + position.column as usize
&& completion.source_range.end >= offset_to_line + position.column as usize;
return in_range;
}
}
}
}
false
} else {
false
}
}
)
});
let Some(state) = state else {
return Task::ready(Ok(Vec::new()));
};
fn sort_completions(&self) -> bool {
false
}
fn filter_completions(&self) -> bool {
false
}
}
impl ContextPickerCompletionProvider {
fn complete_mentions(
&self,
state: MentionCompletion,
buffer: Entity<Buffer>,
_buffer_position: Anchor,
cx: &mut Context<Editor>,
) -> Task<Result<Vec<CompletionResponse>>> {
let Some(workspace) = self.workspace.upgrade() else {
return Task::ready(Ok(Vec::new()));
};
@@ -931,85 +753,49 @@ impl ContextPickerCompletionProvider {
Ok(vec![CompletionResponse {
completions,
// Since this does its own filtering (see `filter_completions()` returns false),
// there is no benefit to computing whether this set of completions is incomplete.
is_incomplete: true,
}])
})
}
fn complete_slash_commands(
fn is_completion_trigger(
&self,
state: SlashCommandCompletion,
buffer: Entity<Buffer>,
_buffer_position: Anchor,
buffer: &Entity<language::Buffer>,
position: language::Anchor,
_text: &str,
_trigger_in_words: bool,
_menu_is_open: bool,
cx: &mut Context<Editor>,
) -> Task<Result<Vec<CompletionResponse>>> {
let Some(thread) = self.thread.borrow().as_ref().cloned() else {
return Task::ready(Ok(Vec::new()));
};
) -> bool {
let buffer = buffer.read(cx);
let position = position.to_point(buffer);
let line_start = Point::new(position.row, 0);
let offset_to_line = buffer.point_to_offset(line_start);
let mut lines = buffer.text_for_range(line_start..position).lines();
if let Some(line) = lines.next() {
MentionCompletion::try_parse(
self.prompt_capabilities.get().embedded_context,
line,
offset_to_line,
)
.map(|completion| {
completion.source_range.start <= offset_to_line + position.column as usize
&& completion.source_range.end >= offset_to_line + position.column as usize
})
.unwrap_or(false)
} else {
false
}
}
let snapshot = buffer.read(cx).snapshot();
let source_range = snapshot.anchor_before(state.source_range.start)
..snapshot.anchor_after(state.source_range.end);
fn sort_completions(&self) -> bool {
false
}
let command_prefix = state.command_name.clone();
cx.spawn(async move |_, cx| {
// Get session ID and connection from the thread
let (session_id, connection) = thread.read_with(cx, |thread, _| {
(thread.session_id().clone(), thread.connection().clone())
})?;
// Fetch commands from the agent
let commands_task = cx.update(|cx| {
connection.list_commands(&session_id, cx)
})?;
let response = commands_task.await?;
// Filter commands matching the typed prefix
let matching_commands: Vec<_> = response.commands
.into_iter()
.filter(|cmd| {
// Support both prefix matching and fuzzy matching
cmd.name.starts_with(&command_prefix) ||
cmd.name.to_lowercase().contains(&command_prefix.to_lowercase())
})
.collect();
// Convert to project::Completion following existing patterns
let completions: Vec<_> = matching_commands
.into_iter()
.map(|command| {
let new_text = if command.requires_argument {
format!("/{} ", command.name) // Add space for argument
} else {
format!("/{}", command.name)
};
Completion {
replace_range: source_range.clone(),
new_text: new_text.clone(),
label: CodeLabel::plain(command.name.clone(), None),
icon_path: Some(IconName::ZedAssistant.path().into()),
documentation: if !command.description.is_empty() {
Some(CompletionDocumentation::SingleLine(command.description.clone().into()))
} else {
None
},
source: project::CompletionSource::Custom,
insert_text_mode: None,
confirm: Some(Arc::new(move |_, _, _| {
// For now, just insert the text - command execution will be handled later
false
})),
}
})
.collect();
Ok(vec![CompletionResponse {
completions,
is_incomplete: false,
}])
})
fn filter_completions(&self) -> bool {
false
}
}
@@ -1142,7 +928,6 @@ impl MentionCompletion {
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -6,8 +6,8 @@ use agent2::HistoryStore;
use collections::HashMap;
use editor::{Editor, EditorMode, MinimapVisibility};
use gpui::{
AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
ScrollHandle, TextStyleRefinement, WeakEntity, Window,
AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, Focusable, ScrollHandle,
TextStyleRefinement, WeakEntity, Window,
};
use language::language_settings::SoftWrap;
use project::Project;
@@ -247,13 +247,6 @@ pub enum Entry {
}
impl Entry {
pub fn focus_handle(&self, cx: &App) -> Option<FocusHandle> {
match self {
Self::UserMessage(editor) => Some(editor.read(cx).focus_handle(cx)),
Self::AssistantMessage(_) | Self::Content(_) => None,
}
}
pub fn message_editor(&self) -> Option<&Entity<MessageEditor>> {
match self {
Self::UserMessage(editor) => Some(editor),

View File

@@ -4,7 +4,7 @@ use crate::{
};
use acp_thread::{MentionUri, selection_name};
use agent_client_protocol as acp;
use agent_servers::{AgentServer, AgentServerDelegate};
use agent_servers::AgentServer;
use agent2::HistoryStore;
use anyhow::{Result, anyhow};
use assistant_slash_commands::codeblock_fence_for_path;
@@ -65,7 +65,6 @@ pub struct MessageEditor {
prompt_store: Option<Entity<PromptStore>>,
prevent_slash_commands: bool,
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
completion_provider: Rc<ContextPickerCompletionProvider>,
_subscriptions: Vec<Subscription>,
_parse_slash_command_task: Task<()>,
}
@@ -100,15 +99,13 @@ impl MessageEditor {
},
None,
);
let context_completion_provider = ContextPickerCompletionProvider::new(
let completion_provider = ContextPickerCompletionProvider::new(
cx.weak_entity(),
workspace.clone(),
history_store.clone(),
prompt_store.clone(),
prompt_capabilities.clone(),
);
let completion_provider = Rc::new(context_completion_provider);
let semantics_provider = Rc::new(SlashCommandSemanticsProvider {
range: Cell::new(None),
});
@@ -122,7 +119,7 @@ impl MessageEditor {
editor.set_show_indent_guides(false, cx);
editor.set_soft_wrap();
editor.set_use_modal_editing(true);
editor.set_completion_provider(Some(completion_provider.clone()));
editor.set_completion_provider(Some(Rc::new(completion_provider)));
editor.set_context_menu_options(ContextMenuOptions {
min_entries_visible: 12,
max_entries_visible: 12,
@@ -173,17 +170,11 @@ impl MessageEditor {
prompt_store,
prevent_slash_commands,
prompt_capabilities,
completion_provider,
_subscriptions: subscriptions,
_parse_slash_command_task: Task::ready(()),
}
}
pub fn set_thread(&mut self, thread: WeakEntity<acp_thread::AcpThread>, _cx: &mut Context<Self>) {
// Update the completion provider with the thread reference
self.completion_provider.set_thread(thread);
}
pub fn insert_thread_summary(
&mut self,
thread: agent2::DbThreadMetadata,
@@ -654,8 +645,7 @@ impl MessageEditor {
self.project.read(cx).fs().clone(),
self.history_store.clone(),
));
let delegate = AgentServerDelegate::new(self.project.clone(), watch::channel("".into()).0);
let connection = server.connect(Path::new(""), delegate, cx);
let connection = server.connect(Path::new(""), &self.project, cx);
cx.spawn(async move |_, cx| {
let agent = connection.await?;
let agent = agent.downcast::<agent2::NativeAgentConnection>().unwrap();
@@ -1914,7 +1904,6 @@ mod tests {
image: true,
audio: true,
embedded_context: true,
supports_custom_commands: false,
});
cx.simulate_input("Lorem ");

View File

@@ -73,8 +73,11 @@ impl AcpModelPickerDelegate {
this.update_in(cx, |this, window, cx| {
this.delegate.models = models.ok();
this.delegate.selected_model = selected_model.ok();
this.refresh(window, cx)
})
this.delegate.update_matches(this.query(cx), window, cx)
})?
.await;
Ok(())
}
refresh(&this, &session_id, cx).await.log_err();

View File

@@ -6,10 +6,10 @@ use acp_thread::{
use acp_thread::{AgentConnection, Plan};
use action_log::ActionLog;
use agent_client_protocol::{self as acp, PromptCapabilities};
use agent_servers::{AgentServer, AgentServerDelegate, ClaudeCode};
use agent_servers::{AgentServer, ClaudeCode};
use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, NotifyWhenAgentWaiting};
use agent2::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore};
use anyhow::{Result, anyhow, bail};
use anyhow::bail;
use audio::{Audio, Sound};
use buffer_diff::BufferDiff;
use client::zed_urls;
@@ -18,7 +18,6 @@ use editor::scroll::Autoscroll;
use editor::{Editor, EditorEvent, EditorMode, MultiBuffer, PathKey, SelectionEffects};
use file_icons::FileIcons;
use fs::Fs;
use futures::FutureExt as _;
use gpui::{
Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem,
CursorStyle, EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length,
@@ -40,13 +39,11 @@ use std::path::Path;
use std::sync::Arc;
use std::time::Instant;
use std::{collections::BTreeMap, rc::Rc, time::Duration};
use task::SpawnInTerminal;
use terminal_view::terminal_panel::TerminalPanel;
use text::Anchor;
use theme::ThemeSettings;
use ui::{
Callout, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding, PopoverMenuHandle,
Scrollbar, ScrollbarState, SpinnerLabel, Tooltip, prelude::*,
Scrollbar, ScrollbarState, SpinnerLabel, TintColor, Tooltip, prelude::*,
};
use util::{ResultExt, size::format_file_size, time::duration_alt_display};
use workspace::{CollaboratorId, Workspace};
@@ -96,10 +93,6 @@ impl ThreadError {
error.downcast_ref::<language_model::ModelRequestLimitReachedError>()
{
Self::ModelRequestLimitReached(error.plan)
} else if let Some(acp_error) = error.downcast_ref::<acp::Error>()
&& acp_error.code == acp::ErrorCode::AUTH_REQUIRED.code
{
Self::AuthenticationRequired(acp_error.message.clone().into())
} else {
let string = error.to_string();
// TODO: we should have Gemini return better errors here.
@@ -285,12 +278,15 @@ pub struct AcpThreadView {
editing_message: Option<usize>,
prompt_capabilities: Rc<Cell<PromptCapabilities>>,
is_loading_contents: bool,
install_command_markdown: Entity<Markdown>,
_cancel_task: Option<Task<()>>,
_subscriptions: [Subscription; 3],
}
enum ThreadState {
Loading(Entity<LoadingView>),
Loading {
_task: Task<()>,
},
Ready {
thread: Entity<AcpThread>,
title_editor: Option<Entity<Editor>>,
@@ -306,12 +302,6 @@ enum ThreadState {
},
}
struct LoadingView {
title: SharedString,
_load_task: Task<()>,
_update_title_task: Task<anyhow::Result<()>>,
}
impl AcpThreadView {
pub fn new(
agent: Rc<dyn AgentServer>,
@@ -402,6 +392,7 @@ impl AcpThreadView {
hovered_recent_history_item: None,
prompt_capabilities,
is_loading_contents: false,
install_command_markdown: cx.new(|cx| Markdown::new("".into(), None, None, cx)),
_subscriptions: subscriptions,
_cancel_task: None,
focus_handle: cx.focus_handle(),
@@ -422,10 +413,8 @@ impl AcpThreadView {
.next()
.map(|worktree| worktree.read(cx).abs_path())
.unwrap_or_else(|| paths::home_dir().as_path().into());
let (tx, mut rx) = watch::channel("Loading…".into());
let delegate = AgentServerDelegate::new(project.clone(), tx);
let connect_task = agent.connect(&root_dir, delegate, cx);
let connect_task = agent.connect(&root_dir, &project, cx);
let load_task = cx.spawn_in(window, async move |this, cx| {
let connection = match connect_task.await {
Ok(connection) => connection,
@@ -490,14 +479,11 @@ impl AcpThreadView {
.set(thread.read(cx).prompt_capabilities());
let count = thread.read(cx).entries().len();
this.list_state.splice(0..0, count);
this.entry_view_state.update(cx, |view_state, cx| {
for ix in 0..count {
view_state.sync_entry(ix, &thread, window, cx);
}
this.list_state.splice_focusable(
0..0,
(0..count).map(|ix| view_state.entry(ix)?.focus_handle(cx)),
);
});
if let Some(resume) = resume_thread {
@@ -551,16 +537,10 @@ impl AcpThreadView {
None
};
this.thread_state = ThreadState::Ready {
thread: thread.clone(),
thread,
title_editor,
_subscriptions: subscriptions,
};
// Update the message editor with the thread reference for slash command completion
this.message_editor.update(cx, |editor, cx| {
editor.set_thread(thread.downgrade(), cx);
});
this.message_editor.focus_handle(cx).focus(window);
this.profile_selector = this.as_native_thread(cx).map(|thread| {
@@ -584,25 +564,7 @@ impl AcpThreadView {
.log_err();
});
let loading_view = cx.new(|cx| {
let update_title_task = cx.spawn(async move |this, cx| {
loop {
let status = rx.recv().await?;
this.update(cx, |this: &mut LoadingView, cx| {
this.title = status;
cx.notify();
})?;
}
});
LoadingView {
title: "Loading…".into(),
_load_task: load_task,
_update_title_task: update_title_task,
}
});
ThreadState::Loading(loading_view)
ThreadState::Loading { _task: load_task }
}
fn handle_auth_required(
@@ -702,15 +664,13 @@ impl AcpThreadView {
}
}
pub fn title(&self, cx: &App) -> SharedString {
pub fn title(&self) -> SharedString {
match &self.thread_state {
ThreadState::Ready { .. } | ThreadState::Unauthenticated { .. } => "New Thread".into(),
ThreadState::Loading(loading_view) => loading_view.read(cx).title.clone(),
ThreadState::Loading { .. } => "Loading".into(),
ThreadState::LoadError(error) => match error {
LoadError::NotInstalled { .. } => format!("Install {}", self.agent.name()).into(),
LoadError::Unsupported { .. } => format!("Upgrade {}", self.agent.name()).into(),
LoadError::FailedToInstall(_) => {
format!("Failed to Install {}", self.agent.name()).into()
}
LoadError::Exited { .. } => format!("{} Exited", self.agent.name()).into(),
LoadError::Other(_) => format!("Error Loading {}", self.agent.name()).into(),
},
@@ -935,7 +895,7 @@ impl AcpThreadView {
fn send_impl(
&mut self,
contents: Task<Result<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>>,
contents: Task<anyhow::Result<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>>,
window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -945,10 +905,9 @@ impl AcpThreadView {
self.editing_message.take();
self.thread_feedback.clear();
let Some(thread) = self.thread() else {
let Some(thread) = self.thread().cloned() else {
return;
};
let thread = thread.downgrade();
if self.should_be_following {
self.workspace
.update(cx, |workspace, cx| {
@@ -1156,14 +1115,9 @@ impl AcpThreadView {
let len = thread.read(cx).entries().len();
let index = len - 1;
self.entry_view_state.update(cx, |view_state, cx| {
view_state.sync_entry(index, thread, window, cx);
self.list_state.splice_focusable(
index..index,
[view_state
.entry(index)
.and_then(|entry| entry.focus_handle(cx))],
);
view_state.sync_entry(index, thread, window, cx)
});
self.list_state.splice(index..index, 1);
}
AcpThreadEvent::EntryUpdated(index) => {
self.entry_view_state.update(cx, |view_state, cx| {
@@ -1271,31 +1225,6 @@ impl AcpThreadView {
});
return;
}
} else if method.0.as_ref() == "anthropic-api-key" {
let registry = LanguageModelRegistry::global(cx);
let provider = registry
.read(cx)
.provider(&language_model::ANTHROPIC_PROVIDER_ID)
.unwrap();
if !provider.is_authenticated(cx) {
let this = cx.weak_entity();
let agent = self.agent.clone();
let connection = connection.clone();
window.defer(cx, |window, cx| {
Self::handle_auth_required(
this,
AuthRequired {
description: Some("ANTHROPIC_API_KEY must be set".to_owned()),
provider_id: Some(language_model::ANTHROPIC_PROVIDER_ID),
},
agent,
connection,
window,
cx,
);
});
return;
}
} else if method.0.as_ref() == "vertex-ai"
&& std::env::var("GOOGLE_API_KEY").is_err()
&& (std::env::var("GOOGLE_CLOUD_PROJECT").is_err()
@@ -1327,15 +1256,7 @@ impl AcpThreadView {
self.thread_error.take();
configuration_view.take();
pending_auth_method.replace(method.clone());
let authenticate = if method.0.as_ref() == "claude-login" {
if let Some(workspace) = self.workspace.upgrade() {
Self::spawn_claude_login(&workspace, window, cx)
} else {
Task::ready(Ok(()))
}
} else {
connection.authenticate(method, cx)
};
let authenticate = connection.authenticate(method, cx);
cx.notify();
self.auth_task =
Some(cx.spawn_in(window, {
@@ -1359,13 +1280,6 @@ impl AcpThreadView {
this.update_in(cx, |this, window, cx| {
if let Err(err) = result {
if let ThreadState::Unauthenticated {
pending_auth_method,
..
} = &mut this.thread_state
{
pending_auth_method.take();
}
this.handle_thread_error(err, cx);
} else {
this.thread_state = Self::initial_state(
@@ -1384,76 +1298,6 @@ impl AcpThreadView {
}));
}
fn spawn_claude_login(
workspace: &Entity<Workspace>,
window: &mut Window,
cx: &mut App,
) -> Task<Result<()>> {
let Some(terminal_panel) = workspace.read(cx).panel::<TerminalPanel>(cx) else {
return Task::ready(Ok(()));
};
let project = workspace.read(cx).project().read(cx);
let cwd = project.first_project_directory(cx);
let shell = project.terminal_settings(&cwd, cx).shell.clone();
let terminal = terminal_panel.update(cx, |terminal_panel, cx| {
terminal_panel.spawn_task(
&SpawnInTerminal {
id: task::TaskId("claude-login".into()),
full_label: "claude /login".to_owned(),
label: "claude /login".to_owned(),
command: Some("claude".to_owned()),
args: vec!["/login".to_owned()],
command_label: "claude /login".to_owned(),
cwd,
use_new_terminal: true,
allow_concurrent_runs: true,
hide: task::HideStrategy::Always,
shell,
..Default::default()
},
window,
cx,
)
});
cx.spawn(async move |cx| {
let terminal = terminal.await?;
let mut exit_status = terminal
.read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
.fuse();
let logged_in = cx
.spawn({
let terminal = terminal.clone();
async move |cx| {
loop {
cx.background_executor().timer(Duration::from_secs(1)).await;
let content =
terminal.update(cx, |terminal, _cx| terminal.get_content())?;
if content.contains("Login successful") {
return anyhow::Ok(());
}
}
}
})
.fuse();
futures::pin_mut!(logged_in);
futures::select_biased! {
result = logged_in => {
if let Err(e) = result {
log::error!("{e}");
return Err(anyhow!("exited before logging in"));
}
}
_ = exit_status => {
return Err(anyhow!("exited before logging in"));
}
}
terminal.update(cx, |terminal, _| terminal.kill_active_task())?;
Ok(())
})
}
fn authorize_tool_call(
&mut self,
tool_call_id: acp::ToolCallId,
@@ -2980,26 +2824,18 @@ impl AcpThreadView {
window: &mut Window,
cx: &mut Context<Self>,
) -> AnyElement {
let (title, message, action_slot): (_, SharedString, _) = match e {
let (message, action_slot): (SharedString, _) = match e {
LoadError::NotInstalled => {
return self.render_not_installed(None, window, cx);
}
LoadError::Unsupported {
command: path,
current_version,
minimum_version,
} => {
return self.render_unsupported(path, current_version, minimum_version, window, cx);
return self.render_not_installed(Some((path, current_version)), window, cx);
}
LoadError::FailedToInstall(msg) => (
"Failed to Install",
msg.into(),
Some(self.create_copy_button(msg.to_string()).into_any_element()),
),
LoadError::Exited { status } => (
"Failed to Launch",
format!("Server exited with status {status}").into(),
None,
),
LoadError::Exited { .. } => ("Server exited with status {status}".into(), None),
LoadError::Other(msg) => (
"Failed to Launch",
msg.into(),
Some(self.create_copy_button(msg.to_string()).into_any_element()),
),
@@ -3008,34 +2844,95 @@ impl AcpThreadView {
Callout::new()
.severity(Severity::Error)
.icon(IconName::XCircleFilled)
.title(title)
.title("Failed to Launch")
.description(message)
.actions_slot(div().children(action_slot))
.into_any_element()
}
fn render_unsupported(
fn install_agent(&self, window: &mut Window, cx: &mut Context<Self>) {
telemetry::event!("Agent Install CLI", agent = self.agent.telemetry_id());
let Some(install_command) = self.agent.install_command().map(|s| s.to_owned()) else {
return;
};
let task = self
.workspace
.update(cx, |workspace, cx| {
let project = workspace.project().read(cx);
let cwd = project.first_project_directory(cx);
let shell = project.terminal_settings(&cwd, cx).shell.clone();
let spawn_in_terminal = task::SpawnInTerminal {
id: task::TaskId(install_command.clone()),
full_label: install_command.clone(),
label: install_command.clone(),
command: Some(install_command.clone()),
args: Vec::new(),
command_label: install_command.clone(),
cwd,
env: Default::default(),
use_new_terminal: true,
allow_concurrent_runs: true,
reveal: Default::default(),
reveal_target: Default::default(),
hide: Default::default(),
shell,
show_summary: true,
show_command: true,
show_rerun: false,
};
workspace.spawn_in_terminal(spawn_in_terminal, window, cx)
})
.ok();
let Some(task) = task else { return };
cx.spawn_in(window, async move |this, cx| {
if let Some(Ok(_)) = task.await {
this.update_in(cx, |this, window, cx| {
this.reset(window, cx);
})
.ok();
}
})
.detach()
}
fn render_not_installed(
&self,
path: &SharedString,
version: &SharedString,
minimum_version: &SharedString,
_window: &mut Window,
existing_version: Option<(&SharedString, &SharedString)>,
window: &mut Window,
cx: &mut Context<Self>,
) -> AnyElement {
let (heading_label, description_label) = (
format!("Upgrade {} to work with Zed", self.agent.name()),
if version.is_empty() {
format!(
"Currently using {}, which does not report a valid --version",
path,
let install_command = self.agent.install_command().unwrap_or_default();
self.install_command_markdown.update(cx, |markdown, cx| {
if !markdown.source().contains(&install_command) {
markdown.replace(format!("```\n{}\n```", install_command), cx);
}
});
let (heading_label, description_label, button_label) =
if let Some((path, version)) = existing_version {
(
format!("Upgrade {} to work with Zed", self.agent.name()),
if version.is_empty() {
format!(
"Currently using {}, which does not report a valid --version",
path,
)
} else {
format!(
"Currently using {}, which is only version {}",
path, version
)
},
format!("Upgrade {}", self.agent.name()),
)
} else {
format!(
"Currently using {}, which is only version {} (need at least {minimum_version})",
path, version
(
format!("Get Started with {} in Zed", self.agent.name()),
"Use Google's new coding agent directly in Zed.".to_string(),
format!("Install {}", self.agent.name()),
)
},
);
};
v_flex()
.w_full()
@@ -3055,6 +2952,34 @@ impl AcpThreadView {
.color(Color::Muted),
),
)
.child(
Button::new("install_gemini", button_label)
.full_width()
.size(ButtonSize::Medium)
.style(ButtonStyle::Tinted(TintColor::Accent))
.label_size(LabelSize::Small)
.icon(IconName::TerminalGhost)
.icon_color(Color::Muted)
.icon_size(IconSize::Small)
.icon_position(IconPosition::Start)
.on_click(cx.listener(|this, _, window, cx| this.install_agent(window, cx))),
)
.child(
Label::new("Or, run the following command in your terminal:")
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(MarkdownElement::new(
self.install_command_markdown.clone(),
default_markdown_style(false, false, window, cx),
))
.when_some(existing_version, |el, (path, _)| {
el.child(
Label::new(format!("If this does not work you will need to upgrade manually, or uninstall your existing version from {}", path))
.size(LabelSize::Small)
.color(Color::Muted),
)
})
.into_any_element()
}
@@ -4090,7 +4015,7 @@ impl AcpThreadView {
workspace: Entity<Workspace>,
window: &mut Window,
cx: &mut App,
) -> Task<Result<()>> {
) -> Task<anyhow::Result<()>> {
let markdown_language_task = workspace
.read(cx)
.app_state()
@@ -4943,6 +4868,18 @@ impl AcpThreadView {
}))
}
fn reset(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.thread_state = Self::initial_state(
self.agent.clone(),
None,
self.workspace.clone(),
self.project.clone(),
window,
cx,
);
cx.notify();
}
pub fn delete_history_entry(&mut self, entry: HistoryEntry, cx: &mut Context<Self>) {
let task = match entry {
HistoryEntry::AcpThread(thread) => self.history_store.update(cx, |history, cx| {
@@ -5471,10 +5408,22 @@ pub(crate) mod tests {
"Test".into()
}
fn empty_state_headline(&self) -> SharedString {
"Test".into()
}
fn empty_state_message(&self) -> SharedString {
"Test".into()
}
fn install_command(&self) -> Option<&'static str> {
None
}
fn connect(
&self,
_root_dir: &Path,
_delegate: AgentServerDelegate,
_project: &Entity<Project>,
_cx: &mut App,
) -> Task<gpui::Result<Rc<dyn AgentConnection>>> {
Task::ready(Ok(Rc::new(self.connection.clone())))

View File

@@ -5,7 +5,7 @@ mod tool_picker;
use std::{ops::Range, sync::Arc, time::Duration};
use agent_servers::{AgentServerCommand, AllAgentServersSettings, CustomAgentServerSettings};
use agent_servers::{AgentServerCommand, AgentServerSettings, AllAgentServersSettings, Gemini};
use agent_settings::AgentSettings;
use anyhow::Result;
use assistant_tool::{ToolSource, ToolWorkingSet};
@@ -27,6 +27,7 @@ use language_model::{
};
use notifications::status_toast::{StatusToast, ToastIcon};
use project::{
Project,
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
project_settings::{ContextServerSettings, ProjectSettings},
};
@@ -51,6 +52,7 @@ pub struct AgentConfiguration {
fs: Arc<dyn Fs>,
language_registry: Arc<LanguageRegistry>,
workspace: WeakEntity<Workspace>,
project: WeakEntity<Project>,
focus_handle: FocusHandle,
configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>,
context_server_store: Entity<ContextServerStore>,
@@ -60,6 +62,7 @@ pub struct AgentConfiguration {
_registry_subscription: Subscription,
scroll_handle: ScrollHandle,
scrollbar_state: ScrollbarState,
gemini_is_installed: bool,
_check_for_gemini: Task<()>,
}
@@ -70,6 +73,7 @@ impl AgentConfiguration {
tools: Entity<ToolWorkingSet>,
language_registry: Arc<LanguageRegistry>,
workspace: WeakEntity<Workspace>,
project: WeakEntity<Project>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
@@ -94,6 +98,11 @@ impl AgentConfiguration {
cx.subscribe(&context_server_store, |_, _, _, cx| cx.notify())
.detach();
cx.observe_global_in::<SettingsStore>(window, |this, _, cx| {
this.check_for_gemini(cx);
cx.notify();
})
.detach();
let scroll_handle = ScrollHandle::new();
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
@@ -102,6 +111,7 @@ impl AgentConfiguration {
fs,
language_registry,
workspace,
project,
focus_handle,
configuration_views_by_provider: HashMap::default(),
context_server_store,
@@ -111,9 +121,11 @@ impl AgentConfiguration {
_registry_subscription: registry_subscription,
scroll_handle,
scrollbar_state,
gemini_is_installed: false,
_check_for_gemini: Task::ready(()),
};
this.build_provider_configuration_views(window, cx);
this.check_for_gemini(cx);
this
}
@@ -143,6 +155,34 @@ impl AgentConfiguration {
self.configuration_views_by_provider
.insert(provider.id(), configuration_view);
}
fn check_for_gemini(&mut self, cx: &mut Context<Self>) {
let project = self.project.clone();
let settings = AllAgentServersSettings::get_global(cx).clone();
self._check_for_gemini = cx.spawn({
async move |this, cx| {
let Some(project) = project.upgrade() else {
return;
};
let gemini_is_installed = AgentServerCommand::resolve(
Gemini::binary_name(),
&[],
// TODO expose fallback path from the Gemini/CC types so we don't have to hardcode it again here
None,
settings.gemini,
&project,
cx,
)
.await
.is_some();
this.update(cx, |this, cx| {
this.gemini_is_installed = gemini_is_installed;
cx.notify();
})
.ok();
}
});
}
}
impl Focusable for AgentConfiguration {
@@ -1001,8 +1041,9 @@ impl AgentConfiguration {
name.clone(),
ExternalAgent::Custom {
name: name.clone(),
command: settings.command.clone(),
settings: settings.clone(),
},
None,
cx,
)
.into_any_element()
@@ -1061,6 +1102,7 @@ impl AgentConfiguration {
IconName::AiGemini,
"Gemini CLI",
ExternalAgent::Gemini,
(!self.gemini_is_installed).then_some(Gemini::install_command().into()),
cx,
))
// TODO add CC
@@ -1073,6 +1115,7 @@ impl AgentConfiguration {
icon: IconName,
name: impl Into<SharedString>,
agent: ExternalAgent,
install_command: Option<SharedString>,
cx: &mut Context<Self>,
) -> impl IntoElement {
let name = name.into();
@@ -1092,28 +1135,88 @@ impl AgentConfiguration {
.child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
.child(Label::new(name.clone())),
)
.child(
h_flex().gap_1().child(
Button::new(
SharedString::from(format!("start_acp_thread-{name}")),
"Start New Thread",
.map(|this| {
if let Some(install_command) = install_command {
this.child(
Button::new(
SharedString::from(format!("install_external_agent-{name}")),
"Install Agent",
)
.label_size(LabelSize::Small)
.icon(IconName::Plus)
.icon_position(IconPosition::Start)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.tooltip(Tooltip::text(install_command.clone()))
.on_click(cx.listener(
move |this, _, window, cx| {
let Some(project) = this.project.upgrade() else {
return;
};
let Some(workspace) = this.workspace.upgrade() else {
return;
};
let cwd = project.read(cx).first_project_directory(cx);
let shell =
project.read(cx).terminal_settings(&cwd, cx).shell.clone();
let spawn_in_terminal = task::SpawnInTerminal {
id: task::TaskId(install_command.to_string()),
full_label: install_command.to_string(),
label: install_command.to_string(),
command: Some(install_command.to_string()),
args: Vec::new(),
command_label: install_command.to_string(),
cwd,
env: Default::default(),
use_new_terminal: true,
allow_concurrent_runs: true,
reveal: Default::default(),
reveal_target: Default::default(),
hide: Default::default(),
shell,
show_summary: true,
show_command: true,
show_rerun: false,
};
let task = workspace.update(cx, |workspace, cx| {
workspace.spawn_in_terminal(spawn_in_terminal, window, cx)
});
cx.spawn(async move |this, cx| {
task.await;
this.update(cx, |this, cx| {
this.check_for_gemini(cx);
})
.ok();
})
.detach();
},
)),
)
.label_size(LabelSize::Small)
.icon(IconName::Thread)
.icon_position(IconPosition::Start)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.on_click(move |_, window, cx| {
window.dispatch_action(
NewExternalAgentThread {
agent: Some(agent.clone()),
}
.boxed_clone(),
cx,
);
}),
),
)
} else {
this.child(
h_flex().gap_1().child(
Button::new(
SharedString::from(format!("start_acp_thread-{name}")),
"Start New Thread",
)
.label_size(LabelSize::Small)
.icon(IconName::Thread)
.icon_position(IconPosition::Start)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.on_click(move |_, window, cx| {
window.dispatch_action(
NewExternalAgentThread {
agent: Some(agent.clone()),
}
.boxed_clone(),
cx,
);
}),
),
)
}
})
}
}
@@ -1290,7 +1393,7 @@ async fn open_new_agent_servers_entry_in_settings_editor(
unique_server_name = Some(server_name.clone());
file.custom.insert(
server_name,
CustomAgentServerSettings {
AgentServerSettings {
command: AgentServerCommand {
path: "path_to_executable".into(),
args: vec![],

View File

@@ -5,7 +5,7 @@ use std::sync::Arc;
use std::time::Duration;
use acp_thread::AcpThread;
use agent_servers::AgentServerCommand;
use agent_servers::AgentServerSettings;
use agent2::{DbThreadMetadata, HistoryEntry};
use db::kvp::{Dismissable, KEY_VALUE_STORE};
use serde::{Deserialize, Serialize};
@@ -259,7 +259,7 @@ pub enum AgentType {
NativeAgent,
Custom {
name: SharedString,
command: AgentServerCommand,
settings: AgentServerSettings,
},
}
@@ -1479,6 +1479,7 @@ impl AgentPanel {
tools,
self.language_registry.clone(),
self.workspace.clone(),
self.project.downgrade(),
window,
cx,
)
@@ -1895,8 +1896,8 @@ impl AgentPanel {
window,
cx,
),
AgentType::Custom { name, command } => self.external_thread(
Some(crate::ExternalAgent::Custom { name, command }),
AgentType::Custom { name, settings } => self.external_thread(
Some(crate::ExternalAgent::Custom { name, settings }),
None,
None,
window,
@@ -2114,7 +2115,7 @@ impl AgentPanel {
.child(title_editor)
.into_any_element()
} else {
Label::new(thread_view.read(cx).title(cx))
Label::new(thread_view.read(cx).title())
.color(Color::Muted)
.truncate()
.into_any_element()
@@ -2663,9 +2664,9 @@ impl AgentPanel {
AgentType::Custom {
name: agent_name
.clone(),
command: agent_settings
.command
.clone(),
settings:
agent_settings
.clone(),
},
window,
cx,

View File

@@ -28,7 +28,7 @@ use std::rc::Rc;
use std::sync::Arc;
use agent::{Thread, ThreadId};
use agent_servers::AgentServerCommand;
use agent_servers::AgentServerSettings;
use agent_settings::{AgentProfileId, AgentSettings, LanguageModelSelection};
use assistant_slash_command::SlashCommandRegistry;
use client::Client;
@@ -170,7 +170,7 @@ enum ExternalAgent {
NativeAgent,
Custom {
name: SharedString,
command: AgentServerCommand,
settings: AgentServerSettings,
},
}
@@ -193,9 +193,9 @@ impl ExternalAgent {
Self::Gemini => Rc::new(agent_servers::Gemini),
Self::ClaudeCode => Rc::new(agent_servers::ClaudeCode),
Self::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs, history)),
Self::Custom { name, command } => Rc::new(agent_servers::CustomAgentServer::new(
Self::Custom { name, settings } => Rc::new(agent_servers::CustomAgentServer::new(
name.clone(),
command.clone(),
settings,
)),
}
}

View File

@@ -62,8 +62,6 @@ impl AgentNotification {
app_id: Some(app_id.to_owned()),
window_min_size: None,
window_decorations: Some(WindowDecorations::Client),
tabbing_identifier: None,
..Default::default()
}
}
}

View File

@@ -373,7 +373,7 @@ pub async fn complete(
.uri(uri)
.header("Anthropic-Version", "2023-06-01")
.header("Anthropic-Beta", beta_headers)
.header("X-Api-Key", api_key.trim())
.header("X-Api-Key", api_key)
.header("Content-Type", "application/json");
let serialized_request =
@@ -526,7 +526,7 @@ pub async fn stream_completion_with_rate_limit_info(
.uri(uri)
.header("Anthropic-Version", "2023-06-01")
.header("Anthropic-Beta", beta_headers)
.header("X-Api-Key", api_key.trim())
.header("X-Api-Key", api_key)
.header("Content-Type", "application/json");
let serialized_request =
serde_json::to_string(&request).map_err(AnthropicError::SerializeRequest)?;

View File

@@ -492,7 +492,7 @@ mod custom_path_matcher {
pub fn new(globs: &[String]) -> Result<Self, globset::Error> {
let globs = globs
.iter()
.map(|glob| Glob::new(&SanitizedPath::new(glob).to_glob_string()))
.map(|glob| Glob::new(&SanitizedPath::from(glob).to_glob_string()))
.collect::<Result<Vec<_>, _>>()?;
let sources = globs.iter().map(|glob| glob.glob().to_owned()).collect();
let sources_with_trailing_slash = globs

View File

@@ -15,7 +15,7 @@ use language::LineEnding;
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use markdown::{Markdown, MarkdownElement, MarkdownStyle};
use portable_pty::{CommandBuilder, PtySize, native_pty_system};
use project::Project;
use project::{Project, terminals::TerminalKind};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
@@ -213,16 +213,17 @@ impl Tool for TerminalTool {
async move |cx| {
let program = program.await;
let env = env.await;
project
.update(cx, |project, cx| {
project.create_terminal_task(
task::SpawnInTerminal {
project.create_terminal(
TerminalKind::Task(task::SpawnInTerminal {
command: Some(program),
args,
cwd,
env,
..Default::default()
},
}),
cx,
)
})?

View File

@@ -287,7 +287,6 @@ pub enum Status {
},
ConnectionLost,
Reauthenticating,
Reauthenticated,
Reconnecting,
ReconnectionError {
next_reconnection: Instant,
@@ -299,21 +298,6 @@ impl Status {
matches!(self, Self::Connected { .. })
}
pub fn was_connected(&self) -> bool {
matches!(
self,
Self::ConnectionLost
| Self::Reauthenticating
| Self::Reauthenticated
| Self::Reconnecting
)
}
/// Returns whether the client is currently connected or was connected at some point.
pub fn is_or_was_connected(&self) -> bool {
self.is_connected() || self.was_connected()
}
pub fn is_signing_in(&self) -> bool {
matches!(
self,
@@ -873,13 +857,11 @@ impl Client {
try_provider: bool,
cx: &AsyncApp,
) -> Result<Credentials> {
let is_reauthenticating = if self.status().borrow().is_signed_out() {
if self.status().borrow().is_signed_out() {
self.set_status(Status::Authenticating, cx);
false
} else {
self.set_status(Status::Reauthenticating, cx);
true
};
}
let mut credentials = None;
@@ -937,14 +919,7 @@ impl Client {
self.cloud_client
.set_credentials(credentials.user_id as u32, credentials.access_token.clone());
self.state.write().credentials = Some(credentials.clone());
self.set_status(
if is_reauthenticating {
Status::Reauthenticated
} else {
Status::Authenticated
},
cx,
);
self.set_status(Status::Authenticated, cx);
Ok(credentials)
}
@@ -1059,7 +1034,6 @@ impl Client {
| Status::Authenticating
| Status::AuthenticationError
| Status::Reauthenticating
| Status::Reauthenticated
| Status::ReconnectionError { .. } => false,
Status::Connected { .. } | Status::Connecting | Status::Reconnecting => {
return ConnectionResult::Result(Ok(()));

View File

@@ -216,9 +216,7 @@ impl UserStore {
return Ok(());
};
match status {
Status::Authenticated
| Status::Reauthenticated
| Status::Connected { .. } => {
Status::Authenticated | Status::Connected { .. } => {
if let Some(user_id) = client.user_id() {
let response = client
.cloud_client()

View File

@@ -175,7 +175,6 @@ CREATE TABLE "language_servers" (
"project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
"name" VARCHAR NOT NULL,
"capabilities" TEXT NOT NULL,
"worktree_id" BIGINT,
PRIMARY KEY (project_id, id)
);

View File

@@ -1,2 +0,0 @@
ALTER TABLE language_servers
ADD COLUMN worktree_id BIGINT;

View File

@@ -694,7 +694,6 @@ impl Database {
project_id: ActiveValue::set(project_id),
id: ActiveValue::set(server.id as i64),
name: ActiveValue::set(server.name.clone()),
worktree_id: ActiveValue::set(server.worktree_id.map(|id| id as i64)),
capabilities: ActiveValue::set(update.capabilities.clone()),
})
.on_conflict(
@@ -705,7 +704,6 @@ impl Database {
.update_columns([
language_server::Column::Name,
language_server::Column::Capabilities,
language_server::Column::WorktreeId,
])
.to_owned(),
)
@@ -1067,7 +1065,7 @@ impl Database {
server: proto::LanguageServer {
id: language_server.id as u64,
name: language_server.name,
worktree_id: language_server.worktree_id.map(|id| id as u64),
worktree_id: None,
},
capabilities: language_server.capabilities,
})

View File

@@ -809,7 +809,7 @@ impl Database {
server: proto::LanguageServer {
id: language_server.id as u64,
name: language_server.name,
worktree_id: language_server.worktree_id.map(|id| id as u64),
worktree_id: None,
},
capabilities: language_server.capabilities,
})

View File

@@ -10,7 +10,6 @@ pub struct Model {
pub id: i64,
pub name: String,
pub capabilities: String,
pub worktree_id: Option<i64>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View File

@@ -476,9 +476,7 @@ impl Server {
.add_request_handler(forward_mutating_project_request::<proto::GitChangeBranch>)
.add_request_handler(forward_mutating_project_request::<proto::CheckForPushedCommits>)
.add_message_handler(broadcast_project_message_from_host::<proto::AdvertiseContexts>)
.add_message_handler(update_context)
.add_request_handler(forward_mutating_project_request::<proto::ToggleLspLogs>)
.add_message_handler(broadcast_project_message_from_host::<proto::LanguageServerLog>);
.add_message_handler(update_context);
Arc::new(server)
}

View File

@@ -3047,7 +3047,7 @@ impl Render for CollabPanel {
.on_action(cx.listener(CollabPanel::move_channel_down))
.track_focus(&self.focus_handle)
.size_full()
.child(if !self.client.status().borrow().is_or_was_connected() {
.child(if !self.client.status().borrow().is_connected() {
self.render_signed_out(cx)
} else {
self.render_signed_in(window, cx)

View File

@@ -66,7 +66,5 @@ fn notification_window_options(
app_id: Some(app_id.to_owned()),
window_min_size: None,
window_decorations: Some(WindowDecorations::Client),
tabbing_identifier: None,
..Default::default()
}
}

View File

@@ -62,6 +62,12 @@ impl CopilotChatConfiguration {
}
}
// Copilot's base model; defined by Microsoft in premium requests table
// This will be moved to the front of the Copilot model list, and will be used for
// 'fast' requests (e.g. title generation)
// https://docs.github.com/en/copilot/managing-copilot/monitoring-usage-and-entitlements/about-premium-requests
const DEFAULT_MODEL_ID: &str = "gpt-4.1";
#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum Role {
@@ -95,39 +101,22 @@ where
Ok(models)
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct Model {
billing: ModelBilling,
capabilities: ModelCapabilities,
id: String,
name: String,
policy: Option<ModelPolicy>,
vendor: ModelVendor,
is_chat_default: bool,
// The model with this value true is selected by VSCode copilot if a premium request limit is
// reached. Zed does not currently implement this behaviour
is_chat_fallback: bool,
model_picker_enabled: bool,
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
struct ModelBilling {
is_premium: bool,
multiplier: f64,
// List of plans a model is restricted to
// Field is not present if a model is available for all plans
#[serde(default)]
restricted_to: Option<Vec<String>>,
}
#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
struct ModelCapabilities {
family: String,
#[serde(default)]
limits: ModelLimits,
supports: ModelSupportedFeatures,
#[serde(rename = "type")]
model_type: String,
}
#[derive(Default, Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
@@ -613,7 +602,6 @@ async fn get_models(
.into_iter()
.filter(|model| {
model.model_picker_enabled
&& model.capabilities.model_type.as_str() == "chat"
&& model
.policy
.as_ref()
@@ -622,7 +610,9 @@ async fn get_models(
.dedup_by(|a, b| a.capabilities.family == b.capabilities.family)
.collect();
if let Some(default_model_position) = models.iter().position(|model| model.is_chat_default) {
if let Some(default_model_position) =
models.iter().position(|model| model.id == DEFAULT_MODEL_ID)
{
let default_model = models.remove(default_model_position);
models.insert(0, default_model);
}
@@ -640,9 +630,7 @@ async fn request_models(
.uri(models_url.as_ref())
.header("Authorization", format!("Bearer {}", api_token))
.header("Content-Type", "application/json")
.header("Copilot-Integration-Id", "vscode-chat")
.header("Editor-Version", "vscode/1.103.2")
.header("x-github-api-version", "2025-05-01");
.header("Copilot-Integration-Id", "vscode-chat");
let request = request_builder.body(AsyncBody::empty())?;
@@ -813,10 +801,6 @@ mod tests {
let json = r#"{
"data": [
{
"billing": {
"is_premium": false,
"multiplier": 0
},
"capabilities": {
"family": "gpt-4",
"limits": {
@@ -830,8 +814,6 @@ mod tests {
"type": "chat"
},
"id": "gpt-4",
"is_chat_default": false,
"is_chat_fallback": false,
"model_picker_enabled": false,
"name": "GPT 4",
"object": "model",
@@ -843,16 +825,6 @@ mod tests {
"some-unknown-field": 123
},
{
"billing": {
"is_premium": true,
"multiplier": 1,
"restricted_to": [
"pro",
"pro_plus",
"business",
"enterprise"
]
},
"capabilities": {
"family": "claude-3.7-sonnet",
"limits": {
@@ -876,8 +848,6 @@ mod tests {
"type": "chat"
},
"id": "claude-3.7-sonnet",
"is_chat_default": false,
"is_chat_fallback": false,
"model_picker_enabled": true,
"name": "Claude 3.7 Sonnet",
"object": "model",

View File

@@ -234,7 +234,6 @@ impl PythonDebugAdapter {
.await
.map_err(|e| format!("{e:#?}"))?
.success();
if !did_succeed {
return Err("Failed to create base virtual environment".into());
}

View File

@@ -85,10 +85,6 @@ actions!(
Rerun,
/// Toggles expansion of the selected item in the debugger UI.
ToggleExpandItem,
/// Toggle the user frame filter in the stack frame list
/// When toggled on, only frames from the user's code are shown
/// When toggled off, all frames are shown
ToggleUserFrames,
]
);
@@ -276,25 +272,12 @@ pub fn init(cx: &mut App) {
}
})
.on_action({
let active_item = active_item.clone();
move |_: &ToggleIgnoreBreakpoints, _, cx| {
active_item
.update(cx, |item, cx| item.toggle_ignore_breakpoints(cx))
.ok();
}
})
.on_action(move |_: &ToggleUserFrames, _, cx| {
if let Some((thread_status, stack_frame_list)) = active_item
.read_with(cx, |item, cx| {
(item.thread_status(cx), item.stack_frame_list().clone())
})
.ok()
{
stack_frame_list.update(cx, |stack_frame_list, cx| {
stack_frame_list.toggle_frame_filter(thread_status, cx);
})
}
})
});
})
.detach();

View File

@@ -1383,28 +1383,14 @@ impl PickerDelegate for DebugDelegate {
.border_color(cx.theme().colors().border_variant)
.children({
let action = menu::SecondaryConfirm.boxed_clone();
if self.matches.is_empty() {
Some(
Button::new("edit-debug-json", "Edit debug.json")
.label_size(LabelSize::Small)
.on_click(cx.listener(|_picker, _, window, cx| {
window.dispatch_action(
zed_actions::OpenProjectDebugTasks.boxed_clone(),
cx,
);
cx.emit(DismissEvent);
})),
)
} else {
KeyBinding::for_action(&*action, window, cx).map(|keybind| {
Button::new("edit-debug-task", "Edit in debug.json")
.label_size(LabelSize::Small)
.key_binding(keybind)
.on_click(move |_, window, cx| {
window.dispatch_action(action.boxed_clone(), cx)
})
})
}
KeyBinding::for_action(&*action, window, cx).map(|keybind| {
Button::new("edit-debug-task", "Edit in debug.json")
.label_size(LabelSize::Small)
.key_binding(keybind)
.on_click(move |_, window, cx| {
window.dispatch_action(action.boxed_clone(), cx)
})
})
})
.map(|this| {
if (current_modifiers.alt || self.matches.is_empty()) && !self.prompt.is_empty() {

View File

@@ -270,9 +270,12 @@ pub(crate) fn deserialize_pane_layout(
.children
.iter()
.map(|child| match child {
DebuggerPaneItem::Frames => {
Box::new(SubView::stack_frame_list(stack_frame_list.clone(), cx))
}
DebuggerPaneItem::Frames => Box::new(SubView::new(
stack_frame_list.focus_handle(cx),
stack_frame_list.clone().into(),
DebuggerPaneItem::Frames,
cx,
)),
DebuggerPaneItem::Variables => Box::new(SubView::new(
variable_list.focus_handle(cx),
variable_list.clone().into(),

View File

@@ -36,6 +36,7 @@ use module_list::ModuleList;
use project::{
DebugScenarioContext, Project, WorktreeId,
debugger::session::{self, Session, SessionEvent, SessionStateEvent, ThreadId, ThreadStatus},
terminals::TerminalKind,
};
use rpc::proto::ViewId;
use serde_json::Value;
@@ -157,29 +158,6 @@ impl SubView {
})
}
pub(crate) fn stack_frame_list(
stack_frame_list: Entity<StackFrameList>,
cx: &mut App,
) -> Entity<Self> {
let weak_list = stack_frame_list.downgrade();
let this = Self::new(
stack_frame_list.focus_handle(cx),
stack_frame_list.into(),
DebuggerPaneItem::Frames,
cx,
);
this.update(cx, |this, _| {
this.with_actions(Box::new(move |_, cx| {
weak_list
.update(cx, |this, _| this.render_control_strip())
.unwrap_or_else(|_| div().into_any_element())
}));
});
this
}
pub(crate) fn console(console: Entity<Console>, cx: &mut App) -> Entity<Self> {
let weak_console = console.downgrade();
let this = Self::new(
@@ -1039,11 +1017,12 @@ impl RunningState {
};
let terminal = project
.update(cx, |project, cx| {
project.create_terminal_task(
task_with_shell.clone(),
project.create_terminal(
TerminalKind::Task(task_with_shell.clone()),
cx,
)
})?.await?;
})?
.await?;
let terminal_view = cx.new_window_entity(|window, cx| {
TerminalView::new(
@@ -1187,7 +1166,7 @@ impl RunningState {
.filter(|title| !title.is_empty())
.or_else(|| command.clone())
.unwrap_or_else(|| "Debug terminal".to_string());
let kind = task::SpawnInTerminal {
let kind = TerminalKind::Task(task::SpawnInTerminal {
id: task::TaskId("debug".to_string()),
full_label: title.clone(),
label: title.clone(),
@@ -1205,13 +1184,12 @@ impl RunningState {
show_summary: false,
show_command: false,
show_rerun: false,
};
});
let workspace = self.workspace.clone();
let weak_project = project.downgrade();
let terminal_task =
project.update(cx, |project, cx| project.create_terminal_task(kind, cx));
let terminal_task = project.update(cx, |project, cx| project.create_terminal(kind, cx));
let terminal_task = cx.spawn_in(window, async move |_, cx| {
let terminal = terminal_task.await?;

View File

@@ -4,17 +4,16 @@ use std::time::Duration;
use anyhow::{Context as _, Result, anyhow};
use dap::StackFrameId;
use db::kvp::KEY_VALUE_STORE;
use gpui::{
Action, AnyElement, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, ListState,
MouseButton, Stateful, Subscription, Task, WeakEntity, list,
AnyElement, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, ListState, MouseButton,
Stateful, Subscription, Task, WeakEntity, list,
};
use util::debug_panic;
use crate::{StackTraceView, ToggleUserFrames};
use crate::StackTraceView;
use language::PointUtf16;
use project::debugger::breakpoint_store::ActiveStackFrame;
use project::debugger::session::{Session, SessionEvent, StackFrame, ThreadStatus};
use project::debugger::session::{Session, SessionEvent, StackFrame};
use project::{ProjectItem, ProjectPath};
use ui::{Scrollbar, ScrollbarState, Tooltip, prelude::*};
use workspace::{ItemHandle, Workspace};
@@ -27,34 +26,6 @@ pub enum StackFrameListEvent {
BuiltEntries,
}
/// Represents the filter applied to the stack frame list
#[derive(PartialEq, Eq, Copy, Clone)]
enum StackFrameFilter {
/// Show all frames
All,
/// Show only frames from the user's code
OnlyUserFrames,
}
impl StackFrameFilter {
fn from_str_or_default(s: impl AsRef<str>) -> Self {
match s.as_ref() {
"user" => StackFrameFilter::OnlyUserFrames,
"all" => StackFrameFilter::All,
_ => StackFrameFilter::All,
}
}
}
impl From<StackFrameFilter> for String {
fn from(filter: StackFrameFilter) -> Self {
match filter {
StackFrameFilter::All => "all".to_string(),
StackFrameFilter::OnlyUserFrames => "user".to_string(),
}
}
}
pub struct StackFrameList {
focus_handle: FocusHandle,
_subscription: Subscription,
@@ -66,8 +37,6 @@ pub struct StackFrameList {
opened_stack_frame_id: Option<StackFrameId>,
scrollbar_state: ScrollbarState,
list_state: ListState,
list_filter: StackFrameFilter,
filter_entries_indices: Vec<usize>,
error: Option<SharedString>,
_refresh_task: Task<()>,
}
@@ -104,16 +73,6 @@ impl StackFrameList {
let list_state = ListState::new(0, gpui::ListAlignment::Top, px(1000.));
let scrollbar_state = ScrollbarState::new(list_state.clone());
let list_filter = KEY_VALUE_STORE
.read_kvp(&format!(
"stack-frame-list-filter-{}",
session.read(cx).adapter().0
))
.ok()
.flatten()
.map(StackFrameFilter::from_str_or_default)
.unwrap_or(StackFrameFilter::All);
let mut this = Self {
session,
workspace,
@@ -121,11 +80,9 @@ impl StackFrameList {
state,
_subscription,
entries: Default::default(),
filter_entries_indices: Vec::default(),
error: None,
selected_ix: None,
opened_stack_frame_id: None,
list_filter,
list_state,
scrollbar_state,
_refresh_task: Task::ready(()),
@@ -146,15 +103,7 @@ impl StackFrameList {
) -> Vec<dap::StackFrame> {
self.entries
.iter()
.enumerate()
.filter(|(ix, _)| {
self.list_filter == StackFrameFilter::All
|| self
.filter_entries_indices
.binary_search_by_key(&ix, |ix| ix)
.is_ok()
})
.flat_map(|(_, frame)| match frame {
.flat_map(|frame| match frame {
StackFrameEntry::Normal(frame) => vec![frame.clone()],
StackFrameEntry::Label(frame) if show_labels => vec![frame.clone()],
StackFrameEntry::Collapsed(frames) if show_collapsed => frames.clone(),
@@ -177,15 +126,7 @@ impl StackFrameList {
self.stack_frames(cx)
.unwrap_or_default()
.into_iter()
.enumerate()
.filter(|(ix, _)| {
self.list_filter == StackFrameFilter::All
|| self
.filter_entries_indices
.binary_search_by_key(&ix, |ix| ix)
.is_ok()
})
.map(|(_, stack_frame)| stack_frame.dap)
.map(|stack_frame| stack_frame.dap)
.collect()
}
@@ -251,32 +192,7 @@ impl StackFrameList {
return;
}
};
let worktree_prefixes: Vec<_> = self
.workspace
.read_with(cx, |workspace, cx| {
workspace
.visible_worktrees(cx)
.map(|tree| tree.read(cx).abs_path())
.collect()
})
.unwrap_or_default();
let mut filter_entries_indices = Vec::default();
for (ix, stack_frame) in stack_frames.iter().enumerate() {
let frame_in_visible_worktree = stack_frame.dap.source.as_ref().is_some_and(|source| {
source.path.as_ref().is_some_and(|path| {
worktree_prefixes
.iter()
.filter_map(|tree| tree.to_str())
.any(|tree| path.starts_with(tree))
})
});
if frame_in_visible_worktree {
filter_entries_indices.push(ix);
}
for stack_frame in &stack_frames {
match stack_frame.dap.presentation_hint {
Some(dap::StackFramePresentationHint::Deemphasize)
| Some(dap::StackFramePresentationHint::Subtle) => {
@@ -309,10 +225,8 @@ impl StackFrameList {
let collapsed_entries = std::mem::take(&mut collapsed_entries);
if !collapsed_entries.is_empty() {
entries.push(StackFrameEntry::Collapsed(collapsed_entries));
self.filter_entries_indices.push(entries.len() - 1);
}
self.entries = entries;
self.filter_entries_indices = filter_entries_indices;
if let Some(ix) = first_stack_frame_with_path
.or(first_stack_frame)
@@ -328,14 +242,7 @@ impl StackFrameList {
self.selected_ix = ix;
}
match self.list_filter {
StackFrameFilter::All => {
self.list_state.reset(self.entries.len());
}
StackFrameFilter::OnlyUserFrames => {
self.list_state.reset(self.filter_entries_indices.len());
}
}
self.list_state.reset(self.entries.len());
cx.emit(StackFrameListEvent::BuiltEntries);
cx.notify();
}
@@ -665,11 +572,6 @@ impl StackFrameList {
}
fn render_entry(&self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
let ix = match self.list_filter {
StackFrameFilter::All => ix,
StackFrameFilter::OnlyUserFrames => self.filter_entries_indices[ix],
};
match &self.entries[ix] {
StackFrameEntry::Label(stack_frame) => self.render_label_entry(stack_frame, cx),
StackFrameEntry::Normal(stack_frame) => self.render_normal_entry(ix, stack_frame, cx),
@@ -800,67 +702,6 @@ impl StackFrameList {
self.activate_selected_entry(window, cx);
}
pub(crate) fn toggle_frame_filter(
&mut self,
thread_status: Option<ThreadStatus>,
cx: &mut Context<Self>,
) {
self.list_filter = match self.list_filter {
StackFrameFilter::All => StackFrameFilter::OnlyUserFrames,
StackFrameFilter::OnlyUserFrames => StackFrameFilter::All,
};
if let Some(database_id) = self
.workspace
.read_with(cx, |workspace, _| workspace.database_id())
.ok()
.flatten()
{
let database_id: i64 = database_id.into();
let save_task = KEY_VALUE_STORE.write_kvp(
format!(
"stack-frame-list-filter-{}-{}",
self.session.read(cx).adapter().0,
database_id,
),
self.list_filter.into(),
);
cx.background_spawn(save_task).detach();
}
if let Some(ThreadStatus::Stopped) = thread_status {
match self.list_filter {
StackFrameFilter::All => {
self.list_state.reset(self.entries.len());
}
StackFrameFilter::OnlyUserFrames => {
self.list_state.reset(self.filter_entries_indices.len());
if !self
.selected_ix
.map(|ix| self.filter_entries_indices.contains(&ix))
.unwrap_or_default()
{
self.selected_ix = None;
}
}
}
if let Some(ix) = self.selected_ix {
let scroll_to = match self.list_filter {
StackFrameFilter::All => ix,
StackFrameFilter::OnlyUserFrames => self
.filter_entries_indices
.binary_search_by_key(&ix, |ix| *ix)
.expect("This index will always exist"),
};
self.list_state.scroll_to_reveal_item(scroll_to);
}
cx.emit(StackFrameListEvent::BuiltEntries);
cx.notify();
}
}
fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div().p_1().size_full().child(
list(
@@ -870,30 +711,6 @@ impl StackFrameList {
.size_full(),
)
}
pub(crate) fn render_control_strip(&self) -> AnyElement {
let tooltip_title = match self.list_filter {
StackFrameFilter::All => "Show stack frames from your project",
StackFrameFilter::OnlyUserFrames => "Show all stack frames",
};
h_flex()
.child(
IconButton::new(
"filter-by-visible-worktree-stack-frame-list",
IconName::ListFilter,
)
.tooltip(move |window, cx| {
Tooltip::for_action(tooltip_title, &ToggleUserFrames, window, cx)
})
.toggle_state(self.list_filter == StackFrameFilter::OnlyUserFrames)
.icon_size(IconSize::Small)
.on_click(|_, window, cx| {
window.dispatch_action(ToggleUserFrames.boxed_clone(), cx)
}),
)
.into_any_element()
}
}
impl Render for StackFrameList {

View File

@@ -752,288 +752,3 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo
});
});
}
#[gpui::test]
async fn test_stack_frame_filter(executor: BackgroundExecutor, cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(executor.clone());
let test_file_content = r#"
function main() {
doSomething();
}
function doSomething() {
console.log('doing something');
}
"#
.unindent();
fs.insert_tree(
path!("/project"),
json!({
"src": {
"test.js": test_file_content,
}
}),
)
.await;
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
client.on_request::<Threads, _>(move |_, _| {
Ok(dap::ThreadsResponse {
threads: vec![dap::Thread {
id: 1,
name: "Thread 1".into(),
}],
})
});
client.on_request::<Scopes, _>(move |_, _| Ok(dap::ScopesResponse { scopes: vec![] }));
let stack_frames = vec![
StackFrame {
id: 1,
name: "main".into(),
source: Some(dap::Source {
name: Some("test.js".into()),
path: Some(path!("/project/src/test.js").into()),
source_reference: None,
presentation_hint: None,
origin: None,
sources: None,
adapter_data: None,
checksums: None,
}),
line: 2,
column: 1,
end_line: None,
end_column: None,
can_restart: None,
instruction_pointer_reference: None,
module_id: None,
presentation_hint: None,
},
StackFrame {
id: 2,
name: "node:internal/modules/cjs/loader".into(),
source: Some(dap::Source {
name: Some("loader.js".into()),
path: Some(path!("/usr/lib/node/internal/modules/cjs/loader.js").into()),
source_reference: None,
presentation_hint: None,
origin: None,
sources: None,
adapter_data: None,
checksums: None,
}),
line: 100,
column: 1,
end_line: None,
end_column: None,
can_restart: None,
instruction_pointer_reference: None,
module_id: None,
presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
},
StackFrame {
id: 3,
name: "node:internal/modules/run_main".into(),
source: Some(dap::Source {
name: Some("run_main.js".into()),
path: Some(path!("/usr/lib/node/internal/modules/run_main.js").into()),
source_reference: None,
presentation_hint: None,
origin: None,
sources: None,
adapter_data: None,
checksums: None,
}),
line: 50,
column: 1,
end_line: None,
end_column: None,
can_restart: None,
instruction_pointer_reference: None,
module_id: None,
presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
},
StackFrame {
id: 4,
name: "doSomething".into(),
source: Some(dap::Source {
name: Some("test.js".into()),
path: Some(path!("/project/src/test.js").into()),
source_reference: None,
presentation_hint: None,
origin: None,
sources: None,
adapter_data: None,
checksums: None,
}),
line: 3,
column: 1,
end_line: None,
end_column: None,
can_restart: None,
instruction_pointer_reference: None,
module_id: None,
presentation_hint: None,
},
];
// Store a copy for assertions
let stack_frames_for_assertions = stack_frames.clone();
client.on_request::<StackTrace, _>({
let stack_frames = Arc::new(stack_frames.clone());
move |_, args| {
assert_eq!(1, args.thread_id);
Ok(dap::StackTraceResponse {
stack_frames: (*stack_frames).clone(),
total_frames: None,
})
}
});
client
.fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
reason: dap::StoppedEventReason::Pause,
description: None,
thread_id: Some(1),
preserve_focus_hint: None,
text: None,
all_threads_stopped: None,
hit_breakpoint_ids: None,
}))
.await;
cx.run_until_parked();
// trigger threads to load
active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
session.running_state().update(cx, |running_state, cx| {
running_state
.session()
.update(cx, |session, cx| session.threads(cx));
});
});
cx.run_until_parked();
// select first thread
active_debug_session_panel(workspace, cx).update_in(cx, |session, window, cx| {
session.running_state().update(cx, |running_state, cx| {
running_state.select_current_thread(
&running_state
.session()
.update(cx, |session, cx| session.threads(cx)),
window,
cx,
);
});
});
cx.run_until_parked();
// trigger stack frames to load
active_debug_session_panel(workspace, cx).update(cx, |debug_panel_item, cx| {
let stack_frame_list = debug_panel_item
.running_state()
.update(cx, |state, _| state.stack_frame_list().clone());
stack_frame_list.update(cx, |stack_frame_list, cx| {
stack_frame_list.dap_stack_frames(cx);
});
});
cx.run_until_parked();
active_debug_session_panel(workspace, cx).update_in(cx, |debug_panel_item, window, cx| {
let stack_frame_list = debug_panel_item
.running_state()
.update(cx, |state, _| state.stack_frame_list().clone());
stack_frame_list.update(cx, |stack_frame_list, cx| {
stack_frame_list.build_entries(true, window, cx);
// Verify we have the expected collapsed structure
assert_eq!(
stack_frame_list.entries(),
&vec![
StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()),
StackFrameEntry::Collapsed(vec![
stack_frames_for_assertions[1].clone(),
stack_frames_for_assertions[2].clone()
]),
StackFrameEntry::Normal(stack_frames_for_assertions[3].clone()),
]
);
// Test 1: Verify filtering works
let all_frames = stack_frame_list.flatten_entries(true, false);
assert_eq!(all_frames.len(), 4, "Should see all 4 frames initially");
// Toggle to user frames only
stack_frame_list
.toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
let user_frames = stack_frame_list.dap_stack_frames(cx);
assert_eq!(user_frames.len(), 2, "Should only see 2 user frames");
assert_eq!(user_frames[0].name, "main");
assert_eq!(user_frames[1].name, "doSomething");
// Test 2: Verify filtering toggles correctly
// Check we can toggle back and see all frames again
// Toggle back to all frames
stack_frame_list
.toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
let all_frames_again = stack_frame_list.flatten_entries(true, false);
assert_eq!(
all_frames_again.len(),
4,
"Should see all 4 frames after toggling back"
);
// Test 3: Verify collapsed entries stay expanded
stack_frame_list.expand_collapsed_entry(1, cx);
assert_eq!(
stack_frame_list.entries(),
&vec![
StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()),
StackFrameEntry::Normal(stack_frames_for_assertions[1].clone()),
StackFrameEntry::Normal(stack_frames_for_assertions[2].clone()),
StackFrameEntry::Normal(stack_frames_for_assertions[3].clone()),
]
);
// Toggle filter twice
stack_frame_list
.toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
stack_frame_list
.toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
// Verify entries remain expanded
assert_eq!(
stack_frame_list.entries(),
&vec![
StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()),
StackFrameEntry::Normal(stack_frames_for_assertions[1].clone()),
StackFrameEntry::Normal(stack_frames_for_assertions[2].clone()),
StackFrameEntry::Normal(stack_frames_for_assertions[3].clone()),
],
"Expanded entries should remain expanded after toggling filter"
);
});
});
}

View File

@@ -268,7 +268,7 @@ pub async fn stream_completion(
.method(Method::POST)
.uri(uri)
.header("Content-Type", "application/json")
.header("Authorization", format!("Bearer {}", api_key.trim()));
.header("Authorization", format!("Bearer {}", api_key));
let request = request_builder.body(AsyncBody::from(serde_json::to_string(&request)?))?;
let mut response = client.send(request).await?;

View File

@@ -753,8 +753,6 @@ actions!(
UniqueLinesCaseInsensitive,
/// Removes duplicate lines (case-sensitive).
UniqueLinesCaseSensitive,
UnwrapSyntaxNode,
/// Wraps selections in tag specified by language.
WrapSelectionsInTag
UnwrapSyntaxNode
]
);

View File

@@ -2588,7 +2588,7 @@ impl Editor {
|| binding
.keystrokes()
.first()
.is_some_and(|keystroke| keystroke.modifiers().modified())
.is_some_and(|keystroke| keystroke.display_modifiers.modified())
}))
}
@@ -7686,16 +7686,16 @@ impl Editor {
.keystroke()
{
modifiers_held = modifiers_held
|| (accept_keystroke.modifiers() == modifiers
&& accept_keystroke.modifiers().modified());
|| (&accept_keystroke.display_modifiers == modifiers
&& accept_keystroke.display_modifiers.modified());
};
if let Some(accept_partial_keystroke) = self
.accept_edit_prediction_keybind(true, window, cx)
.keystroke()
{
modifiers_held = modifiers_held
|| (accept_partial_keystroke.modifiers() == modifiers
&& accept_partial_keystroke.modifiers().modified());
|| (&accept_partial_keystroke.display_modifiers == modifiers
&& accept_partial_keystroke.display_modifiers.modified());
}
if modifiers_held {
@@ -9044,7 +9044,7 @@ impl Editor {
let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac;
let modifiers_color = if *accept_keystroke.modifiers() == window.modifiers() {
let modifiers_color = if accept_keystroke.display_modifiers == window.modifiers() {
Color::Accent
} else {
Color::Muted
@@ -9056,19 +9056,19 @@ impl Editor {
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
.text_size(TextSize::XSmall.rems(cx))
.child(h_flex().children(ui::render_modifiers(
accept_keystroke.modifiers(),
&accept_keystroke.display_modifiers,
PlatformStyle::platform(),
Some(modifiers_color),
Some(IconSize::XSmall.rems().into()),
true,
)))
.when(is_platform_style_mac, |parent| {
parent.child(accept_keystroke.key().to_string())
parent.child(accept_keystroke.display_key.clone())
})
.when(!is_platform_style_mac, |parent| {
parent.child(
Key::new(
util::capitalize(accept_keystroke.key()),
util::capitalize(&accept_keystroke.display_key),
Some(Color::Default),
)
.size(Some(IconSize::XSmall.rems().into())),
@@ -9249,7 +9249,7 @@ impl Editor {
accept_keystroke.as_ref(),
|el, accept_keystroke| {
el.child(h_flex().children(ui::render_modifiers(
accept_keystroke.modifiers(),
&accept_keystroke.display_modifiers,
PlatformStyle::platform(),
Some(Color::Default),
Some(IconSize::XSmall.rems().into()),
@@ -9319,7 +9319,7 @@ impl Editor {
.child(completion),
)
.when_some(accept_keystroke, |el, accept_keystroke| {
if !accept_keystroke.modifiers().modified() {
if !accept_keystroke.display_modifiers.modified() {
return el;
}
@@ -9338,7 +9338,7 @@ impl Editor {
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
.when(is_platform_style_mac, |parent| parent.gap_1())
.child(h_flex().children(ui::render_modifiers(
accept_keystroke.modifiers(),
&accept_keystroke.display_modifiers,
PlatformStyle::platform(),
Some(if !has_completion {
Color::Muted
@@ -10447,86 +10447,6 @@ impl Editor {
})
}
fn enable_wrap_selections_in_tag(&self, cx: &App) -> bool {
let snapshot = self.buffer.read(cx).snapshot(cx);
for selection in self.selections.disjoint_anchors().iter() {
if snapshot
.language_at(selection.start)
.and_then(|lang| lang.config().wrap_characters.as_ref())
.is_some()
{
return true;
}
}
false
}
fn wrap_selections_in_tag(
&mut self,
_: &WrapSelectionsInTag,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
let snapshot = self.buffer.read(cx).snapshot(cx);
let mut edits = Vec::new();
let mut boundaries = Vec::new();
for selection in self.selections.all::<Point>(cx).iter() {
let Some(wrap_config) = snapshot
.language_at(selection.start)
.and_then(|lang| lang.config().wrap_characters.clone())
else {
continue;
};
let open_tag = format!("{}{}", wrap_config.start_prefix, wrap_config.start_suffix);
let close_tag = format!("{}{}", wrap_config.end_prefix, wrap_config.end_suffix);
let start_before = snapshot.anchor_before(selection.start);
let end_after = snapshot.anchor_after(selection.end);
edits.push((start_before..start_before, open_tag));
edits.push((end_after..end_after, close_tag));
boundaries.push((
start_before,
end_after,
wrap_config.start_prefix.len(),
wrap_config.end_suffix.len(),
));
}
if edits.is_empty() {
return;
}
self.transact(window, cx, |this, window, cx| {
let buffer = this.buffer.update(cx, |buffer, cx| {
buffer.edit(edits, None, cx);
buffer.snapshot(cx)
});
let mut new_selections = Vec::with_capacity(boundaries.len() * 2);
for (start_before, end_after, start_prefix_len, end_suffix_len) in
boundaries.into_iter()
{
let open_offset = start_before.to_offset(&buffer) + start_prefix_len;
let close_offset = end_after.to_offset(&buffer).saturating_sub(end_suffix_len);
new_selections.push(open_offset..open_offset);
new_selections.push(close_offset..close_offset);
}
this.change_selections(Default::default(), window, cx, |s| {
s.select_ranges(new_selections);
});
this.request_autoscroll(Autoscroll::fit(), cx);
});
}
pub fn reload_file(&mut self, _: &ReloadFile, window: &mut Window, cx: &mut Context<Self>) {
let Some(project) = self.project.clone() else {
return;

View File

@@ -4403,129 +4403,6 @@ async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
"});
}
#[gpui::test]
async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
let js_language = Arc::new(Language::new(
LanguageConfig {
name: "JavaScript".into(),
wrap_characters: Some(language::WrapCharactersConfig {
start_prefix: "<".into(),
start_suffix: ">".into(),
end_prefix: "</".into(),
end_suffix: ">".into(),
}),
..LanguageConfig::default()
},
None,
));
cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
cx.set_state(indoc! {"
«testˇ»
"});
cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
cx.assert_editor_state(indoc! {"
<«ˇ»>test</«ˇ»>
"});
cx.set_state(indoc! {"
«test
testˇ»
"});
cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
cx.assert_editor_state(indoc! {"
<«ˇ»>test
test</«ˇ»>
"});
cx.set_state(indoc! {"
teˇst
"});
cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
cx.assert_editor_state(indoc! {"
te<«ˇ»></«ˇ»>st
"});
}
#[gpui::test]
async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
let js_language = Arc::new(Language::new(
LanguageConfig {
name: "JavaScript".into(),
wrap_characters: Some(language::WrapCharactersConfig {
start_prefix: "<".into(),
start_suffix: ">".into(),
end_prefix: "</".into(),
end_suffix: ">".into(),
}),
..LanguageConfig::default()
},
None,
));
cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
cx.set_state(indoc! {"
«testˇ»
«testˇ» «testˇ»
«testˇ»
"});
cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
cx.assert_editor_state(indoc! {"
<«ˇ»>test</«ˇ»>
<«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
<«ˇ»>test</«ˇ»>
"});
cx.set_state(indoc! {"
«test
testˇ»
«test
testˇ»
"});
cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
cx.assert_editor_state(indoc! {"
<«ˇ»>test
test</«ˇ»>
<«ˇ»>test
test</«ˇ»>
"});
}
#[gpui::test]
async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
let plaintext_language = Arc::new(Language::new(
LanguageConfig {
name: "Plain Text".into(),
..LanguageConfig::default()
},
None,
));
cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
cx.set_state(indoc! {"
«testˇ»
"});
cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
cx.assert_editor_state(indoc! {"
«testˇ»
"});
}
#[gpui::test]
async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
init_test(cx, |_| {});

View File

@@ -585,9 +585,6 @@ impl EditorElement {
register_action(editor, window, Editor::edit_log_breakpoint);
register_action(editor, window, Editor::enable_breakpoint);
register_action(editor, window, Editor::disable_breakpoint);
if editor.read(cx).enable_wrap_selections_in_tag(cx) {
register_action(editor, window, Editor::wrap_selections_in_tag);
}
}
fn register_key_listeners(&self, window: &mut Window, _: &mut App, layout: &EditorLayout) {

View File

@@ -495,8 +495,7 @@ impl Fs for RealFs {
};
// todo(windows)
// When new version of `windows-rs` release, make this operation `async`
let path = path.canonicalize()?;
let path = SanitizedPath::new(&path);
let path = SanitizedPath::from(path.canonicalize()?);
let path_string = path.to_string();
let file = StorageFile::GetFileFromPathAsync(&HSTRING::from(path_string))?.get()?;
file.DeleteAsync(StorageDeleteOption::Default)?.get()?;
@@ -523,8 +522,7 @@ impl Fs for RealFs {
// todo(windows)
// When new version of `windows-rs` release, make this operation `async`
let path = path.canonicalize()?;
let path = SanitizedPath::new(&path);
let path = SanitizedPath::from(path.canonicalize()?);
let path_string = path.to_string();
let folder = StorageFolder::GetFolderFromPathAsync(&HSTRING::from(path_string))?.get()?;
folder.DeleteAsync(StorageDeleteOption::Default)?.get()?;
@@ -785,7 +783,7 @@ impl Fs for RealFs {
{
target = parent.join(target);
if let Ok(canonical) = self.canonicalize(&target).await {
target = SanitizedPath::new(&canonical).as_path().to_path_buf();
target = SanitizedPath::from(canonical).as_path().to_path_buf();
}
}
watcher.add(&target).ok();

View File

@@ -42,7 +42,7 @@ impl Drop for FsWatcher {
impl Watcher for FsWatcher {
fn add(&self, path: &std::path::Path) -> anyhow::Result<()> {
let root_path = SanitizedPath::new_arc(path);
let root_path = SanitizedPath::from(path);
let tx = self.tx.clone();
let pending_paths = self.pending_path_events.clone();
@@ -70,7 +70,7 @@ impl Watcher for FsWatcher {
.paths
.iter()
.filter_map(|event_path| {
let event_path = SanitizedPath::new(event_path);
let event_path = SanitizedPath::from(event_path);
event_path.starts_with(&root_path).then(|| PathEvent {
path: event_path.as_path().to_path_buf(),
kind,

View File

@@ -13,7 +13,6 @@ pub async fn stream_generate_content(
api_key: &str,
mut request: GenerateContentRequest,
) -> Result<BoxStream<'static, Result<GenerateContentResponse>>> {
let api_key = api_key.trim();
validate_generate_content_request(&request)?;
// The `model` field is emptied as it is provided as a path parameter.

View File

@@ -152,36 +152,6 @@ impl Render for WindowDemo {
)
.unwrap();
}))
.child(button("Unresizable", move |_, cx| {
cx.open_window(
WindowOptions {
is_resizable: false,
window_bounds: Some(window_bounds),
..Default::default()
},
|_, cx| {
cx.new(|_| SubWindow {
custom_titlebar: false,
})
},
)
.unwrap();
}))
.child(button("Unminimizable", move |_, cx| {
cx.open_window(
WindowOptions {
is_minimizable: false,
window_bounds: Some(window_bounds),
..Default::default()
},
|_, cx| {
cx.new(|_| SubWindow {
custom_titlebar: false,
})
},
)
.unwrap();
}))
.child(button("Hide Application", |window, cx| {
cx.hide();

View File

@@ -62,8 +62,6 @@ fn build_window_options(display_id: DisplayId, bounds: Bounds<Pixels>) -> Window
app_id: None,
window_min_size: None,
window_decorations: None,
tabbing_identifier: None,
..Default::default()
}
}

View File

@@ -7,7 +7,7 @@ use std::{
path::{Path, PathBuf},
rc::{Rc, Weak},
sync::{Arc, atomic::Ordering::SeqCst},
time::{Duration, Instant},
time::Duration,
};
use anyhow::{Context as _, Result, anyhow};
@@ -17,7 +17,6 @@ use futures::{
channel::oneshot,
future::{LocalBoxFuture, Shared},
};
use itertools::Itertools;
use parking_lot::RwLock;
use slotmap::SlotMap;
@@ -40,8 +39,8 @@ use crate::{
Keymap, Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform,
PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, Point, PromptBuilder,
PromptButton, PromptHandle, PromptLevel, Render, RenderImage, RenderablePromptHandle,
Reservation, ScreenCaptureSource, SharedString, SubscriberSet, Subscription, SvgRenderer, Task,
TextSystem, Window, WindowAppearance, WindowHandle, WindowId, WindowInvalidator,
Reservation, ScreenCaptureSource, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem,
Window, WindowAppearance, WindowHandle, WindowId, WindowInvalidator,
colors::{Colors, GlobalColors},
current_platform, hash, init_app_menus,
};
@@ -238,303 +237,6 @@ type WindowClosedHandler = Box<dyn FnMut(&mut App)>;
type ReleaseListener = Box<dyn FnOnce(&mut dyn Any, &mut App) + 'static>;
type NewEntityListener = Box<dyn FnMut(AnyEntity, &mut Option<&mut Window>, &mut App) + 'static>;
#[doc(hidden)]
#[derive(Clone, PartialEq, Eq)]
pub struct SystemWindowTab {
pub id: WindowId,
pub title: SharedString,
pub handle: AnyWindowHandle,
pub last_active_at: Instant,
}
impl SystemWindowTab {
/// Create a new instance of the window tab.
pub fn new(title: SharedString, handle: AnyWindowHandle) -> Self {
Self {
id: handle.id,
title,
handle,
last_active_at: Instant::now(),
}
}
}
/// A controller for managing window tabs.
#[derive(Default)]
pub struct SystemWindowTabController {
visible: Option<bool>,
tab_groups: FxHashMap<usize, Vec<SystemWindowTab>>,
}
impl Global for SystemWindowTabController {}
impl SystemWindowTabController {
/// Create a new instance of the window tab controller.
pub fn new() -> Self {
Self {
visible: None,
tab_groups: FxHashMap::default(),
}
}
/// Initialize the global window tab controller.
pub fn init(cx: &mut App) {
cx.set_global(SystemWindowTabController::new());
}
/// Get all tab groups.
pub fn tab_groups(&self) -> &FxHashMap<usize, Vec<SystemWindowTab>> {
&self.tab_groups
}
/// Get the next tab group window handle.
pub fn get_next_tab_group_window(cx: &mut App, id: WindowId) -> Option<&AnyWindowHandle> {
let controller = cx.global::<SystemWindowTabController>();
let current_group = controller
.tab_groups
.iter()
.find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| group));
let current_group = current_group?;
let mut group_ids: Vec<_> = controller.tab_groups.keys().collect();
let idx = group_ids.iter().position(|g| *g == current_group)?;
let next_idx = (idx + 1) % group_ids.len();
controller
.tab_groups
.get(group_ids[next_idx])
.and_then(|tabs| {
tabs.iter()
.max_by_key(|tab| tab.last_active_at)
.or_else(|| tabs.first())
.map(|tab| &tab.handle)
})
}
/// Get the previous tab group window handle.
pub fn get_prev_tab_group_window(cx: &mut App, id: WindowId) -> Option<&AnyWindowHandle> {
let controller = cx.global::<SystemWindowTabController>();
let current_group = controller
.tab_groups
.iter()
.find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| group));
let current_group = current_group?;
let mut group_ids: Vec<_> = controller.tab_groups.keys().collect();
let idx = group_ids.iter().position(|g| *g == current_group)?;
let prev_idx = if idx == 0 {
group_ids.len() - 1
} else {
idx - 1
};
controller
.tab_groups
.get(group_ids[prev_idx])
.and_then(|tabs| {
tabs.iter()
.max_by_key(|tab| tab.last_active_at)
.or_else(|| tabs.first())
.map(|tab| &tab.handle)
})
}
/// Get all tabs in the same window.
pub fn tabs(&self, id: WindowId) -> Option<&Vec<SystemWindowTab>> {
let tab_group = self
.tab_groups
.iter()
.find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| *group));
if let Some(tab_group) = tab_group {
self.tab_groups.get(&tab_group)
} else {
None
}
}
/// Initialize the visibility of the system window tab controller.
pub fn init_visible(cx: &mut App, visible: bool) {
let mut controller = cx.global_mut::<SystemWindowTabController>();
if controller.visible.is_none() {
controller.visible = Some(visible);
}
}
/// Get the visibility of the system window tab controller.
pub fn is_visible(&self) -> bool {
self.visible.unwrap_or(false)
}
/// Set the visibility of the system window tab controller.
pub fn set_visible(cx: &mut App, visible: bool) {
let mut controller = cx.global_mut::<SystemWindowTabController>();
controller.visible = Some(visible);
}
/// Update the last active of a window.
pub fn update_last_active(cx: &mut App, id: WindowId) {
let mut controller = cx.global_mut::<SystemWindowTabController>();
for windows in controller.tab_groups.values_mut() {
for tab in windows.iter_mut() {
if tab.id == id {
tab.last_active_at = Instant::now();
}
}
}
}
/// Update the position of a tab within its group.
pub fn update_tab_position(cx: &mut App, id: WindowId, ix: usize) {
let mut controller = cx.global_mut::<SystemWindowTabController>();
for (_, windows) in controller.tab_groups.iter_mut() {
if let Some(current_pos) = windows.iter().position(|tab| tab.id == id) {
if ix < windows.len() && current_pos != ix {
let window_tab = windows.remove(current_pos);
windows.insert(ix, window_tab);
}
break;
}
}
}
/// Update the title of a tab.
pub fn update_tab_title(cx: &mut App, id: WindowId, title: SharedString) {
let controller = cx.global::<SystemWindowTabController>();
let tab = controller
.tab_groups
.values()
.flat_map(|windows| windows.iter())
.find(|tab| tab.id == id);
if tab.map_or(true, |t| t.title == title) {
return;
}
let mut controller = cx.global_mut::<SystemWindowTabController>();
for windows in controller.tab_groups.values_mut() {
for tab in windows.iter_mut() {
if tab.id == id {
tab.title = title.clone();
}
}
}
}
/// Insert a tab into a tab group.
pub fn add_tab(cx: &mut App, id: WindowId, tabs: Vec<SystemWindowTab>) {
let mut controller = cx.global_mut::<SystemWindowTabController>();
let Some(tab) = tabs.clone().into_iter().find(|tab| tab.id == id) else {
return;
};
let mut expected_tab_ids: Vec<_> = tabs
.iter()
.filter(|tab| tab.id != id)
.map(|tab| tab.id)
.sorted()
.collect();
let mut tab_group_id = None;
for (group_id, group_tabs) in &controller.tab_groups {
let tab_ids: Vec<_> = group_tabs.iter().map(|tab| tab.id).sorted().collect();
if tab_ids == expected_tab_ids {
tab_group_id = Some(*group_id);
break;
}
}
if let Some(tab_group_id) = tab_group_id {
if let Some(tabs) = controller.tab_groups.get_mut(&tab_group_id) {
tabs.push(tab);
}
} else {
let new_group_id = controller.tab_groups.len();
controller.tab_groups.insert(new_group_id, tabs);
}
}
/// Remove a tab from a tab group.
pub fn remove_tab(cx: &mut App, id: WindowId) -> Option<SystemWindowTab> {
let mut controller = cx.global_mut::<SystemWindowTabController>();
let mut removed_tab = None;
controller.tab_groups.retain(|_, tabs| {
if let Some(pos) = tabs.iter().position(|tab| tab.id == id) {
removed_tab = Some(tabs.remove(pos));
}
!tabs.is_empty()
});
removed_tab
}
/// Move a tab to a new tab group.
pub fn move_tab_to_new_window(cx: &mut App, id: WindowId) {
let mut removed_tab = Self::remove_tab(cx, id);
let mut controller = cx.global_mut::<SystemWindowTabController>();
if let Some(tab) = removed_tab {
let new_group_id = controller.tab_groups.keys().max().map_or(0, |k| k + 1);
controller.tab_groups.insert(new_group_id, vec![tab]);
}
}
/// Merge all tab groups into a single group.
pub fn merge_all_windows(cx: &mut App, id: WindowId) {
let mut controller = cx.global_mut::<SystemWindowTabController>();
let Some(initial_tabs) = controller.tabs(id) else {
return;
};
let mut all_tabs = initial_tabs.clone();
for tabs in controller.tab_groups.values() {
all_tabs.extend(
tabs.iter()
.filter(|tab| !initial_tabs.contains(tab))
.cloned(),
);
}
controller.tab_groups.clear();
controller.tab_groups.insert(0, all_tabs);
}
/// Selects the next tab in the tab group in the trailing direction.
pub fn select_next_tab(cx: &mut App, id: WindowId) {
let mut controller = cx.global_mut::<SystemWindowTabController>();
let Some(tabs) = controller.tabs(id) else {
return;
};
let current_index = tabs.iter().position(|tab| tab.id == id).unwrap();
let next_index = (current_index + 1) % tabs.len();
let _ = &tabs[next_index].handle.update(cx, |_, window, _| {
window.activate_window();
});
}
/// Selects the previous tab in the tab group in the leading direction.
pub fn select_previous_tab(cx: &mut App, id: WindowId) {
let mut controller = cx.global_mut::<SystemWindowTabController>();
let Some(tabs) = controller.tabs(id) else {
return;
};
let current_index = tabs.iter().position(|tab| tab.id == id).unwrap();
let previous_index = if current_index == 0 {
tabs.len() - 1
} else {
current_index - 1
};
let _ = &tabs[previous_index].handle.update(cx, |_, window, _| {
window.activate_window();
});
}
}
/// Contains the state of the full application, and passed as a reference to a variety of callbacks.
/// Other [Context] derefs to this type.
/// You need a reference to an `App` to access the state of a [Entity].
@@ -670,7 +372,6 @@ impl App {
});
init_app_menus(platform.as_ref(), &app.borrow());
SystemWindowTabController::init(&mut app.borrow_mut());
platform.on_keyboard_layout_change(Box::new({
let app = Rc::downgrade(&app);

View File

@@ -638,7 +638,7 @@ mod tests {
fn assert_bindings(keymap: &Keymap, action: &dyn Action, expected: &[&str]) {
let actual = keymap
.bindings_for_action(action)
.map(|binding| binding.keystrokes[0].inner().unparse())
.map(|binding| binding.keystrokes[0].inner.unparse())
.collect::<Vec<_>>();
assert_eq!(actual, expected, "{:?}", action);
}

View File

@@ -57,7 +57,7 @@ impl KeyBinding {
.split_whitespace()
.map(|source| {
let keystroke = Keystroke::parse(source)?;
Ok(KeybindingKeystroke::new_with_mapper(
Ok(KeybindingKeystroke::new(
keystroke,
use_key_equivalents,
keyboard_mapper,

View File

@@ -40,8 +40,8 @@ use crate::{
DEFAULT_WINDOW_SIZE, DevicePixels, DispatchEventResult, Font, FontId, FontMetrics, FontRun,
ForegroundExecutor, GlyphId, GpuSpecs, ImageSource, Keymap, LineLayout, Pixels, PlatformInput,
Point, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, ScaledPixels, Scene,
ShapedGlyph, ShapedRun, SharedString, Size, SvgRenderer, SvgSize, SystemWindowTab, Task,
TaskLabel, Window, WindowControlArea, hash, point, px, size,
ShapedGlyph, ShapedRun, SharedString, Size, SvgRenderer, SvgSize, Task, TaskLabel, Window,
WindowControlArea, hash, point, px, size,
};
use anyhow::Result;
use async_task::Runnable;
@@ -502,26 +502,9 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
// macOS specific methods
fn get_title(&self) -> String {
String::new()
}
fn tabbed_windows(&self) -> Option<Vec<SystemWindowTab>> {
None
}
fn tab_bar_visible(&self) -> bool {
false
}
fn set_edited(&mut self, _edited: bool) {}
fn show_character_palette(&self) {}
fn titlebar_double_click(&self) {}
fn on_move_tab_to_new_window(&self, _callback: Box<dyn FnMut()>) {}
fn on_merge_all_windows(&self, _callback: Box<dyn FnMut()>) {}
fn on_select_previous_tab(&self, _callback: Box<dyn FnMut()>) {}
fn on_select_next_tab(&self, _callback: Box<dyn FnMut()>) {}
fn on_toggle_tab_bar(&self, _callback: Box<dyn FnMut()>) {}
fn merge_all_windows(&self) {}
fn move_tab_to_new_window(&self) {}
fn toggle_window_tab_overview(&self) {}
#[cfg(target_os = "windows")]
fn get_raw_handle(&self) -> windows::HWND;
@@ -1108,12 +1091,6 @@ pub struct WindowOptions {
/// Whether the window should be movable by the user
pub is_movable: bool,
/// Whether the window should be resizable by the user
pub is_resizable: bool,
/// Whether the window should be minimized by the user
pub is_minimizable: bool,
/// The display to create the window on, if this is None,
/// the window will be created on the main display
pub display_id: Option<DisplayId>,
@@ -1130,9 +1107,6 @@ pub struct WindowOptions {
/// Whether to use client or server side decorations. Wayland only
/// Note that this may be ignored.
pub window_decorations: Option<WindowDecorations>,
/// Tab group name, allows opening the window as a native tab on macOS 10.12+. Windows with the same tabbing identifier will be grouped together.
pub tabbing_identifier: Option<String>,
}
/// The variables that can be configured when creating a new window
@@ -1159,14 +1133,6 @@ pub(crate) struct WindowParams {
#[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
pub is_movable: bool,
/// Whether the window should be resizable by the user
#[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
pub is_resizable: bool,
/// Whether the window should be minimized by the user
#[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
pub is_minimizable: bool,
#[cfg_attr(
any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
allow(dead_code)
@@ -1180,8 +1146,6 @@ pub(crate) struct WindowParams {
pub display_id: Option<DisplayId>,
pub window_min_size: Option<Size<Pixels>>,
#[cfg(target_os = "macos")]
pub tabbing_identifier: Option<String>,
}
/// Represents the status of how a window should be opened.
@@ -1227,14 +1191,11 @@ impl Default for WindowOptions {
show: true,
kind: WindowKind::Normal,
is_movable: true,
is_resizable: true,
is_minimizable: true,
display_id: None,
window_background: WindowBackgroundAppearance::default(),
app_id: None,
window_min_size: None,
window_decorations: None,
tabbing_identifier: None,
}
}
}

View File

@@ -36,13 +36,11 @@ pub struct Keystroke {
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct KeybindingKeystroke {
/// The GPUI representation of the keystroke.
inner: Keystroke,
pub inner: Keystroke,
/// The modifiers to display.
#[cfg(target_os = "windows")]
display_modifiers: Modifiers,
pub display_modifiers: Modifiers,
/// The key to display.
#[cfg(target_os = "windows")]
display_key: String,
pub display_key: String,
}
/// Error type for `Keystroke::parse`. This is used instead of `anyhow::Error` so that Zed can use
@@ -264,17 +262,8 @@ impl Keystroke {
}
impl KeybindingKeystroke {
#[cfg(target_os = "windows")]
pub(crate) fn new(inner: Keystroke, display_modifiers: Modifiers, display_key: String) -> Self {
KeybindingKeystroke {
inner,
display_modifiers,
display_key,
}
}
/// Create a new keybinding keystroke from the given keystroke using the given keyboard mapper.
pub fn new_with_mapper(
/// Create a new keybinding keystroke from the given keystroke
pub fn new(
inner: Keystroke,
use_key_equivalents: bool,
keyboard_mapper: &dyn PlatformKeyboardMapper,
@@ -282,95 +271,19 @@ impl KeybindingKeystroke {
keyboard_mapper.map_key_equivalent(inner, use_key_equivalents)
}
/// Create a new keybinding keystroke from the given keystroke, without any platform-specific mapping.
pub fn from_keystroke(keystroke: Keystroke) -> Self {
#[cfg(target_os = "windows")]
{
let key = keystroke.key.clone();
let modifiers = keystroke.modifiers;
KeybindingKeystroke {
inner: keystroke,
display_modifiers: modifiers,
display_key: key,
}
pub(crate) fn from_keystroke(keystroke: Keystroke) -> Self {
let key = keystroke.key.clone();
let modifiers = keystroke.modifiers;
KeybindingKeystroke {
inner: keystroke,
display_modifiers: modifiers,
display_key: key,
}
#[cfg(not(target_os = "windows"))]
{
KeybindingKeystroke { inner: keystroke }
}
}
/// Returns the GPUI representation of the keystroke.
pub fn inner(&self) -> &Keystroke {
&self.inner
}
/// Returns the modifiers.
///
/// Platform-specific behavior:
/// - On macOS and Linux, this modifiers is the same as `inner.modifiers`, which is the GPUI representation of the keystroke.
/// - On Windows, this modifiers is the display modifiers, for example, a `ctrl-@` keystroke will have `inner.modifiers` as
/// `Modifiers::control()` and `display_modifiers` as `Modifiers::control_shift()`.
pub fn modifiers(&self) -> &Modifiers {
#[cfg(target_os = "windows")]
{
&self.display_modifiers
}
#[cfg(not(target_os = "windows"))]
{
&self.inner.modifiers
}
}
/// Returns the key.
///
/// Platform-specific behavior:
/// - On macOS and Linux, this key is the same as `inner.key`, which is the GPUI representation of the keystroke.
/// - On Windows, this key is the display key, for example, a `ctrl-@` keystroke will have `inner.key` as `@` and `display_key` as `2`.
pub fn key(&self) -> &str {
#[cfg(target_os = "windows")]
{
&self.display_key
}
#[cfg(not(target_os = "windows"))]
{
&self.inner.key
}
}
/// Sets the modifiers. On Windows this modifies both `inner.modifiers` and `display_modifiers`.
pub fn set_modifiers(&mut self, modifiers: Modifiers) {
self.inner.modifiers = modifiers;
#[cfg(target_os = "windows")]
{
self.display_modifiers = modifiers;
}
}
/// Sets the key. On Windows this modifies both `inner.key` and `display_key`.
pub fn set_key(&mut self, key: String) {
#[cfg(target_os = "windows")]
{
self.display_key = key.clone();
}
self.inner.key = key;
}
/// Produces a representation of this key that Parse can understand.
pub fn unparse(&self) -> String {
#[cfg(target_os = "windows")]
{
unparse(&self.display_modifiers, &self.display_key)
}
#[cfg(not(target_os = "windows"))]
{
unparse(&self.inner.modifiers, &self.inner.key)
}
}
/// Removes the key_char
pub fn remove_key_char(&mut self) {
self.inner.key_char = None;
unparse(&self.display_modifiers, &self.display_key)
}
}
@@ -437,8 +350,8 @@ impl std::fmt::Display for Keystroke {
impl std::fmt::Display for KeybindingKeystroke {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
display_modifiers(self.modifiers(), f)?;
display_key(self.key(), f)
display_modifiers(&self.display_modifiers, f)?;
display_key(&self.display_key, f)
}
}

View File

@@ -354,19 +354,19 @@ impl MacPlatform {
let mut mask = NSEventModifierFlags::empty();
for (modifier, flag) in &[
(
keystroke.modifiers().platform,
keystroke.display_modifiers.platform,
NSEventModifierFlags::NSCommandKeyMask,
),
(
keystroke.modifiers().control,
keystroke.display_modifiers.control,
NSEventModifierFlags::NSControlKeyMask,
),
(
keystroke.modifiers().alt,
keystroke.display_modifiers.alt,
NSEventModifierFlags::NSAlternateKeyMask,
),
(
keystroke.modifiers().shift,
keystroke.display_modifiers.shift,
NSEventModifierFlags::NSShiftKeyMask,
),
] {
@@ -379,7 +379,7 @@ impl MacPlatform {
.initWithTitle_action_keyEquivalent_(
ns_string(name),
selector,
ns_string(key_to_native(keystroke.key()).as_ref()),
ns_string(key_to_native(&keystroke.display_key).as_ref()),
)
.autorelease();
if Self::os_version() >= SemanticVersion::new(12, 0, 0) {

View File

@@ -4,10 +4,8 @@ use crate::{
ForegroundExecutor, KeyDownEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton,
MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay,
PlatformInput, PlatformWindow, Point, PromptButton, PromptLevel, RequestFrameOptions,
ScaledPixels, SharedString, Size, SystemWindowTab, Timer, WindowAppearance,
WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowKind, WindowParams,
dispatch_get_main_queue, dispatch_sys::dispatch_async_f, platform::PlatformInputHandler, point,
px, size,
ScaledPixels, Size, Timer, WindowAppearance, WindowBackgroundAppearance, WindowBounds,
WindowControlArea, WindowKind, WindowParams, platform::PlatformInputHandler, point, px, size,
};
use block::ConcreteBlock;
use cocoa::{
@@ -26,7 +24,6 @@ use cocoa::{
NSUserDefaults,
},
};
use core_graphics::display::{CGDirectDisplayID, CGPoint, CGRect};
use ctor::ctor;
use futures::channel::oneshot;
@@ -85,12 +82,6 @@ type NSDragOperation = NSUInteger;
const NSDragOperationNone: NSDragOperation = 0;
#[allow(non_upper_case_globals)]
const NSDragOperationCopy: NSDragOperation = 1;
#[derive(PartialEq)]
pub enum UserTabbingPreference {
Never,
Always,
InFullScreen,
}
#[link(name = "CoreGraphics", kind = "framework")]
unsafe extern "C" {
@@ -352,36 +343,6 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C
conclude_drag_operation as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(addTitlebarAccessoryViewController:),
add_titlebar_accessory_view_controller as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(moveTabToNewWindow:),
move_tab_to_new_window as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(mergeAllWindows:),
merge_all_windows as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(selectNextTab:),
select_next_tab as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(selectPreviousTab:),
select_previous_tab as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(toggleTabBar:),
toggle_tab_bar as extern "C" fn(&Object, Sel, id),
);
decl.register()
}
}
@@ -414,11 +375,6 @@ struct MacWindowState {
// Whether the next left-mouse click is also the focusing click.
first_mouse: bool,
fullscreen_restore_bounds: Bounds<Pixels>,
move_tab_to_new_window_callback: Option<Box<dyn FnMut()>>,
merge_all_windows_callback: Option<Box<dyn FnMut()>>,
select_next_tab_callback: Option<Box<dyn FnMut()>>,
select_previous_tab_callback: Option<Box<dyn FnMut()>>,
toggle_tab_bar_callback: Option<Box<dyn FnMut()>>,
}
impl MacWindowState {
@@ -574,13 +530,10 @@ impl MacWindow {
titlebar,
kind,
is_movable,
is_resizable,
is_minimizable,
focus,
show,
display_id,
window_min_size,
tabbing_identifier,
}: WindowParams,
executor: ForegroundExecutor,
renderer_context: renderer::Context,
@@ -588,25 +541,14 @@ impl MacWindow {
unsafe {
let pool = NSAutoreleasePool::new(nil);
let allows_automatic_window_tabbing = tabbing_identifier.is_some();
if allows_automatic_window_tabbing {
let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: YES];
} else {
let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: NO];
}
let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: NO];
let mut style_mask;
if let Some(titlebar) = titlebar.as_ref() {
style_mask =
NSWindowStyleMask::NSClosableWindowMask | NSWindowStyleMask::NSTitledWindowMask;
if is_resizable {
style_mask |= NSWindowStyleMask::NSResizableWindowMask;
}
if is_minimizable {
style_mask |= NSWindowStyleMask::NSMiniaturizableWindowMask;
}
style_mask = NSWindowStyleMask::NSClosableWindowMask
| NSWindowStyleMask::NSMiniaturizableWindowMask
| NSWindowStyleMask::NSResizableWindowMask
| NSWindowStyleMask::NSTitledWindowMask;
if titlebar.appears_transparent {
style_mask |= NSWindowStyleMask::NSFullSizeContentViewWindowMask;
@@ -718,11 +660,6 @@ impl MacWindow {
external_files_dragged: false,
first_mouse: false,
fullscreen_restore_bounds: Bounds::default(),
move_tab_to_new_window_callback: None,
merge_all_windows_callback: None,
select_next_tab_callback: None,
select_previous_tab_callback: None,
toggle_tab_bar_callback: None,
})));
(*native_window).set_ivar(
@@ -777,11 +714,6 @@ impl MacWindow {
WindowKind::Normal => {
native_window.setLevel_(NSNormalWindowLevel);
native_window.setAcceptsMouseMovedEvents_(YES);
if let Some(tabbing_identifier) = tabbing_identifier {
let tabbing_id = NSString::alloc(nil).init_str(tabbing_identifier.as_str());
let _: () = msg_send![native_window, setTabbingIdentifier: tabbing_id];
}
}
WindowKind::PopUp => {
// Use a tracking area to allow receiving MouseMoved events even when
@@ -810,38 +742,6 @@ impl MacWindow {
}
}
let app = NSApplication::sharedApplication(nil);
let main_window: id = msg_send![app, mainWindow];
if allows_automatic_window_tabbing
&& !main_window.is_null()
&& main_window != native_window
{
let main_window_is_fullscreen = main_window
.styleMask()
.contains(NSWindowStyleMask::NSFullScreenWindowMask);
let user_tabbing_preference = Self::get_user_tabbing_preference()
.unwrap_or(UserTabbingPreference::InFullScreen);
let should_add_as_tab = user_tabbing_preference == UserTabbingPreference::Always
|| user_tabbing_preference == UserTabbingPreference::InFullScreen
&& main_window_is_fullscreen;
if should_add_as_tab {
let main_window_can_tab: BOOL =
msg_send![main_window, respondsToSelector: sel!(addTabbedWindow:ordered:)];
let main_window_visible: BOOL = msg_send![main_window, isVisible];
if main_window_can_tab == YES && main_window_visible == YES {
let _: () = msg_send![main_window, addTabbedWindow: native_window ordered: NSWindowOrderingMode::NSWindowAbove];
// Ensure the window is visible immediately after adding the tab, since the tab bar is updated with a new entry at this point.
// Note: Calling orderFront here can break fullscreen mode (makes fullscreen windows exit fullscreen), so only do this if the main window is not fullscreen.
if !main_window_is_fullscreen {
let _: () = msg_send![native_window, orderFront: nil];
}
}
}
}
if focus && show {
native_window.makeKeyAndOrderFront_(nil);
} else if show {
@@ -896,33 +796,6 @@ impl MacWindow {
window_handles
}
}
pub fn get_user_tabbing_preference() -> Option<UserTabbingPreference> {
unsafe {
let defaults: id = NSUserDefaults::standardUserDefaults();
let domain = NSString::alloc(nil).init_str("NSGlobalDomain");
let key = NSString::alloc(nil).init_str("AppleWindowTabbingMode");
let dict: id = msg_send![defaults, persistentDomainForName: domain];
let value: id = if !dict.is_null() {
msg_send![dict, objectForKey: key]
} else {
nil
};
let value_str = if !value.is_null() {
CStr::from_ptr(NSString::UTF8String(value)).to_string_lossy()
} else {
"".into()
};
match value_str.as_ref() {
"manual" => Some(UserTabbingPreference::Never),
"always" => Some(UserTabbingPreference::Always),
_ => Some(UserTabbingPreference::InFullScreen),
}
}
}
}
impl Drop for MacWindow {
@@ -978,46 +851,6 @@ impl PlatformWindow for MacWindow {
.detach();
}
fn merge_all_windows(&self) {
let native_window = self.0.lock().native_window;
unsafe extern "C" fn merge_windows_async(context: *mut std::ffi::c_void) {
let native_window = context as id;
let _: () = msg_send![native_window, mergeAllWindows:nil];
}
unsafe {
dispatch_async_f(
dispatch_get_main_queue(),
native_window as *mut std::ffi::c_void,
Some(merge_windows_async),
);
}
}
fn move_tab_to_new_window(&self) {
let native_window = self.0.lock().native_window;
unsafe extern "C" fn move_tab_async(context: *mut std::ffi::c_void) {
let native_window = context as id;
let _: () = msg_send![native_window, moveTabToNewWindow:nil];
let _: () = msg_send![native_window, makeKeyAndOrderFront: nil];
}
unsafe {
dispatch_async_f(
dispatch_get_main_queue(),
native_window as *mut std::ffi::c_void,
Some(move_tab_async),
);
}
}
fn toggle_window_tab_overview(&self) {
let native_window = self.0.lock().native_window;
unsafe {
let _: () = msg_send![native_window, toggleTabOverview:nil];
}
}
fn scale_factor(&self) -> f32 {
self.0.as_ref().lock().scale_factor()
}
@@ -1218,17 +1051,6 @@ impl PlatformWindow for MacWindow {
}
}
fn get_title(&self) -> String {
unsafe {
let title: id = msg_send![self.0.lock().native_window, title];
if title.is_null() {
"".to_string()
} else {
title.to_str().to_string()
}
}
}
fn set_app_id(&mut self, _app_id: &str) {}
fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
@@ -1390,62 +1212,6 @@ impl PlatformWindow for MacWindow {
self.0.lock().appearance_changed_callback = Some(callback);
}
fn tabbed_windows(&self) -> Option<Vec<SystemWindowTab>> {
unsafe {
let windows: id = msg_send![self.0.lock().native_window, tabbedWindows];
if windows.is_null() {
return None;
}
let count: NSUInteger = msg_send![windows, count];
let mut result = Vec::new();
for i in 0..count {
let window: id = msg_send![windows, objectAtIndex:i];
if msg_send![window, isKindOfClass: WINDOW_CLASS] {
let handle = get_window_state(&*window).lock().handle;
let title: id = msg_send![window, title];
let title = SharedString::from(title.to_str().to_string());
result.push(SystemWindowTab::new(title, handle));
}
}
Some(result)
}
}
fn tab_bar_visible(&self) -> bool {
unsafe {
let tab_group: id = msg_send![self.0.lock().native_window, tabGroup];
if tab_group.is_null() {
false
} else {
let tab_bar_visible: BOOL = msg_send![tab_group, isTabBarVisible];
tab_bar_visible == YES
}
}
}
fn on_move_tab_to_new_window(&self, callback: Box<dyn FnMut()>) {
self.0.as_ref().lock().move_tab_to_new_window_callback = Some(callback);
}
fn on_merge_all_windows(&self, callback: Box<dyn FnMut()>) {
self.0.as_ref().lock().merge_all_windows_callback = Some(callback);
}
fn on_select_next_tab(&self, callback: Box<dyn FnMut()>) {
self.0.as_ref().lock().select_next_tab_callback = Some(callback);
}
fn on_select_previous_tab(&self, callback: Box<dyn FnMut()>) {
self.0.as_ref().lock().select_previous_tab_callback = Some(callback);
}
fn on_toggle_tab_bar(&self, callback: Box<dyn FnMut()>) {
self.0.as_ref().lock().toggle_tab_bar_callback = Some(callback);
}
fn draw(&self, scene: &crate::Scene) {
let mut this = self.0.lock();
this.renderer.draw(scene);
@@ -1887,7 +1653,6 @@ extern "C" fn window_did_change_occlusion_state(this: &Object, _: Sel, _: id) {
.occlusionState()
.contains(NSWindowOcclusionState::NSWindowOcclusionStateVisible)
{
lock.move_traffic_light();
lock.start_display_link();
} else {
lock.stop_display_link();
@@ -1949,7 +1714,7 @@ extern "C" fn window_did_change_screen(this: &Object, _: Sel, _: id) {
extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id) {
let window_state = unsafe { get_window_state(this) };
let mut lock = window_state.lock();
let lock = window_state.lock();
let is_active = unsafe { lock.native_window.isKeyWindow() == YES };
// When opening a pop-up while the application isn't active, Cocoa sends a spurious
@@ -1970,34 +1735,9 @@ extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id)
let executor = lock.executor.clone();
drop(lock);
// If window is becoming active, trigger immediate synchronous frame request.
if selector == sel!(windowDidBecomeKey:) && is_active {
let window_state = unsafe { get_window_state(this) };
let mut lock = window_state.lock();
if let Some(mut callback) = lock.request_frame_callback.take() {
#[cfg(not(feature = "macos-blade"))]
lock.renderer.set_presents_with_transaction(true);
lock.stop_display_link();
drop(lock);
callback(Default::default());
let mut lock = window_state.lock();
lock.request_frame_callback = Some(callback);
#[cfg(not(feature = "macos-blade"))]
lock.renderer.set_presents_with_transaction(false);
lock.start_display_link();
}
}
executor
.spawn(async move {
let mut lock = window_state.as_ref().lock();
if is_active {
lock.move_traffic_light();
}
if let Some(mut callback) = lock.activate_callback.take() {
drop(lock);
callback(is_active);
@@ -2533,80 +2273,3 @@ unsafe fn remove_layer_background(layer: id) {
}
}
}
extern "C" fn add_titlebar_accessory_view_controller(this: &Object, _: Sel, view_controller: id) {
unsafe {
let _: () = msg_send![super(this, class!(NSWindow)), addTitlebarAccessoryViewController: view_controller];
// Hide the native tab bar and set its height to 0, since we render our own.
let accessory_view: id = msg_send![view_controller, view];
let _: () = msg_send![accessory_view, setHidden: YES];
let mut frame: NSRect = msg_send![accessory_view, frame];
frame.size.height = 0.0;
let _: () = msg_send![accessory_view, setFrame: frame];
}
}
extern "C" fn move_tab_to_new_window(this: &Object, _: Sel, _: id) {
unsafe {
let _: () = msg_send![super(this, class!(NSWindow)), moveTabToNewWindow:nil];
let window_state = get_window_state(this);
let mut lock = window_state.as_ref().lock();
if let Some(mut callback) = lock.move_tab_to_new_window_callback.take() {
drop(lock);
callback();
window_state.lock().move_tab_to_new_window_callback = Some(callback);
}
}
}
extern "C" fn merge_all_windows(this: &Object, _: Sel, _: id) {
unsafe {
let _: () = msg_send![super(this, class!(NSWindow)), mergeAllWindows:nil];
let window_state = get_window_state(this);
let mut lock = window_state.as_ref().lock();
if let Some(mut callback) = lock.merge_all_windows_callback.take() {
drop(lock);
callback();
window_state.lock().merge_all_windows_callback = Some(callback);
}
}
}
extern "C" fn select_next_tab(this: &Object, _sel: Sel, _id: id) {
let window_state = unsafe { get_window_state(this) };
let mut lock = window_state.as_ref().lock();
if let Some(mut callback) = lock.select_next_tab_callback.take() {
drop(lock);
callback();
window_state.lock().select_next_tab_callback = Some(callback);
}
}
extern "C" fn select_previous_tab(this: &Object, _sel: Sel, _id: id) {
let window_state = unsafe { get_window_state(this) };
let mut lock = window_state.as_ref().lock();
if let Some(mut callback) = lock.select_previous_tab_callback.take() {
drop(lock);
callback();
window_state.lock().select_previous_tab_callback = Some(callback);
}
}
extern "C" fn toggle_tab_bar(this: &Object, _sel: Sel, _id: id) {
unsafe {
let _: () = msg_send![super(this, class!(NSWindow)), toggleTabBar:nil];
let window_state = get_window_state(this);
let mut lock = window_state.as_ref().lock();
lock.move_traffic_light();
if let Some(mut callback) = lock.toggle_tab_bar_callback.take() {
drop(lock);
callback();
window_state.lock().toggle_tab_bar_callback = Some(callback);
}
}
}

View File

@@ -83,7 +83,11 @@ impl PlatformKeyboardMapper for WindowsKeyboardMapper {
..keystroke.modifiers
};
KeybindingKeystroke::new(keystroke, modifiers, key)
KeybindingKeystroke {
inner: keystroke,
display_modifiers: modifiers,
display_key: key,
}
}
fn get_key_equivalents(&self) -> Option<&HashMap<char, char>> {
@@ -331,9 +335,9 @@ mod tests {
key_char: None,
};
let mapped = mapper.map_key_equivalent(keystroke.clone(), true);
assert_eq!(*mapped.inner(), keystroke);
assert_eq!(mapped.key(), "a");
assert_eq!(*mapped.modifiers(), Modifiers::control());
assert_eq!(mapped.inner, keystroke);
assert_eq!(mapped.display_key, "a");
assert_eq!(mapped.display_modifiers, Modifiers::control());
// Shifted case, ctrl-$
let keystroke = Keystroke {
@@ -342,9 +346,9 @@ mod tests {
key_char: None,
};
let mapped = mapper.map_key_equivalent(keystroke.clone(), true);
assert_eq!(*mapped.inner(), keystroke);
assert_eq!(mapped.key(), "4");
assert_eq!(*mapped.modifiers(), Modifiers::control_shift());
assert_eq!(mapped.inner, keystroke);
assert_eq!(mapped.display_key, "4");
assert_eq!(mapped.display_modifiers, Modifiers::control_shift());
// Shifted case, but shift is true
let keystroke = Keystroke {
@@ -353,9 +357,9 @@ mod tests {
key_char: None,
};
let mapped = mapper.map_key_equivalent(keystroke, true);
assert_eq!(mapped.inner().modifiers, Modifiers::control());
assert_eq!(mapped.key(), "4");
assert_eq!(*mapped.modifiers(), Modifiers::control_shift());
assert_eq!(mapped.inner.modifiers, Modifiers::control());
assert_eq!(mapped.display_key, "4");
assert_eq!(mapped.display_modifiers, Modifiers::control_shift());
// Windows style
let keystroke = Keystroke {
@@ -364,9 +368,9 @@ mod tests {
key_char: None,
};
let mapped = mapper.map_key_equivalent(keystroke, true);
assert_eq!(mapped.inner().modifiers, Modifiers::control());
assert_eq!(mapped.inner().key, "$");
assert_eq!(mapped.key(), "4");
assert_eq!(*mapped.modifiers(), Modifiers::control_shift());
assert_eq!(mapped.inner.modifiers, Modifiers::control());
assert_eq!(mapped.inner.key, "$");
assert_eq!(mapped.display_key, "4");
assert_eq!(mapped.display_modifiers, Modifiers::control_shift());
}
}

View File

@@ -851,7 +851,7 @@ fn file_save_dialog(
if !directory.to_string_lossy().is_empty()
&& let Some(full_path) = directory.canonicalize().log_err()
{
let full_path = SanitizedPath::new(&full_path);
let full_path = SanitizedPath::from(full_path);
let full_path_string = full_path.to_string();
let path_item: IShellItem =
unsafe { SHCreateItemFromParsingName(&HSTRING::from(full_path_string), None)? };

View File

@@ -382,17 +382,10 @@ impl WindowsWindow {
let (mut dwexstyle, dwstyle) = if params.kind == WindowKind::PopUp {
(WS_EX_TOOLWINDOW, WINDOW_STYLE(0x0))
} else {
let mut dwstyle = WS_SYSMENU;
if params.is_resizable {
dwstyle |= WS_THICKFRAME | WS_MAXIMIZEBOX;
}
if params.is_minimizable {
dwstyle |= WS_MINIMIZEBOX;
}
(WS_EX_APPWINDOW, dwstyle)
(
WS_EX_APPWINDOW,
WS_THICKFRAME | WS_SYSMENU | WS_MAXIMIZEBOX | WS_MINIMIZEBOX,
)
};
if !disable_direct_composition {
dwexstyle |= WS_EX_NOREDIRECTIONBITMAP;

View File

@@ -181,7 +181,7 @@ impl LineWrapper {
matches!(c, '\u{0400}'..='\u{04FF}') ||
// Some other known special characters that should be treated as word characters,
// e.g. `a-b`, `var_name`, `I'm`, '@mention`, `#hashtag`, `100%`, `3.1415`, `2^3`, `a~b`, etc.
matches!(c, '-' | '_' | '.' | '\'' | '$' | '%' | '@' | '#' | '^' | '~' | ',' | '!' | ';' | '*') ||
matches!(c, '-' | '_' | '.' | '\'' | '$' | '%' | '@' | '#' | '^' | '~' | ',') ||
// Characters that used in URL, e.g. `https://github.com/zed-industries/zed?a=1&b=2` for better wrapping a long URL.
matches!(c, '/' | ':' | '?' | '&' | '=') ||
// `⋯` character is special used in Zed, to keep this at the end of the line.

View File

@@ -12,11 +12,11 @@ use crate::{
PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, PromptButton, PromptLevel, Quad,
Render, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, Replay, ResizeEdge,
SMOOTH_SVG_SCALE_FACTOR, SUBPIXEL_VARIANTS, ScaledPixels, Scene, Shadow, SharedString, Size,
StrikethroughStyle, Style, SubscriberSet, Subscription, SystemWindowTab,
SystemWindowTabController, TabHandles, TaffyLayoutEngine, Task, TextStyle, TextStyleRefinement,
TransformationMatrix, Underline, UnderlineStyle, WindowAppearance, WindowBackgroundAppearance,
WindowBounds, WindowControls, WindowDecorations, WindowOptions, WindowParams, WindowTextSystem,
point, prelude::*, px, rems, size, transparent_black,
StrikethroughStyle, Style, SubscriberSet, Subscription, TabHandles, TaffyLayoutEngine, Task,
TextStyle, TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle,
WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControls, WindowDecorations,
WindowOptions, WindowParams, WindowTextSystem, point, prelude::*, px, rems, size,
transparent_black,
};
use anyhow::{Context as _, Result, anyhow};
use collections::{FxHashMap, FxHashSet};
@@ -939,15 +939,11 @@ impl Window {
show,
kind,
is_movable,
is_resizable,
is_minimizable,
display_id,
window_background,
app_id,
window_min_size,
window_decorations,
#[cfg_attr(not(target_os = "macos"), allow(unused_variables))]
tabbing_identifier,
} = options;
let bounds = window_bounds
@@ -960,23 +956,12 @@ impl Window {
titlebar,
kind,
is_movable,
is_resizable,
is_minimizable,
focus,
show,
display_id,
window_min_size,
#[cfg(target_os = "macos")]
tabbing_identifier,
},
)?;
let tab_bar_visible = platform_window.tab_bar_visible();
SystemWindowTabController::init_visible(cx, tab_bar_visible);
if let Some(tabs) = platform_window.tabbed_windows() {
SystemWindowTabController::add_tab(cx, handle.window_id(), tabs);
}
let display_id = platform_window.display().map(|display| display.id());
let sprite_atlas = platform_window.sprite_atlas();
let mouse_position = platform_window.mouse_position();
@@ -1006,13 +991,9 @@ impl Window {
}
platform_window.on_close(Box::new({
let window_id = handle.window_id();
let mut cx = cx.to_async();
move || {
let _ = handle.update(&mut cx, |_, window, _| window.remove_window());
let _ = cx.update(|cx| {
SystemWindowTabController::remove_tab(cx, window_id);
});
}
}));
platform_window.on_request_frame(Box::new({
@@ -1101,11 +1082,7 @@ impl Window {
.activation_observers
.clone()
.retain(&(), |callback| callback(window, cx));
window.bounds_changed(cx);
window.refresh();
SystemWindowTabController::update_last_active(cx, window.handle.id);
})
.log_err();
}
@@ -1146,57 +1123,6 @@ impl Window {
.unwrap_or(None)
})
});
platform_window.on_move_tab_to_new_window({
let mut cx = cx.to_async();
Box::new(move || {
handle
.update(&mut cx, |_, _window, cx| {
SystemWindowTabController::move_tab_to_new_window(cx, handle.window_id());
})
.log_err();
})
});
platform_window.on_merge_all_windows({
let mut cx = cx.to_async();
Box::new(move || {
handle
.update(&mut cx, |_, _window, cx| {
SystemWindowTabController::merge_all_windows(cx, handle.window_id());
})
.log_err();
})
});
platform_window.on_select_next_tab({
let mut cx = cx.to_async();
Box::new(move || {
handle
.update(&mut cx, |_, _window, cx| {
SystemWindowTabController::select_next_tab(cx, handle.window_id());
})
.log_err();
})
});
platform_window.on_select_previous_tab({
let mut cx = cx.to_async();
Box::new(move || {
handle
.update(&mut cx, |_, _window, cx| {
SystemWindowTabController::select_previous_tab(cx, handle.window_id())
})
.log_err();
})
});
platform_window.on_toggle_tab_bar({
let mut cx = cx.to_async();
Box::new(move || {
handle
.update(&mut cx, |_, window, cx| {
let tab_bar_visible = window.platform_window.tab_bar_visible();
SystemWindowTabController::set_visible(cx, tab_bar_visible);
})
.log_err();
})
});
if let Some(app_id) = app_id {
platform_window.set_app_id(&app_id);
@@ -4349,47 +4275,11 @@ impl Window {
}
/// Perform titlebar double-click action.
/// This is macOS specific.
/// This is MacOS specific.
pub fn titlebar_double_click(&self) {
self.platform_window.titlebar_double_click();
}
/// Gets the window's title at the platform level.
/// This is macOS specific.
pub fn window_title(&self) -> String {
self.platform_window.get_title()
}
/// Returns a list of all tabbed windows and their titles.
/// This is macOS specific.
pub fn tabbed_windows(&self) -> Option<Vec<SystemWindowTab>> {
self.platform_window.tabbed_windows()
}
/// Returns the tab bar visibility.
/// This is macOS specific.
pub fn tab_bar_visible(&self) -> bool {
self.platform_window.tab_bar_visible()
}
/// Merges all open windows into a single tabbed window.
/// This is macOS specific.
pub fn merge_all_windows(&self) {
self.platform_window.merge_all_windows()
}
/// Moves the tab to a new containing window.
/// This is macOS specific.
pub fn move_tab_to_new_window(&self) {
self.platform_window.move_tab_to_new_window()
}
/// Shows or hides the window tab overview.
/// This is macOS specific.
pub fn toggle_window_tab_overview(&self) {
self.platform_window.toggle_window_tab_overview()
}
/// Toggles the inspector mode on this window.
#[cfg(any(feature = "inspector", debug_assertions))]
pub fn toggle_inspector(&mut self, cx: &mut App) {
@@ -4578,13 +4468,6 @@ impl Window {
}
None
}
/// For testing: set the current modifier keys state.
/// This does not generate any events.
#[cfg(any(test, feature = "test-support"))]
pub fn set_modifiers(&mut self, modifiers: Modifiers) {
self.modifiers = modifiers;
}
}
// #[derive(Clone, Copy, Eq, PartialEq, Hash)]

View File

@@ -146,7 +146,6 @@ pub enum IconName {
Library,
LineHeight,
ListCollapse,
ListFilter,
ListTodo,
ListTree,
ListX,

View File

@@ -720,9 +720,6 @@ pub struct LanguageConfig {
/// How to soft-wrap long lines of text.
#[serde(default)]
pub soft_wrap: Option<SoftWrap>,
/// When set, selections can be wrapped using prefix/suffix pairs on both sides.
#[serde(default)]
pub wrap_characters: Option<WrapCharactersConfig>,
/// The name of a Prettier parser that will be used for this language when no file path is available.
/// If there's a parser name in the language settings, that will be used instead.
#[serde(default)]
@@ -926,7 +923,6 @@ impl Default for LanguageConfig {
hard_tabs: None,
tab_size: None,
soft_wrap: None,
wrap_characters: None,
prettier_parser_name: None,
hidden: false,
jsx_tag_auto_close: None,
@@ -936,18 +932,6 @@ impl Default for LanguageConfig {
}
}
#[derive(Clone, Debug, Deserialize, JsonSchema)]
pub struct WrapCharactersConfig {
/// Opening token split into a prefix and suffix. The first caret goes
/// after the prefix (i.e., between prefix and suffix).
pub start_prefix: String,
pub start_suffix: String,
/// Closing token split into a prefix and suffix. The second caret goes
/// after the prefix (i.e., between prefix and suffix).
pub end_prefix: String,
pub end_suffix: String,
}
fn auto_indent_using_last_non_empty_line_default() -> bool {
true
}

View File

@@ -11,14 +11,13 @@ use std::{
use async_trait::async_trait;
use collections::HashMap;
use fs::Fs;
use gpui::{AsyncApp, SharedString};
use settings::WorktreeId;
use crate::{LanguageName, ManifestName};
/// Represents a single toolchain.
#[derive(Clone, Eq, Debug)]
#[derive(Clone, Debug, Eq)]
pub struct Toolchain {
/// User-facing label
pub name: SharedString,
@@ -30,29 +29,21 @@ pub struct Toolchain {
impl std::hash::Hash for Toolchain {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
let Self {
name,
path,
language_name,
as_json: _,
} = self;
name.hash(state);
path.hash(state);
language_name.hash(state);
self.name.hash(state);
self.path.hash(state);
self.language_name.hash(state);
}
}
impl PartialEq for Toolchain {
fn eq(&self, other: &Self) -> bool {
let Self {
name,
path,
language_name,
as_json: _,
} = self;
// Do not use as_json for comparisons; it shouldn't impact equality, as it's not user-surfaced.
// Thus, there could be multiple entries that look the same in the UI.
(name, path, language_name).eq(&(&other.name, &other.path, &other.language_name))
(&self.name, &self.path, &self.language_name).eq(&(
&other.name,
&other.path,
&other.language_name,
))
}
}
@@ -61,14 +52,13 @@ pub trait ToolchainLister: Send + Sync {
async fn list(
&self,
worktree_root: PathBuf,
subroot_relative_path: Arc<Path>,
subroot_relative_path: Option<Arc<Path>>,
project_env: Option<HashMap<String, String>>,
) -> ToolchainList;
// Returns a term which we should use in UI to refer to a toolchain.
fn term(&self) -> SharedString;
/// Returns the name of the manifest file for this toolchain.
fn manifest_name(&self) -> ManifestName;
async fn activation_script(&self, toolchain: &Toolchain, fs: &dyn Fs) -> Option<String>;
}
#[async_trait(?Send)]
@@ -92,7 +82,7 @@ pub trait LocalLanguageToolchainStore: Send + Sync + 'static {
) -> Option<Toolchain>;
}
#[async_trait(?Send)]
#[async_trait(?Send )]
impl<T: LocalLanguageToolchainStore> LanguageToolchainStore for T {
async fn active_toolchain(
self: Arc<Self>,

View File

@@ -208,7 +208,6 @@ impl LanguageModelRegistry {
) -> impl Iterator<Item = Arc<dyn LanguageModel>> + 'a {
self.providers
.values()
.filter(|provider| provider.is_authenticated(cx))
.flat_map(|provider| provider.provided_models(cx))
}

View File

@@ -381,7 +381,7 @@ impl LanguageModel for OpenRouterLanguageModel {
fn tool_input_format(&self) -> LanguageModelToolSchemaFormat {
let model_id = self.model.id().trim().to_lowercase();
if model_id.contains("gemini") || model_id.contains("grok") {
if model_id.contains("gemini") || model_id.contains("grok-4") {
LanguageModelToolSchemaFormat::JsonSchemaSubset
} else {
LanguageModelToolSchemaFormat::JsonSchema

View File

@@ -24,7 +24,6 @@ itertools.workspace = true
language.workspace = true
lsp.workspace = true
project.workspace = true
proto.workspace = true
serde_json.workspace = true
settings.workspace = true
theme.workspace = true

View File

@@ -1,20 +1,20 @@
mod key_context_view;
pub mod lsp_button;
pub mod lsp_log_view;
mod lsp_log;
pub mod lsp_tool;
mod syntax_tree_view;
#[cfg(test)]
mod lsp_log_view_tests;
mod lsp_log_tests;
use gpui::{App, AppContext, Entity};
pub use lsp_log_view::LspLogView;
pub use lsp_log::{LogStore, LspLogToolbarItemView, LspLogView};
pub use syntax_tree_view::{SyntaxTreeToolbarItemView, SyntaxTreeView};
use ui::{Context, Window};
use workspace::{Item, ItemHandle, SplitDirection, Workspace};
pub fn init(cx: &mut App) {
lsp_log_view::init(true, cx);
lsp_log::init(cx);
syntax_tree_view::init(cx);
key_context_view::init(cx);
}

View File

@@ -1,22 +1,20 @@
use std::sync::Arc;
use crate::lsp_log_view::LogMenuItem;
use crate::lsp_log::LogMenuItem;
use super::*;
use futures::StreamExt;
use gpui::{AppContext as _, SemanticVersion, TestAppContext, VisualTestContext};
use language::{FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, tree_sitter_rust};
use lsp::LanguageServerName;
use project::{
FakeFs, Project,
lsp_store::log_store::{LanguageServerKind, LogKind, LogStore},
};
use lsp_log::LogKind;
use project::{FakeFs, Project};
use serde_json::json;
use settings::SettingsStore;
use util::path;
#[gpui::test]
async fn test_lsp_log_view(cx: &mut TestAppContext) {
async fn test_lsp_logs(cx: &mut TestAppContext) {
zlog::init_test();
init_test(cx);
@@ -53,7 +51,7 @@ async fn test_lsp_log_view(cx: &mut TestAppContext) {
},
);
let log_store = cx.new(|cx| LogStore::new(true, cx));
let log_store = cx.new(LogStore::new);
log_store.update(cx, |store, cx| store.add_project(&project, cx));
let _rust_buffer = project
@@ -96,7 +94,7 @@ async fn test_lsp_log_view(cx: &mut TestAppContext) {
rpc_trace_enabled: false,
selected_entry: LogKind::Logs,
trace_level: lsp::TraceValue::Off,
server_kind: LanguageServerKind::Local {
server_kind: lsp_log::LanguageServerKind::Local {
project: project.downgrade()
}
}]

View File

@@ -11,10 +11,7 @@ use editor::{Editor, EditorEvent};
use gpui::{Corner, Entity, Subscription, Task, WeakEntity, actions};
use language::{BinaryStatus, BufferId, ServerHealth};
use lsp::{LanguageServerId, LanguageServerName, LanguageServerSelector};
use project::{
LspStore, LspStoreEvent, Worktree, lsp_store::log_store::GlobalLogStore,
project_settings::ProjectSettings,
};
use project::{LspStore, LspStoreEvent, Worktree, project_settings::ProjectSettings};
use settings::{Settings as _, SettingsStore};
use ui::{
Context, ContextMenu, ContextMenuEntry, ContextMenuItem, DocumentationAside, DocumentationSide,
@@ -23,7 +20,7 @@ use ui::{
use workspace::{StatusItemView, Workspace};
use crate::lsp_log_view;
use crate::lsp_log::GlobalLogStore;
actions!(
lsp_tool,
@@ -33,7 +30,7 @@ actions!(
]
);
pub struct LspButton {
pub struct LspTool {
server_state: Entity<LanguageServerState>,
popover_menu_handle: PopoverMenuHandle<ContextMenu>,
lsp_menu: Option<Entity<ContextMenu>>,
@@ -124,8 +121,9 @@ impl LanguageServerState {
menu = menu.align_popover_bottom();
let lsp_logs = cx
.try_global::<GlobalLogStore>()
.map(|lsp_logs| lsp_logs.0.clone());
let Some(lsp_logs) = lsp_logs else {
.and_then(|lsp_logs| lsp_logs.0.upgrade());
let lsp_store = self.lsp_store.upgrade();
let Some((lsp_logs, lsp_store)) = lsp_logs.zip(lsp_store) else {
return menu;
};
@@ -212,11 +210,10 @@ impl LanguageServerState {
};
let server_selector = server_info.server_selector();
let is_remote = self
.lsp_store
.update(cx, |lsp_store, _| lsp_store.as_remote().is_some())
.unwrap_or(false);
let has_logs = is_remote || lsp_logs.read(cx).has_server_logs(&server_selector);
// TODO currently, Zed remote does not work well with the LSP logs
// https://github.com/zed-industries/zed/issues/28557
let has_logs = lsp_store.read(cx).as_local().is_some()
&& lsp_logs.read(cx).has_server_logs(&server_selector);
let status_color = server_info
.binary_status
@@ -244,10 +241,10 @@ impl LanguageServerState {
.as_ref()
.or_else(|| server_info.binary_status.as_ref()?.message.as_ref())
.cloned();
let hover_label = if message.is_some() {
Some("View Message")
} else if has_logs {
let hover_label = if has_logs {
Some("View Logs")
} else if message.is_some() {
Some("View Message")
} else {
None
};
@@ -291,7 +288,16 @@ impl LanguageServerState {
let server_name = server_info.name.clone();
let workspace = self.workspace.clone();
move |window, cx| {
if let Some(message) = &message {
if has_logs {
lsp_logs.update(cx, |lsp_logs, cx| {
lsp_logs.open_server_trace(
workspace.clone(),
server_selector.clone(),
window,
cx,
);
});
} else if let Some(message) = &message {
let Some(create_buffer) = workspace
.update(cx, |workspace, cx| {
workspace
@@ -341,14 +347,6 @@ impl LanguageServerState {
anyhow::Ok(())
})
.detach();
} else if has_logs {
lsp_log_view::open_server_trace(
&lsp_logs,
workspace.clone(),
server_selector.clone(),
window,
cx,
);
} else {
cx.propagate();
}
@@ -512,7 +510,7 @@ impl ServerData<'_> {
}
}
impl LspButton {
impl LspTool {
pub fn new(
workspace: &Workspace,
popover_menu_handle: PopoverMenuHandle<ContextMenu>,
@@ -520,59 +518,37 @@ impl LspButton {
cx: &mut Context<Self>,
) -> Self {
let settings_subscription =
cx.observe_global_in::<SettingsStore>(window, move |lsp_button, window, cx| {
cx.observe_global_in::<SettingsStore>(window, move |lsp_tool, window, cx| {
if ProjectSettings::get_global(cx).global_lsp_settings.button {
if lsp_button.lsp_menu.is_none() {
lsp_button.refresh_lsp_menu(true, window, cx);
if lsp_tool.lsp_menu.is_none() {
lsp_tool.refresh_lsp_menu(true, window, cx);
}
} else if lsp_button.lsp_menu.take().is_some() {
} else if lsp_tool.lsp_menu.take().is_some() {
cx.notify();
}
});
let lsp_store = workspace.project().read(cx).lsp_store();
let mut language_servers = LanguageServers::default();
for (_, status) in lsp_store.read(cx).language_server_statuses() {
language_servers.binary_statuses.insert(
status.name.clone(),
LanguageServerBinaryStatus {
status: BinaryStatus::None,
message: None,
},
);
}
let lsp_store_subscription =
cx.subscribe_in(&lsp_store, window, |lsp_button, _, e, window, cx| {
lsp_button.on_lsp_store_event(e, window, cx)
cx.subscribe_in(&lsp_store, window, |lsp_tool, _, e, window, cx| {
lsp_tool.on_lsp_store_event(e, window, cx)
});
let server_state = cx.new(|_| LanguageServerState {
let state = cx.new(|_| LanguageServerState {
workspace: workspace.weak_handle(),
items: Vec::new(),
lsp_store: lsp_store.downgrade(),
active_editor: None,
language_servers,
language_servers: LanguageServers::default(),
});
let mut lsp_button = Self {
server_state,
Self {
server_state: state,
popover_menu_handle,
lsp_menu: None,
lsp_menu_refresh: Task::ready(()),
_subscriptions: vec![settings_subscription, lsp_store_subscription],
};
if !lsp_button
.server_state
.read(cx)
.language_servers
.binary_statuses
.is_empty()
{
lsp_button.refresh_lsp_menu(true, window, cx);
}
lsp_button
}
fn on_lsp_store_event(
@@ -732,25 +708,6 @@ impl LspButton {
}
}
}
state
.lsp_store
.update(cx, |lsp_store, cx| {
for (server_id, status) in lsp_store.language_server_statuses() {
if let Some(worktree) = status.worktree.and_then(|worktree_id| {
lsp_store
.worktree_store()
.read(cx)
.worktree_for_id(worktree_id, cx)
}) {
server_ids_to_worktrees.insert(server_id, worktree.clone());
server_names_to_worktrees
.entry(status.name.clone())
.or_default()
.insert((worktree, server_id));
}
}
})
.ok();
let mut servers_per_worktree = BTreeMap::<SharedString, Vec<ServerData>>::new();
let mut servers_without_worktree = Vec::<ServerData>::new();
@@ -895,18 +852,18 @@ impl LspButton {
) {
if create_if_empty || self.lsp_menu.is_some() {
let state = self.server_state.clone();
self.lsp_menu_refresh = cx.spawn_in(window, async move |lsp_button, cx| {
self.lsp_menu_refresh = cx.spawn_in(window, async move |lsp_tool, cx| {
cx.background_executor()
.timer(Duration::from_millis(30))
.await;
lsp_button
.update_in(cx, |lsp_button, window, cx| {
lsp_button.regenerate_items(cx);
lsp_tool
.update_in(cx, |lsp_tool, window, cx| {
lsp_tool.regenerate_items(cx);
let menu = ContextMenu::build(window, cx, |menu, _, cx| {
state.update(cx, |state, cx| state.fill_menu(menu, cx))
});
lsp_button.lsp_menu = Some(menu.clone());
lsp_button.popover_menu_handle.refresh_menu(
lsp_tool.lsp_menu = Some(menu.clone());
lsp_tool.popover_menu_handle.refresh_menu(
window,
cx,
Rc::new(move |_, _| Some(menu.clone())),
@@ -919,7 +876,7 @@ impl LspButton {
}
}
impl StatusItemView for LspButton {
impl StatusItemView for LspTool {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn workspace::ItemHandle>,
@@ -942,9 +899,9 @@ impl StatusItemView for LspButton {
let _editor_subscription = cx.subscribe_in(
&editor,
window,
|lsp_button, _, e: &EditorEvent, window, cx| match e {
|lsp_tool, _, e: &EditorEvent, window, cx| match e {
EditorEvent::ExcerptsAdded { buffer, .. } => {
let updated = lsp_button.server_state.update(cx, |state, cx| {
let updated = lsp_tool.server_state.update(cx, |state, cx| {
if let Some(active_editor) = state.active_editor.as_mut() {
let buffer_id = buffer.read(cx).remote_id();
active_editor.editor_buffers.insert(buffer_id)
@@ -953,13 +910,13 @@ impl StatusItemView for LspButton {
}
});
if updated {
lsp_button.refresh_lsp_menu(false, window, cx);
lsp_tool.refresh_lsp_menu(false, window, cx);
}
}
EditorEvent::ExcerptsRemoved {
removed_buffer_ids, ..
} => {
let removed = lsp_button.server_state.update(cx, |state, _| {
let removed = lsp_tool.server_state.update(cx, |state, _| {
let mut removed = false;
if let Some(active_editor) = state.active_editor.as_mut() {
for id in removed_buffer_ids {
@@ -973,7 +930,7 @@ impl StatusItemView for LspButton {
removed
});
if removed {
lsp_button.refresh_lsp_menu(false, window, cx);
lsp_tool.refresh_lsp_menu(false, window, cx);
}
}
_ => {}
@@ -1003,7 +960,7 @@ impl StatusItemView for LspButton {
}
}
impl Render for LspButton {
impl Render for LspTool {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
if self.server_state.read(cx).language_servers.is_empty() || self.lsp_menu.is_none() {
return div();
@@ -1048,11 +1005,11 @@ impl Render for LspButton {
(None, "All Servers Operational")
};
let lsp_button = cx.entity();
let lsp_tool = cx.entity();
div().child(
PopoverMenu::new("lsp-tool")
.menu(move |_, cx| lsp_button.read(cx).lsp_menu.clone())
.menu(move |_, cx| lsp_tool.read(cx).lsp_menu.clone())
.anchor(Corner::BottomLeft)
.with_handle(self.popover_menu_handle.clone())
.trigger_with_tooltip(

View File

@@ -764,7 +764,6 @@ mod tests {
let highlight_type = grammar.highlight_id_for_name("type").unwrap();
let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
let highlight_number = grammar.highlight_id_for_name("number").unwrap();
let highlight_field = grammar.highlight_id_for_name("property").unwrap();
assert_eq!(
adapter
@@ -829,7 +828,7 @@ mod tests {
Some(CodeLabel {
text: "two.Three a.Bcd".to_string(),
filter_range: 0..9,
runs: vec![(4..9, highlight_field), (12..15, highlight_type)],
runs: vec![(12..15, highlight_type)],
})
);
}

View File

@@ -1,13 +1,13 @@
(identifier) @variable
(type_identifier) @type
(field_identifier) @property
(field_identifier) @variable.member
(package_identifier) @namespace
(keyed_element
.
(literal_element
(identifier) @property))
(identifier) @variable.member))
(call_expression
function: (identifier) @function)

Some files were not shown because too many files have changed in this diff Show More