Release Notes:
- Added channel reordering for administrators (use `cmd-up` and
`cmd-down` on macOS or `ctrl-up` `ctrl-down` on Linux to move channels
up or down within their parent)
## Summary
This PR introduces the ability for channel administrators to reorder
channels within their parent context, providing better organizational
control over channel hierarchies. Users can now move channels up or down
relative to their siblings using keyboard shortcuts.
## Problem
Previously, channels were displayed in alphabetical order with no way to
customize their arrangement. This made it difficult for teams to
organize channels in a logical order that reflected their workflow or
importance, forcing users to prefix channel names with numbers or
special characters as a workaround.
## Solution
The implementation adds a persistent `channel_order` field to channels
that determines their display order within their parent. Channels with
the same parent are sorted by this field rather than alphabetically.
## Implementation Details
### Database Schema
Added a new column and index to support efficient ordering:
```sql
-- crates/collab/migrations/20250530175450_add_channel_order.sql
ALTER TABLE channels ADD COLUMN channel_order INTEGER NOT NULL DEFAULT 1;
CREATE INDEX CONCURRENTLY "index_channels_on_parent_path_and_order" ON "channels" ("parent_path", "channel_order");
```
### RPC Protocol
Extended the channel proto with ordering support:
```proto
// crates/proto/proto/channel.proto
message Channel {
uint64 id = 1;
string name = 2;
ChannelVisibility visibility = 3;
int32 channel_order = 4;
repeated uint64 parent_path = 5;
}
message ReorderChannel {
uint64 channel_id = 1;
enum Direction {
Up = 0;
Down = 1;
}
Direction direction = 2;
}
```
### Server-side Logic
The reordering is handled by swapping `channel_order` values between
adjacent channels:
```rust
// crates/collab/src/db/queries/channels.rs
pub async fn reorder_channel(
&self,
channel_id: ChannelId,
direction: proto::reorder_channel::Direction,
user_id: UserId,
) -> Result<Vec<Channel>> {
// Find the sibling channel to swap with
let sibling_channel = match direction {
proto::reorder_channel::Direction::Up => {
// Find channel with highest order less than current
channel::Entity::find()
.filter(
channel::Column::ParentPath
.eq(&channel.parent_path)
.and(channel::Column::ChannelOrder.lt(channel.channel_order)),
)
.order_by_desc(channel::Column::ChannelOrder)
.one(&*tx)
.await?
}
// Similar logic for Down...
};
// Swap the channel_order values
let temp_order = channel.channel_order;
channel.channel_order = sibling_channel.channel_order;
sibling_channel.channel_order = temp_order;
}
```
### Client-side Sorting
Optimized the sorting algorithm to avoid O(n²) complexity:
```rust
// crates/collab/src/db/queries/channels.rs
// Pre-compute sort keys for efficient O(n log n) sorting
let mut channels_with_keys: Vec<(Vec<i32>, Channel)> = channels
.into_iter()
.map(|channel| {
let mut sort_key = Vec::with_capacity(channel.parent_path.len() + 1);
// Build sort key from parent path orders
for parent_id in &channel.parent_path {
sort_key.push(channel_order_map.get(parent_id).copied().unwrap_or(i32::MAX));
}
sort_key.push(channel.channel_order);
(sort_key, channel)
})
.collect();
channels_with_keys.sort_by(|a, b| a.0.cmp(&b.0));
```
### User Interface
Added keyboard shortcuts and proper context handling:
```json
// assets/keymaps/default-macos.json
{
"context": "CollabPanel && not_editing",
"bindings": {
"cmd-up": "collab_panel::MoveChannelUp",
"cmd-down": "collab_panel::MoveChannelDown"
}
}
```
The CollabPanel now properly sets context to distinguish between editing
and navigation modes:
```rust
// crates/collab_ui/src/collab_panel.rs
fn dispatch_context(&self, window: &Window, cx: &Context<Self>) -> KeyContext {
let mut dispatch_context = KeyContext::new_with_defaults();
dispatch_context.add("CollabPanel");
dispatch_context.add("menu");
let identifier = if self.channel_name_editor.focus_handle(cx).is_focused(window) {
"editing"
} else {
"not_editing"
};
dispatch_context.add(identifier);
dispatch_context
}
```
## Testing
Comprehensive tests were added to verify:
- Basic reordering functionality (up/down movement)
- Boundary conditions (first/last channels)
- Permission checks (non-admins cannot reorder)
- Ordering persistence across server restarts
- Correct broadcasting of changes to channel members
## Migration Strategy
Existing channels are assigned initial `channel_order` values based on
their current alphabetical sorting to maintain the familiar order users
expect:
```sql
UPDATE channels
SET channel_order = (
SELECT ROW_NUMBER() OVER (
PARTITION BY parent_path
ORDER BY name, id
)
FROM channels c2
WHERE c2.id = channels.id
);
```
## Future Enhancements
While this PR provides basic reordering functionality, potential future
improvements could include:
- Drag-and-drop reordering in the UI
- Bulk reordering operations
- Custom sorting strategies (by activity, creation date, etc.)
## Checklist
- [x] Database migration included
- [x] Tests added for new functionality
- [x] Keybindings work on macOS and Linux
- [x] Permissions properly enforced
- [x] Error handling implemented throughout
- [x] Manual testing completed
- [x] Documentation updated
---------
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
Zed Server
This crate is what we run at https://collab.zed.dev.
It contains our back-end logic for collaboration, to which we connect from the Zed client via a websocket after authenticating via https://zed.dev, which is a separate repo running on Vercel.
Local Development
Database setup
Before you can run the collab server locally, you'll need to set up a zed Postgres database. Follow the steps sequentially:
- Ensure you have postgres installed. If not, install with
brew install postgresql@15. - Follow the steps on Brew's formula and verify your
$PATHcontains/opt/homebrew/opt/postgresql@15/bin. - If you hadn't done it before, create the
postgresuser withcreateuser -s postgres. - You are now ready to run the
bootstrapscript:
script/bootstrap
This script will set up the zed Postgres database, and populate it with some users. It requires internet access, because it fetches some users from the GitHub API.
The script will create several admin users, who you'll sign in as by default when developing locally. The GitHub logins for the default users are specified in the seed.default.json file.
To use a different set of admin users, create crates/collab/seed.json.
{
"admins": ["yourgithubhere"],
"channels": ["zed"]
}
Testing collaborative features locally
In one terminal, run Zed's collaboration server and the livekit dev server:
foreman start
In a second terminal, run two or more instances of Zed.
script/zed-local -2
This script starts one to four instances of Zed, depending on the -2, -3 or -4 flags. Each instance will be connected to the local collab server, signed in as a different user from seed.json or seed.default.json.
Deployment
We run two instances of collab:
- Staging (https://staging-collab.zed.dev)
- Production (https://collab.zed.dev)
Both of these run on the Kubernetes cluster hosted in Digital Ocean.
Deployment is triggered by pushing to the collab-staging (or collab-production) tag in GitHub. The best way to do this is:
./script/deploy-collab staging./script/deploy-collab production
You can tell what is currently deployed with ./script/what-is-deployed.
Database Migrations
To create a new migration:
./script/create-migration <name>
Migrations are run automatically on service start, so run foreman start again. The service will crash if the migrations fail.
When you create a new migration, you also need to update the SQLite schema that is used for testing.