From e792286660220c8ea7c8a743276b69ff58d67f00 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Thu, 14 Dec 2023 15:54:31 -0500 Subject: [PATCH 01/18] Add contributing guide --- CONTRIBUTING.md | 39 ++++++++++++++++++ .../screenshots/staff_usage_of_channels.png | Bin 0 -> 134356 bytes 2 files changed, 39 insertions(+) create mode 100644 CONTRIBUTING.md create mode 100644 assets/screenshots/staff_usage_of_channels.png diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000000000000000000000000000000000..fbb2aa29488f5b62adcd35fbe3a25c54614ea894 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,39 @@ +# CONTRIBUTING + +## Introduction + +[Since ~February 2022, the Zed Industries team has been exclusively using Zed to build Zed](https://x.com/nathansobo/status/1497958891509932035). We've built these tools to specifically address our own issues and frustrations with the current state of collaborative coding. These are not features we've built to simply look flashy, we work in channels every week, aggressively dogfooding our own tools. + +![Staff usage of channels (metrics were not being collected before August, 2023)](./assets/screenshots/staff_usage_of_channels.png) + +While we still have improvements to make, we believe we've sanded down a lot of the sharp edges and that experience is both smooth and enjoyable - one that gets you as close to hypothetically sitting next to your teammates as possible, even if you're potentially on different sides of the globe. We want to continue working this way amongst ourselves, but we are extremely excited to work with *you* in this way. We invite you to contribute to Zed *through* Zed. + +If you're new to Zed's channels, here's a guide [link to up-to-date docs] to help bring you up to speed. + +## Contribution ideas + +*If you already have an idea of what you'd like to contribute, you can skip this section.* + +- Our public roadmap [include link] shows the largest, most-wanted features we plan to add to Zed. +- Our [Top-Ranking Issues issue](https://github.com/zed-industries/community/issues/52) shows the most popular feature requests and issues, as voted on by the community. + +*If you are a plugin developer looking to contribute by building out the Zed ecosystem, have a look at these [issues](https://github.com/zed-industries/community/issues?q=is%3Aopen+is%3Aissue+label%3A%22potential+plugin%22+sort%3Areactions-%2B1-desc).* + +## Proposal & Discussion + +Once you have an idea of what you'd like to contribute, you'll want to communicate this to the team. Find a public channel [link to list of all public channels] that is relevant to your contribution, check the channel notes to see which Zed team members typically work in that channel, and post a message in the chat. If you're not sure which channel is best, you can post in the channel. *Please wait to begin working on your contribution until you've received feedback from the team. Turning down a contribution that was not discussed beforehand is a bummer for everyone.* + +## Implementation & Help + +Once approved, feel free to begin working on your contribution. If you have any questions, you can post in the channel you originally proposed your contribution in, or you can post in the channel. If you need help, reach out to a Zed teammate - we're happy to pair with you to help you learn the codebase and get your contribution merged. + +Reviewing code in a pull request, after the fact, is hard and tedious - the team generally likes to build trust and review code through pair programming. We'd prefer have conversations about the code, through Zed, while it is being written, so decisions can be made in real-time and less time is spent on fixing things after the fact. Ideally, GitHub is only used to merge code that has already been discussed and reviewed in Zed. + +--- + +Other things to mention here +- [ ] Etiquette +- [ ] CLA + +Things to do: +- [ ] Put names devs who "own" each channel in the channel notes diff --git a/assets/screenshots/staff_usage_of_channels.png b/assets/screenshots/staff_usage_of_channels.png new file mode 100644 index 0000000000000000000000000000000000000000..b9e607e59cfdd31c9477756e1f40283dde9ccbe0 GIT binary patch literal 134356 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

yaSU}o+QpZDB* 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

QPF7&Ie;E`5;HK&Xr^?h}Mr^uf59UYPynwt9# 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 literal 0 HcmV?d00001 From 5f25fa1227de9bb909a0681eb719fe07f8c098cc Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Thu, 14 Dec 2023 15:58:30 -0500 Subject: [PATCH 02/18] Tweak wording --- CONTRIBUTING.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fbb2aa29488f5b62adcd35fbe3a25c54614ea894..110120c061603739c5f7bd9d7fdf783346fcef5a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,19 +2,21 @@ ## Introduction -[Since ~February 2022, the Zed Industries team has been exclusively using Zed to build Zed](https://x.com/nathansobo/status/1497958891509932035). We've built these tools to specifically address our own issues and frustrations with the current state of collaborative coding. These are not features we've built to simply look flashy, we work in channels every week, aggressively dogfooding our own tools. +[Since ~February 2022, the Zed Industries team has been exclusively using Zed to build Zed](https://x.com/nathansobo/status/1497958891509932035). We've built these tools to specifically address our own issues and frustrations with the current state of collaborative coding. These are not features we've built to simply look flashy, we work in channels every day of the workweek, aggressively dogfooding everything. -![Staff usage of channels (metrics were not being collected before August, 2023)](./assets/screenshots/staff_usage_of_channels.png) +![Staff usage of channels](./assets/screenshots/staff_usage_of_channels.png) -While we still have improvements to make, we believe we've sanded down a lot of the sharp edges and that experience is both smooth and enjoyable - one that gets you as close to hypothetically sitting next to your teammates as possible, even if you're potentially on different sides of the globe. We want to continue working this way amongst ourselves, but we are extremely excited to work with *you* in this way. We invite you to contribute to Zed *through* Zed. +*Metrics were not being collected before August 2023* + +While we still have improvements to make, we believe we've sanded down a lot of the sharp edges and that the experience is both smooth and enjoyable - one that gets you as close to hypothetically sitting next to your teammates as possible, even if you're potentially on different sides of the globe. We want to continue working this way amongst ourselves and we are extremely excited to work with *you* in this way. We invite you to contribute to Zed *through* Zed. If you're new to Zed's channels, here's a guide [link to up-to-date docs] to help bring you up to speed. ## Contribution ideas -*If you already have an idea of what you'd like to contribute, you can skip this section.* +If you already have an idea of what you'd like to contribute, you can skip this section, otherwise, here are a few resources to help you find something to work on: -- Our public roadmap [include link] shows the largest, most-wanted features we plan to add to Zed. +- Our public roadmap [include link] details what features we plan to add to Zed. - Our [Top-Ranking Issues issue](https://github.com/zed-industries/community/issues/52) shows the most popular feature requests and issues, as voted on by the community. *If you are a plugin developer looking to contribute by building out the Zed ecosystem, have a look at these [issues](https://github.com/zed-industries/community/issues?q=is%3Aopen+is%3Aissue+label%3A%22potential+plugin%22+sort%3Areactions-%2B1-desc).* From c7d60bb003fa8ff1dc829778ba9b88ad408db122 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Thu, 14 Dec 2023 16:10:53 -0500 Subject: [PATCH 03/18] Add some TODOs --- CONTRIBUTING.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 110120c061603739c5f7bd9d7fdf783346fcef5a..42afe02fd2663f72c95d8d601f51cab6e046c367 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,6 +36,9 @@ Reviewing code in a pull request, after the fact, is hard and tedious - the team Other things to mention here - [ ] Etiquette - [ ] CLA +- [ ] Importance of tests +- [ ] Look over Piotr's PR and pull in what this is missing (tour of the codebase, etc.) + - https://github.com/zed-industries/zed/pull/3143/files Things to do: - [ ] Put names devs who "own" each channel in the channel notes From 2fd9ac506fa24cf6664d14009142047847b5b70b Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Fri, 15 Dec 2023 01:47:59 -0500 Subject: [PATCH 04/18] Tweak wording --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 42afe02fd2663f72c95d8d601f51cab6e046c367..09e9f3d1d16e1657485a4ed5aa6b0e3da0aeb55b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ ![Staff usage of channels](./assets/screenshots/staff_usage_of_channels.png) -*Metrics were not being collected before August 2023* +*Channel metrics were not collected prior to August 2023* While we still have improvements to make, we believe we've sanded down a lot of the sharp edges and that the experience is both smooth and enjoyable - one that gets you as close to hypothetically sitting next to your teammates as possible, even if you're potentially on different sides of the globe. We want to continue working this way amongst ourselves and we are extremely excited to work with *you* in this way. We invite you to contribute to Zed *through* Zed. From 6cb1a08cc9fe8d969e6ca0b2a5f44f3934b43f51 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Fri, 15 Dec 2023 17:18:52 -0500 Subject: [PATCH 05/18] Add notes --- CONTRIBUTING.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 09e9f3d1d16e1657485a4ed5aa6b0e3da0aeb55b..64fb8246517d3ac3f64363263497bc5c95726117 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,6 +39,8 @@ Other things to mention here - [ ] Importance of tests - [ ] Look over Piotr's PR and pull in what this is missing (tour of the codebase, etc.) - https://github.com/zed-industries/zed/pull/3143/files +- [ ] Ask people to check the PRs to see if something has already been started on +- [ ] Maybe have a channel that maps out what teammates / community users are working on what, so people can see what's being worked on Things to do: - [ ] Put names devs who "own" each channel in the channel notes From 3c2db6a4db646e53f20df2b648bf49e8819bcfc4 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Mon, 18 Dec 2023 13:35:07 -0500 Subject: [PATCH 06/18] Add some rough ideas Co-Authored-By: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> --- CONTRIBUTING.md | 87 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 73 insertions(+), 14 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 64fb8246517d3ac3f64363263497bc5c95726117..bde030de4746c42c8d4667e9729c455f7127dbc0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,59 @@ # CONTRIBUTING -## Introduction +Thanks for your interest in contributing to Zed, the collaborative platform that is also a code editor! + +## Contribution ideas + +If you already have an idea of what you'd like to contribute, you can skip this section, otherwise, here are a few resources to help you find something to work on: + +- Our public roadmap [include link] details what features we plan to add to Zed. +- Our [Top-Ranking Issues issue](https://github.com/zed-industries/community/issues/52) shows the most popular feature requests and issues, as voted on by the community. + +*If you are a plugin developer looking to contribute by building out the Zed ecosystem, have a look at these [issues](https://github.com/zed-industries/community/issues?q=is%3Aopen+is%3Aissue+label%3A%22potential+plugin%22+sort%3Areactions-%2B1-desc).* + +In the short term, we want to provide a generalized solutions to these problems (plugin system/theme system), so we are not looking to add these features to Zed itself. + +- Adding languages +- Themes + +## Resources + +### Bird-eye's view of Zed + +Zed is made up of several smaller crates - let's go over those you're most likely to interact with: +- [gpui](/crates/gpui) is a GPU-accelerated UI framework which provides all of the building blocks for Zed. +- [editor](/crates/editor) contains the core `Editor` type that drives both the code editor and all various input fields within Zed. It also handles a display layer for LSP features such as Inlay Hints or code completions. +- [project](/crates/project) manages files and navigation within the filetree. It is also Zed's side of communication with LSP. +- [workspace](/crates/workspace) handles local state serialization and groups projects together. +- [vim](/crates/vim) is a thin implementation of Vim workflow over `editor`. +- [lsp](/crates/lsp) handles communication with external LSP server. +- [language](/crates/language) drives `editor`'s understanding of language - from providing a list of symbols to the syntax map. +- [collab](/crates/collab) is the collaboration server itself, driving the collaboration features such as project sharing. +- [rpc](/crates/rpc) defines messages to be exchanged with collaboration server. + +// Let's try to make whoever we come into contact with for the first time +// well-equiped to discuss basic concepts around Zed +// Ideally these should link to mdbook/source code docs (doubtful given how source code docs might be more in-depth than necessary) +### Important concepts + +- Views vs Models +- Contexts +- Action +- UI + - Render vs RenderOnce + - ui crate + - storybook +- Workspace +- Project + - Worktree +- vim crate + - Editor + - Multibuffers +- Settings + +## Zed channels + +Once you have an idea of what you'd like to contribute, you'll want to communicate this to the team. If you're new to Zed's channels, here's a guide [link to up-to-date docs] to help bring you up to speed. [Since ~February 2022, the Zed Industries team has been exclusively using Zed to build Zed](https://x.com/nathansobo/status/1497958891509932035). We've built these tools to specifically address our own issues and frustrations with the current state of collaborative coding. These are not features we've built to simply look flashy, we work in channels every day of the workweek, aggressively dogfooding everything. @@ -10,37 +63,43 @@ While we still have improvements to make, we believe we've sanded down a lot of the sharp edges and that the experience is both smooth and enjoyable - one that gets you as close to hypothetically sitting next to your teammates as possible, even if you're potentially on different sides of the globe. We want to continue working this way amongst ourselves and we are extremely excited to work with *you* in this way. We invite you to contribute to Zed *through* Zed. -If you're new to Zed's channels, here's a guide [link to up-to-date docs] to help bring you up to speed. +### Proposal & Discussion -## Contribution ideas +To do that, find a public channel [link to list of all public channels] that is relevant to your contribution, check the channel notes to see which Zed team members typically work in that channel, and post a message in the chat. If you're not sure which channel is best, you can post in the channel. *Please wait to begin working on your contribution until you've received feedback from the team. Turning down a contribution that was not discussed beforehand is a bummer for everyone.* -If you already have an idea of what you'd like to contribute, you can skip this section, otherwise, here are a few resources to help you find something to work on: +## Implementation & Help -- Our public roadmap [include link] details what features we plan to add to Zed. -- Our [Top-Ranking Issues issue](https://github.com/zed-industries/community/issues/52) shows the most popular feature requests and issues, as voted on by the community. +Once approved, feel free to begin working on your contribution. If you have any questions, you can post in the channel you originally proposed your contribution in, or you can post in the channel. If you need help, reach out to a Zed teammate - we're happy to pair with you to help you learn the codebase and get your contribution merged. -*If you are a plugin developer looking to contribute by building out the Zed ecosystem, have a look at these [issues](https://github.com/zed-industries/community/issues?q=is%3Aopen+is%3Aissue+label%3A%22potential+plugin%22+sort%3Areactions-%2B1-desc).* +Reviewing code in a pull request, after the fact, is hard and tedious - the team generally likes to build trust and review code through pair programming. We'd prefer have conversations about the code, through Zed, while it is being written, so decisions can be made in real-time and less time is spent on fixing things after the fact. Ideally, GitHub is only used to merge code that has already been discussed and reviewed in Zed. -## Proposal & Discussion +--- Piotr's original contribution guide --- -Once you have an idea of what you'd like to contribute, you'll want to communicate this to the team. Find a public channel [link to list of all public channels] that is relevant to your contribution, check the channel notes to see which Zed team members typically work in that channel, and post a message in the chat. If you're not sure which channel is best, you can post in the channel. *Please wait to begin working on your contribution until you've received feedback from the team. Turning down a contribution that was not discussed beforehand is a bummer for everyone.* +Read on if you're looking for an outline of your first contribution - from finding your way around the codebase and asking questions, through modifying and testing the changes, finishing off with submitting your changes for review and interacting with Zed core team and Zed community as a whole. -## Implementation & Help +### Getting in touch +We believe that journeys are best when shared - hence there are multiple outlets for Zed users and developers to share their success stories and hurdles. -Once approved, feel free to begin working on your contribution. If you have any questions, you can post in the channel you originally proposed your contribution in, or you can post in the channel. If you need help, reach out to a Zed teammate - we're happy to pair with you to help you learn the codebase and get your contribution merged. +If you have questions, ask them away on our [Discord](https://discord.gg/XTtXmZYEpN) or in a dedicated [Zed channel](https://zed.dev/preview/channel/open-source-81). We also plan to organise office hours on a weekly basis - they will take place in forelinked Zed channel. -Reviewing code in a pull request, after the fact, is hard and tedious - the team generally likes to build trust and review code through pair programming. We'd prefer have conversations about the code, through Zed, while it is being written, so decisions can be made in real-time and less time is spent on fixing things after the fact. Ideally, GitHub is only used to merge code that has already been discussed and reviewed in Zed. +All activity in Zed communities is subject to our [Code of Conduct](https://docs.zed.dev/community/code-of-conduct). ---- +If you're just starting out with Zed, it might be worthwhile to look at some of the other crates that implement bits of UI - such as [go to line](/crates/go_to_line) modal that's bound to ctrl-g by default in Zed. + +### Upstreaming your changes +Here be dragons :) +--- Other things to mention here - [ ] Etiquette - [ ] CLA - [ ] Importance of tests - [ ] Look over Piotr's PR and pull in what this is missing (tour of the codebase, etc.) - - https://github.com/zed-industries/zed/pull/3143/files + - See above - [ ] Ask people to check the PRs to see if something has already been started on - [ ] Maybe have a channel that maps out what teammates / community users are working on what, so people can see what's being worked on +- [ ] Mention Discord or keep it only focused on Zed channels? +- [ ] Mention issue triage doc (https://github.com/zed-industries/community/blob/main/processes/issues_triage.md)? Things to do: - [ ] Put names devs who "own" each channel in the channel notes From 364e33df82472fbb4678b70afde705495db61b4d Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Mon, 8 Jan 2024 13:25:52 -0500 Subject: [PATCH 07/18] Add period --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bde030de4746c42c8d4667e9729c455f7127dbc0..41261250f32005ef669c8b3e4ba830f4fa96a19c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,7 +11,7 @@ If you already have an idea of what you'd like to contribute, you can skip this *If you are a plugin developer looking to contribute by building out the Zed ecosystem, have a look at these [issues](https://github.com/zed-industries/community/issues?q=is%3Aopen+is%3Aissue+label%3A%22potential+plugin%22+sort%3Areactions-%2B1-desc).* -In the short term, we want to provide a generalized solutions to these problems (plugin system/theme system), so we are not looking to add these features to Zed itself. +In the short term, we want to provide a generalized solutions to these problems (plugin system/theme system), so we are not looking to add these features to Zed itself - Adding languages - Themes From 5f1513893398b4b0ed084feb0f32dfeefe3b2e08 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 9 Jan 2024 13:41:42 -0500 Subject: [PATCH 08/18] WIP Co-Authored-By: Julia <30666851+ForLoveOfCats@users.noreply.github.com> --- CONTRIBUTING.md | 79 +++++++++++-------------------------------------- 1 file changed, 17 insertions(+), 62 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 41261250f32005ef669c8b3e4ba830f4fa96a19c..825ddce79d2611bf1e48f4e2fcc11bdc98beeb6b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,26 +2,26 @@ Thanks for your interest in contributing to Zed, the collaborative platform that is also a code editor! +We want to ensure that no one ends up spending time on a pull request that may not be accepted, so we ask that you discuss your ideas with the team and community before starting on a contribution. + +All activity in Zed communities is subject to our [Code of Conduct](https://docs.zed.dev/community/code-of-conduct). Contributors to Zed must sign our [Contributor License Agreement TODO](LINK) before their contributions can be merged. + ## Contribution ideas If you already have an idea of what you'd like to contribute, you can skip this section, otherwise, here are a few resources to help you find something to work on: -- Our public roadmap [include link] details what features we plan to add to Zed. +- Our [public roadmap TODO](LINK) details what features we plan to add to Zed. - Our [Top-Ranking Issues issue](https://github.com/zed-industries/community/issues/52) shows the most popular feature requests and issues, as voted on by the community. -*If you are a plugin developer looking to contribute by building out the Zed ecosystem, have a look at these [issues](https://github.com/zed-industries/community/issues?q=is%3Aopen+is%3Aissue+label%3A%22potential+plugin%22+sort%3Areactions-%2B1-desc).* - -In the short term, we want to provide a generalized solutions to these problems (plugin system/theme system), so we are not looking to add these features to Zed itself - -- Adding languages -- Themes +At the moment, we are generally not looking to extend Zed's language or theme support by directly adding these features to Zed - we really want to build a plugin system to handle making the editor extensible going forward. This isn't to say that we won't accept contributions that add support for a new language or theme, but more to emphasize that we want to discuss these types of contributions first. ## Resources ### Bird-eye's view of Zed Zed is made up of several smaller crates - let's go over those you're most likely to interact with: -- [gpui](/crates/gpui) is a GPU-accelerated UI framework which provides all of the building blocks for Zed. + +- [gpui](/crates/gpui) is a GPU-accelerated UI framework which provides all of the building blocks for Zed. **We recommend familiarizing yourself with the root level GPUI documentation** - [editor](/crates/editor) contains the core `Editor` type that drives both the code editor and all various input fields within Zed. It also handles a display layer for LSP features such as Inlay Hints or code completions. - [project](/crates/project) manages files and navigation within the filetree. It is also Zed's side of communication with LSP. - [workspace](/crates/workspace) handles local state serialization and groups projects together. @@ -31,29 +31,9 @@ Zed is made up of several smaller crates - let's go over those you're most likel - [collab](/crates/collab) is the collaboration server itself, driving the collaboration features such as project sharing. - [rpc](/crates/rpc) defines messages to be exchanged with collaboration server. -// Let's try to make whoever we come into contact with for the first time -// well-equiped to discuss basic concepts around Zed -// Ideally these should link to mdbook/source code docs (doubtful given how source code docs might be more in-depth than necessary) -### Important concepts - -- Views vs Models -- Contexts -- Action -- UI - - Render vs RenderOnce - - ui crate - - storybook -- Workspace -- Project - - Worktree -- vim crate - - Editor - - Multibuffers -- Settings - ## Zed channels -Once you have an idea of what you'd like to contribute, you'll want to communicate this to the team. If you're new to Zed's channels, here's a guide [link to up-to-date docs] to help bring you up to speed. +Once you have an idea of what you'd like to contribute, you'll want to communicate this to the team. If you're new to Zed's channels, here's a guide [link to up-to-date docs TODO](LINK) to help bring you up to speed. [Since ~February 2022, the Zed Industries team has been exclusively using Zed to build Zed](https://x.com/nathansobo/status/1497958891509932035). We've built these tools to specifically address our own issues and frustrations with the current state of collaborative coding. These are not features we've built to simply look flashy, we work in channels every day of the workweek, aggressively dogfooding everything. @@ -63,43 +43,18 @@ Once you have an idea of what you'd like to contribute, you'll want to communica While we still have improvements to make, we believe we've sanded down a lot of the sharp edges and that the experience is both smooth and enjoyable - one that gets you as close to hypothetically sitting next to your teammates as possible, even if you're potentially on different sides of the globe. We want to continue working this way amongst ourselves and we are extremely excited to work with *you* in this way. We invite you to contribute to Zed *through* Zed. -### Proposal & Discussion - -To do that, find a public channel [link to list of all public channels] that is relevant to your contribution, check the channel notes to see which Zed team members typically work in that channel, and post a message in the chat. If you're not sure which channel is best, you can post in the channel. *Please wait to begin working on your contribution until you've received feedback from the team. Turning down a contribution that was not discussed beforehand is a bummer for everyone.* - -## Implementation & Help - -Once approved, feel free to begin working on your contribution. If you have any questions, you can post in the channel you originally proposed your contribution in, or you can post in the channel. If you need help, reach out to a Zed teammate - we're happy to pair with you to help you learn the codebase and get your contribution merged. - -Reviewing code in a pull request, after the fact, is hard and tedious - the team generally likes to build trust and review code through pair programming. We'd prefer have conversations about the code, through Zed, while it is being written, so decisions can be made in real-time and less time is spent on fixing things after the fact. Ideally, GitHub is only used to merge code that has already been discussed and reviewed in Zed. - ---- Piotr's original contribution guide --- +We plan to organize office hours on a weekly basis - they will take place in forelinked Zed channel. -Read on if you're looking for an outline of your first contribution - from finding your way around the codebase and asking questions, through modifying and testing the changes, finishing off with submitting your changes for review and interacting with Zed core team and Zed community as a whole. - -### Getting in touch -We believe that journeys are best when shared - hence there are multiple outlets for Zed users and developers to share their success stories and hurdles. +### Proposal & Discussion -If you have questions, ask them away on our [Discord](https://discord.gg/XTtXmZYEpN) or in a dedicated [Zed channel](https://zed.dev/preview/channel/open-source-81). We also plan to organise office hours on a weekly basis - they will take place in forelinked Zed channel. +Before starting on a contribution, we ask that you look to see if there is any existing PRs, or in-Zed discussions about the thing you want to implement. If there is no existing work, find a [public channel TODO](LINK) that is relevant to your contribution, check the channel notes to see which Zed team members typically work in that channel, and post a message in the chat. If you're not sure which channel is best, you can post in the channel. -All activity in Zed communities is subject to our [Code of Conduct](https://docs.zed.dev/community/code-of-conduct). +*Please wait to begin working on your contribution until you've received feedback from the team. Turning down a contribution that was not discussed beforehand is a bummer for everyone.* +## Implementation & Help -If you're just starting out with Zed, it might be worthwhile to look at some of the other crates that implement bits of UI - such as [go to line](/crates/go_to_line) modal that's bound to ctrl-g by default in Zed. +Once approved, feel free to begin working on your contribution. If you have any questions, you can post in the channel you originally proposed your contribution in, or you can post in the channel. If you need help, reach out to a Zed teammate - we're happy to pair with you to help you learn the codebase and get your contribution merged. -### Upstreaming your changes -Here be dragons :) ---- -Other things to mention here -- [ ] Etiquette -- [ ] CLA -- [ ] Importance of tests -- [ ] Look over Piotr's PR and pull in what this is missing (tour of the codebase, etc.) - - See above -- [ ] Ask people to check the PRs to see if something has already been started on -- [ ] Maybe have a channel that maps out what teammates / community users are working on what, so people can see what's being worked on -- [ ] Mention Discord or keep it only focused on Zed channels? -- [ ] Mention issue triage doc (https://github.com/zed-industries/community/blob/main/processes/issues_triage.md)? +**Zed makes heavy use of unit and integration testing, we encourage you to write tests for your contribution.** -Things to do: -- [ ] Put names devs who "own" each channel in the channel notes +Reviewing code in a pull request, after the fact, is hard and tedious - the team generally likes to build trust and review code through pair programming. We'd prefer have conversations about the code, through Zed, while it is being written, so decisions can be made in real-time and less time is spent on fixing things after the fact. Ideally, GitHub is only used to merge code that has already been discussed and reviewed in Zed. From 904695e4825b264082c8edcd322d349dcc67da08 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 17 Jan 2024 12:32:08 -0500 Subject: [PATCH 09/18] Refine MVP CONTRIBUTING.md Co-Authored-By: Joseph T. Lyons <19867440+JosephTLyons@users.noreply.github.com> --- CONTRIBUTING.md | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 825ddce79d2611bf1e48f4e2fcc11bdc98beeb6b..0c45475e4ca8ccbfbd1b22b9bc6270ca34c28cd3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,16 +4,18 @@ Thanks for your interest in contributing to Zed, the collaborative platform that We want to ensure that no one ends up spending time on a pull request that may not be accepted, so we ask that you discuss your ideas with the team and community before starting on a contribution. -All activity in Zed communities is subject to our [Code of Conduct](https://docs.zed.dev/community/code-of-conduct). Contributors to Zed must sign our [Contributor License Agreement TODO](LINK) before their contributions can be merged. +All activity in Zed communities is subject to our [Code of Conduct](https://docs.zed.dev/community/code-of-conduct). Contributors to Zed must sign our Contributor License Agreement (link coming soon) before their contributions can be merged. ## Contribution ideas If you already have an idea of what you'd like to contribute, you can skip this section, otherwise, here are a few resources to help you find something to work on: -- Our [public roadmap TODO](LINK) details what features we plan to add to Zed. +- Our public roadmap (link coming soon!) details what features we plan to add to Zed. - Our [Top-Ranking Issues issue](https://github.com/zed-industries/community/issues/52) shows the most popular feature requests and issues, as voted on by the community. -At the moment, we are generally not looking to extend Zed's language or theme support by directly adding these features to Zed - we really want to build a plugin system to handle making the editor extensible going forward. This isn't to say that we won't accept contributions that add support for a new language or theme, but more to emphasize that we want to discuss these types of contributions first. +At the moment, we are generally not looking to extend Zed's language or theme support by directly adding these features to Zed - we really want to build a plugin system to handle making the editor extensible going forward. + +If you are passionate about shipping new languages or themes we suggest contributing to the extension system to help us get there faster. ## Resources @@ -30,31 +32,26 @@ Zed is made up of several smaller crates - let's go over those you're most likel - [language](/crates/language) drives `editor`'s understanding of language - from providing a list of symbols to the syntax map. - [collab](/crates/collab) is the collaboration server itself, driving the collaboration features such as project sharing. - [rpc](/crates/rpc) defines messages to be exchanged with collaboration server. +- [theme](/crates/theme) defines the theme system and provides a default theme. +- [ui](/crates/ui) is a collection of UI components and common patterns used throughout Zed. -## Zed channels - -Once you have an idea of what you'd like to contribute, you'll want to communicate this to the team. If you're new to Zed's channels, here's a guide [link to up-to-date docs TODO](LINK) to help bring you up to speed. - -[Since ~February 2022, the Zed Industries team has been exclusively using Zed to build Zed](https://x.com/nathansobo/status/1497958891509932035). We've built these tools to specifically address our own issues and frustrations with the current state of collaborative coding. These are not features we've built to simply look flashy, we work in channels every day of the workweek, aggressively dogfooding everything. - -![Staff usage of channels](./assets/screenshots/staff_usage_of_channels.png) - -*Channel metrics were not collected prior to August 2023* +### Proposal & Discussion -While we still have improvements to make, we believe we've sanded down a lot of the sharp edges and that the experience is both smooth and enjoyable - one that gets you as close to hypothetically sitting next to your teammates as possible, even if you're potentially on different sides of the globe. We want to continue working this way amongst ourselves and we are extremely excited to work with *you* in this way. We invite you to contribute to Zed *through* Zed. +Before starting on a contribution, we ask that you look to see if there is any existing PRs, or in-Zed discussions about the thing you want to implement. If there is no existing work, find a public channel that is relevant to your contribution, check the channel notes to see which Zed team members typically work in that channel, and post a message in the chat. If you're not sure which channel is best, you can start a discussion, ask a team member or another contributor. -We plan to organize office hours on a weekly basis - they will take place in forelinked Zed channel. +*Please remember contributions not discussed with the team ahead of time likely have a lower chance of being merged or looked at in a timely manner.* -### Proposal & Discussion +## Implementation & Help -Before starting on a contribution, we ask that you look to see if there is any existing PRs, or in-Zed discussions about the thing you want to implement. If there is no existing work, find a [public channel TODO](LINK) that is relevant to your contribution, check the channel notes to see which Zed team members typically work in that channel, and post a message in the chat. If you're not sure which channel is best, you can post in the channel. +When you start working on your contribution if you find you are struggling with something specific feel free to reach out to the team for help. -*Please wait to begin working on your contribution until you've received feedback from the team. Turning down a contribution that was not discussed beforehand is a bummer for everyone.* +Remember the team is more likely to be available to help if you have already discussed your contribution or are working on something that is higher priority, like something on the roadmap or a top-ranking issue. -## Implementation & Help +We're happy to pair with you to help you learn the codebase and get your contribution merged. -Once approved, feel free to begin working on your contribution. If you have any questions, you can post in the channel you originally proposed your contribution in, or you can post in the channel. If you need help, reach out to a Zed teammate - we're happy to pair with you to help you learn the codebase and get your contribution merged. +**Zed makes heavy use of unit and integration testing, it is highly likely that contributions without any unit tests will be rejected** -**Zed makes heavy use of unit and integration testing, we encourage you to write tests for your contribution.** +Reviewing code in a pull request, after the fact, is hard and tedious - the team generally likes to build trust and review code through pair programming. +We'd prefer have conversations about the code, through Zed, while it is being written, so decisions can be made in real-time and less time is spent on fixing things after the fact. Ideally, GitHub is only used to merge code that has already been discussed and reviewed in Zed. -Reviewing code in a pull request, after the fact, is hard and tedious - the team generally likes to build trust and review code through pair programming. We'd prefer have conversations about the code, through Zed, while it is being written, so decisions can be made in real-time and less time is spent on fixing things after the fact. Ideally, GitHub is only used to merge code that has already been discussed and reviewed in Zed. +Remeber that smaller, incremental PRs are easier to review and merge than large PRs. From 9415f098d773f3292fbc8dcbef715d97fb5f2971 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 17 Jan 2024 13:09:02 -0500 Subject: [PATCH 10/18] Clean out old readme contents Co-Authored-By: Joseph T. Lyons <19867440+JosephTLyons@users.noreply.github.com> --- README.md | 109 +++++++----------------------------------------------- 1 file changed, 13 insertions(+), 96 deletions(-) diff --git a/README.md b/README.md index eed8dd4d91c249dfa4de57f47d79c6ad1e2749e9..c97f4f5134d82ee6f9aa84c72992233feb47e255 100644 --- a/README.md +++ b/README.md @@ -1,110 +1,27 @@ -# Zed - -[![CI](https://github.com/zed-industries/zed/actions/workflows/ci.yml/badge.svg)](https://github.com/zed-industries/zed/actions/workflows/ci.yml) - -Welcome to Zed, a lightning-fast, collaborative code editor that makes your dreams come true. - -## Development tips - -### Dependencies - -* Install Xcode from https://apps.apple.com/us/app/xcode/id497799835?mt=12, and accept the license: - ``` - sudo xcodebuild -license - ``` - -* Install homebrew, node and rustup-init (rustup, rust, cargo, etc.) - ``` - /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" - brew install node rustup-init - rustup-init # follow the installation steps - ``` - -* Install postgres and configure the database - ``` - brew install postgresql@15 - brew services start postgresql@15 - psql -c "CREATE ROLE postgres SUPERUSER LOGIN" postgres - psql -U postgres -c "CREATE DATABASE zed" - ``` - -* Install the `LiveKit` server, the `PostgREST` API server, and the `foreman` process supervisor: - - ``` - brew install livekit - brew install postgrest - brew install foreman - ``` - -* Ensure the Zed.dev website is checked out in a sibling directory and install its dependencies: - - ``` - cd .. - git clone https://github.com/zed-industries/zed.dev - cd zed.dev && npm install - npm install -g vercel - ``` - -* Return to Zed project directory and Initialize submodules - - ``` - cd zed - git submodule update --init --recursive - ``` +# 🚧 TODO 🚧 -* Set up a local `zed` database and seed it with some initial users: +[ ] Add intro +[ ] Add link to contributing guide +[ ] Add barebones running zed from source instructions +[ ] Link out to further dev docs - [Create a personal GitHub token](https://github.com/settings/tokens/new) to run `script/bootstrap` once successfully: the token needs to have an access to private repositories for the script to work (`repo` OAuth scope). - Then delete that token. - - ``` - GITHUB_TOKEN=<$token> script/bootstrap - ``` - -* Now try running zed with collaboration disabled: - ``` - cargo run - ``` - -### Common errors - -* `xcrun: error: unable to find utility "metal", not a developer tool or in PATH` - * You need to install Xcode and then run: `xcode-select --switch /Applications/Xcode.app/Contents/Developer` - * (see https://github.com/gfx-rs/gfx/issues/2309) - -### Testing against locally-running servers - -Start the web and collab servers: - -``` -foreman start -``` +# Zed -If you want to run Zed pointed at the local servers, you can run: +[![CI](https://github.com/zed-industries/zed/actions/workflows/ci.yml/badge.svg)](https://github.com/zed-industries/zed/actions/workflows/ci.yml) -``` -script/zed-local -``` +Welcome to Zed, a high-performance, multiplayer code editor from the creators of [Atom]https://github.com/atom/atom) and [Tree-sitter](https://github.com/tree-sitter/tree-sitter). -### Dump element JSON +## Developing Zed -If you trigger `cmd-alt-i`, Zed will copy a JSON representation of the current window contents to the clipboard. You can paste this in a tool like [DJSON](https://chrome.google.com/webstore/detail/djson-json-viewer-formatt/chaeijjekipecdajnijdldjjipaegdjc?hl=en) to navigate the state of on-screen elements in a structured way. +- [Building Zed](./docs/src/developing_zed__building_zed.md) +- [Running Collaboration Locally](./docs/src/developing_zed__local_collaboration.md) ### Licensing +License information for third party dependencies must be correctly provided for CI to pass. + We use [`cargo-about`](https://github.com/EmbarkStudios/cargo-about) to automatically comply with open source licenses. If CI is failing, check the following: - Is it showing a `no license specified` error for a crate you've created? If so, add `publish = false` under `[package]` in your crate's Cargo.toml. - Is the error `failed to satisfy license requirements` for a dependency? If so, first determine what license the project has and whether this system is sufficient to comply with this license's requirements. If you're unsure, ask a lawyer. Once you've verified that this system is acceptable add the license's SPDX identifier to the `accepted` array in `script/licenses/zed-licenses.toml`. - Is `cargo-about` unable to find the license for a dependency? If so, add a clarification field at the end of `script/licenses/zed-licenses.toml`, as specified in the [cargo-about book](https://embarkstudios.github.io/cargo-about/cli/generate/config.html#crate-configuration). - - -### Wasm Plugins - -Zed has a Wasm-based plugin runtime which it currently uses to embed plugins. To compile Zed, you'll need to have the `wasm32-wasi` toolchain installed on your system. To install this toolchain, run: - -```bash -rustup target add wasm32-wasi -``` - -Plugins can be found in the `plugins` folder in the root. For more information about how plugins work, check the [Plugin Guide](./crates/plugin_runtime/README.md) in `crates/plugin_runtime/README.md`. From b64ae4df9c47507478360316ffd878e3151b2603 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 17 Jan 2024 13:09:13 -0500 Subject: [PATCH 11/18] Update developing zed doc Co-Authored-By: Joseph T. Lyons <19867440+JosephTLyons@users.noreply.github.com> --- docs/src/developing_zed__building_zed.md | 90 +++++++++++++++--------- 1 file changed, 56 insertions(+), 34 deletions(-) diff --git a/docs/src/developing_zed__building_zed.md b/docs/src/developing_zed__building_zed.md index 7606e369d05cf02226ea5b783f4ff8369a661be3..947858033065619fd8f4bbca512af96bc7f6830e 100644 --- a/docs/src/developing_zed__building_zed.md +++ b/docs/src/developing_zed__building_zed.md @@ -7,56 +7,77 @@ How to build Zed from source for the first time. -## Prerequisites +### Prerequisites + +🚧 TODO 🚧 Update for open source - Be added to the GitHub organization - Be added to the Vercel team +- Create a [Personal Access Token](https://github.com/settings/personal-access-tokens/new) on Github + - 🚧 TODO 🚧 What permissions are required? + - 🚧 TODO 🚧 What changes when repo isn't private? + - Go to https://github.com/settings/tokens and Generate new token + - GitHub currently provides two kinds of tokens: + - Classic Tokens, where only `repo` (Full control of private repositories) OAuth scope has to be selected + Unfortunately, unselecting `repo` scope and selecting every its inner scope instead does not allow the token users to read from private repositories + - (not applicable) Fine-grained Tokens, at the moment of writing, did not allow any kind of access of non-owned private repos + - Keep the token in the browser tab/editor for the next two steps + +### Dependencies + +* Install [Rust](https://www.rust-lang.org/tools/install) + +* Install the [GitHub CLI](https://cli.github.com/), [Livekit] & [Foreman] + +```bash +brew install gh +brew install livekit +brew install foreman +``` + +* Install [Xcode](https://apps.apple.com/us/app/xcode/id497799835?mt=12) from the macOS App Store + +* Install Xcode command line tools + +```bash +xcode-select --install +``` + +- If xcode-select --print-path prints /Library/Developer/CommandLineTools… run `sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer.` -## Process +* Install [Postgres](https://postgresapp.com) -Expect this to take 30min to an hour! Some of these steps will take quite a while based on your connection speed, and how long your first build will be. +* Install the wasm toolchain + +```bash +rustup target add wasm32-wasi +``` + +### Building Zed from Source -1. Install the [GitHub CLI](https://cli.github.com/): - - `brew install gh` 1. Clone the `zed` repo - - `gh repo clone zed-industries/zed` -1. Install Xcode from the macOS App Store -1. Install Xcode command line tools - - `xcode-select --install` - - If xcode-select --print-path prints /Library/Developer/CommandLineTools… run `sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer.` -1. Install [Postgres](https://postgresapp.com) -1. Install rust/rustup - - `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh` -1. Install the wasm toolchain - - `rustup target add wasm32-wasi` -1. Install Livekit & Foreman - - `brew install livekit` - - `brew install foreman` -1. Generate an GitHub API Key - - Go to https://github.com/settings/tokens and Generate new token - - GitHub currently provides two kinds of tokens: - - Classic Tokens, where only `repo` (Full control of private repositories) OAuth scope has to be selected - Unfortunately, unselecting `repo` scope and selecting every its inner scope instead does not allow the token users to read from private repositories - - (not applicable) Fine-grained Tokens, at the moment of writing, did not allow any kind of access of non-owned private repos - - Keep the token in the browser tab/editor for the next two steps -1. (Optional but reccomended) Add your GITHUB_TOKEN to your `.zshrc` or `.bashrc` like this: `export GITHUB_TOKEN=yourGithubAPIToken` -1. Ensure the Zed.dev website is checked out in a sibling directory and install its dependencies: + +```bash +gh repo clone zed-industries/zed +``` + +1. (Optional but recommended) Add your GITHUB_TOKEN to your `.zshrc` or `.bashrc` like this: `export GITHUB_TOKEN=yourGithubAPIToken` +1. (🚧 TODO 🚧 - Will this be relevant for open source?) Ensure the Zed.dev website is checked out in a sibling directory and install its dependencies: ``` cd .. git clone https://github.com/zed-industries/zed.dev cd zed.dev && npm install - npm install -g vercel + pnpm install -g vercel ``` -1. Link your zed.dev project to Vercel +1. (🚧 TODO 🚧 - Will this be relevant for open source?) Link your zed.dev project to Vercel - `vercel link` - Select the `zed-industries` team. If you don't have this get someone on the team to add you to it. - Select the `zed.dev` project -1. Run `vercel pull` to pull down the environment variables and project info from Vercel +1. (🚧 TODO 🚧 - Will this be relevant for open source?) Run `vercel pull` to pull down the environment variables and project info from Vercel 1. Open Postgres.app 1. From `./path/to/zed/`: - Run: - `GITHUB_TOKEN={yourGithubAPIToken} script/bootstrap` - - Replace `{yourGithubAPIToken}` with the API token you generated above. - You don't need to include the GITHUB_TOKEN if you exported it above. - Consider removing the token (if it's fine for you to recreate such tokens during occasional migrations) or store this token somewhere safe (like your Zed 1Password vault). - If you get: @@ -82,11 +103,12 @@ Expect this to take 30min to an hour! Some of these steps will take quite a whil ## Troubleshooting -### `error: failed to run custom build command for gpui v0.1.0 (/Users/path/to/zed)` +**`error: failed to run custom build command for gpui v0.1.0 (/Users/path/to/zed)`** - Try `xcode-select --switch /Applications/Xcode.app/Contents/Developer` -### `xcrun: error: unable to find utility "metal", not a developer tool or in PATH` +**`xcrun: error: unable to find utility "metal", not a developer tool or in PATH`** + ### Seeding errors during `script/bootstrap` runs @@ -104,4 +126,4 @@ Same command ### If you experience errors that mention some dependency is using unstable features -Try `cargo clean` and `cargo build` +Try `cargo clean` and `cargo build`, From d0f22df1eb30d80b0117cca308b352bbf97444ef Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 17 Jan 2024 13:20:27 -0500 Subject: [PATCH 12/18] Reorganize building zed doc Co-Authored-By: Joseph T. Lyons <19867440+JosephTLyons@users.noreply.github.com> --- docs/src/developing_zed__building_zed.md | 100 ++++++++++++----------- 1 file changed, 52 insertions(+), 48 deletions(-) diff --git a/docs/src/developing_zed__building_zed.md b/docs/src/developing_zed__building_zed.md index 947858033065619fd8f4bbca512af96bc7f6830e..a5270e2b2abc8f89e88e23d016e2e3a21a065efd 100644 --- a/docs/src/developing_zed__building_zed.md +++ b/docs/src/developing_zed__building_zed.md @@ -1,7 +1,7 @@ # Building Zed 🚧 TODO: -- [ ] Tidy up & update instructions + - [ ] Remove ZI-specific things - [ ] Rework any steps that currently require a ZI-specific account @@ -14,20 +14,20 @@ How to build Zed from source for the first time. - Be added to the GitHub organization - Be added to the Vercel team - Create a [Personal Access Token](https://github.com/settings/personal-access-tokens/new) on Github - - 🚧 TODO 🚧 What permissions are required? - - 🚧 TODO 🚧 What changes when repo isn't private? - - Go to https://github.com/settings/tokens and Generate new token - - GitHub currently provides two kinds of tokens: - - Classic Tokens, where only `repo` (Full control of private repositories) OAuth scope has to be selected - Unfortunately, unselecting `repo` scope and selecting every its inner scope instead does not allow the token users to read from private repositories - - (not applicable) Fine-grained Tokens, at the moment of writing, did not allow any kind of access of non-owned private repos - - Keep the token in the browser tab/editor for the next two steps + - 🚧 TODO 🚧 What permissions are required? + - 🚧 TODO 🚧 What changes when repo isn't private? + - Go to https://github.com/settings/tokens and Generate new token + - GitHub currently provides two kinds of tokens: + - Classic Tokens, where only `repo` (Full control of private repositories) OAuth scope has to be selected + Unfortunately, unselecting `repo` scope and selecting every its inner scope instead does not allow the token users to read from private repositories + - (not applicable) Fine-grained Tokens, at the moment of writing, did not allow any kind of access of non-owned private repos + - Keep the token in the browser tab/editor for the next two steps ### Dependencies -* Install [Rust](https://www.rust-lang.org/tools/install) +- Install [Rust](https://www.rust-lang.org/tools/install) -* Install the [GitHub CLI](https://cli.github.com/), [Livekit] & [Foreman] +- Install the [GitHub CLI](https://cli.github.com/), [Livekit](https://formulae.brew.sh/formula/livekit) & [Foreman](https://formulae.brew.sh/formula/foreman) ```bash brew install gh @@ -35,15 +35,15 @@ brew install livekit brew install foreman ``` -* Install [Xcode](https://apps.apple.com/us/app/xcode/id497799835?mt=12) from the macOS App Store +- Install [Xcode](https://apps.apple.com/us/app/xcode/id497799835?mt=12) from the macOS App Store -* Install Xcode command line tools +- Install [Xcode command line tools](https://developer.apple.com/xcode/resources/) ```bash xcode-select --install ``` -- If xcode-select --print-path prints /Library/Developer/CommandLineTools… run `sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer.` +- If `xcode-select --print-path prints /Library/Developer/CommandLineTools…` run `sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer.` * Install [Postgres](https://postgresapp.com) @@ -63,43 +63,34 @@ gh repo clone zed-industries/zed 1. (Optional but recommended) Add your GITHUB_TOKEN to your `.zshrc` or `.bashrc` like this: `export GITHUB_TOKEN=yourGithubAPIToken` 1. (🚧 TODO 🚧 - Will this be relevant for open source?) Ensure the Zed.dev website is checked out in a sibling directory and install its dependencies: - ``` - cd .. - git clone https://github.com/zed-industries/zed.dev - cd zed.dev && npm install - pnpm install -g vercel - ``` + +```bash +cd .. +git clone https://github.com/zed-industries/zed.dev +cd zed.dev && npm install +pnpm install -g vercel +``` + 1. (🚧 TODO 🚧 - Will this be relevant for open source?) Link your zed.dev project to Vercel - - `vercel link` - - Select the `zed-industries` team. If you don't have this get someone on the team to add you to it. - - Select the `zed.dev` project + +- `vercel link` +- Select the `zed-industries` team. If you don't have this get someone on the team to add you to it. +- Select the `zed.dev` project + 1. (🚧 TODO 🚧 - Will this be relevant for open source?) Run `vercel pull` to pull down the environment variables and project info from Vercel 1. Open Postgres.app -1. From `./path/to/zed/`: - - Run: - - `GITHUB_TOKEN={yourGithubAPIToken} script/bootstrap` - - You don't need to include the GITHUB_TOKEN if you exported it above. - - Consider removing the token (if it's fine for you to recreate such tokens during occasional migrations) or store this token somewhere safe (like your Zed 1Password vault). - - If you get: - - ```bash - Error: Cannot install in Homebrew on ARM processor in Intel default prefix (/usr/local)! - Please create a new installation in /opt/homebrew using one of the - "Alternative Installs" from: - https://docs.brew.sh/Installation - ``` - - In that case try: - - `/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"` - - If Homebrew is not in your PATH: - - Replace `{username}` with your home folder name (usually your login name) - - `echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> /Users/{username}/.zprofile` - - `eval "$(/opt/homebrew/bin/brew shellenv)"` +1. From `./path/to/zed/` run `GITHUB_TOKEN={yourGithubAPIToken} script/bootstrap` + +- You don't need to include the GITHUB_TOKEN if you exported it above. +- Consider removing the token (if it's fine for you to recreate such tokens during occasional migrations) or store this token somewhere safe (like your Zed 1Password vault). + 1. To run the Zed app: - - If you are working on zed: - - `cargo run` - - If you are just using the latest version, but not working on zed: - - `cargo run --release` - - If you need to run the collaboration server locally: - - `script/zed-local` + - If you are working on zed: + - `cargo run` + - If you are just using the latest version, but not working on zed: + - `cargo run --release` + - If you need to run the collaboration server locally: + - `script/zed-local` ## Troubleshooting @@ -109,8 +100,21 @@ gh repo clone zed-industries/zed **`xcrun: error: unable to find utility "metal", not a developer tool or in PATH`** +### `script/bootstrap` + +```bash +Error: Cannot install in Homebrew on ARM processor in Intel default prefix (/usr/local)! +Please create a new installation in /opt/homebrew using one of the +"Alternative Installs" from: +https://docs.brew.sh/Installation +``` + +- In that case try `/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"` -### Seeding errors during `script/bootstrap` runs +- If Homebrew is not in your PATH: + - Replace `{username}` with your home folder name (usually your login name) + - `echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> /Users/{username}/.zprofile` + - `eval "$(/opt/homebrew/bin/brew shellenv)"` ``` seeding database... From ed67363ea38e06f8fea4fd761439510fe31c5fff Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 17 Jan 2024 13:24:05 -0500 Subject: [PATCH 13/18] Update README.md FIx typos --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c97f4f5134d82ee6f9aa84c72992233feb47e255..737615623194a958f4d8b8d129dfab50475e2e8b 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ # 🚧 TODO 🚧 -[ ] Add intro -[ ] Add link to contributing guide -[ ] Add barebones running zed from source instructions -[ ] Link out to further dev docs +- [ ] Add intro +- [ ] Add link to contributing guide +- [ ] Add barebones running zed from source instructions +- [ ] Link out to further dev docs # Zed [![CI](https://github.com/zed-industries/zed/actions/workflows/ci.yml/badge.svg)](https://github.com/zed-industries/zed/actions/workflows/ci.yml) -Welcome to Zed, a high-performance, multiplayer code editor from the creators of [Atom]https://github.com/atom/atom) and [Tree-sitter](https://github.com/tree-sitter/tree-sitter). +Welcome to Zed, a high-performance, multiplayer code editor from the creators of [Atom](https://github.com/atom/atom) and [Tree-sitter](https://github.com/tree-sitter/tree-sitter). ## Developing Zed From 4e4a1e0dd1196b814667bf235f4b931d8b936e0e Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 17 Jan 2024 13:32:38 -0500 Subject: [PATCH 14/18] Document the public interface of the `vim` crate (#4093) This PR documents the public interface of the `vim` crate. Release Notes: - N/A --------- Co-authored-by: Conrad --- crates/vim/src/mode_indicator.rs | 11 +++-------- crates/vim/src/vim.rs | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/crates/vim/src/mode_indicator.rs b/crates/vim/src/mode_indicator.rs index 4cd8e6690092d0f5f18528373e14d91f2311b156..b669b16112874a89cd303499cc0d17eab6b99267 100644 --- a/crates/vim/src/mode_indicator.rs +++ b/crates/vim/src/mode_indicator.rs @@ -4,12 +4,14 @@ use workspace::{item::ItemHandle, ui::prelude::*, StatusItemView}; use crate::{state::Mode, Vim}; +/// The ModeIndicator displays the current mode in the status bar. pub struct ModeIndicator { - pub mode: Option, + pub(crate) mode: Option, _subscriptions: Vec, } impl ModeIndicator { + /// Construct a new mode indicator in this window. pub fn new(cx: &mut ViewContext) -> Self { let _subscriptions = vec![ cx.observe_global::(|this, cx| this.update_mode(cx)), @@ -37,13 +39,6 @@ impl ModeIndicator { self.mode = None; } } - - pub fn set_mode(&mut self, mode: Mode, cx: &mut ViewContext) { - if self.mode != Some(mode) { - self.mode = Some(mode); - cx.notify(); - } - } } impl Render for ModeIndicator { diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 3579bf36fe29a7276496a1bbefd95dabf6cc8bc2..e03efb0a64b4b55e92ed8d92d8837351ac573e0d 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -1,3 +1,5 @@ +//! Vim support for Zed. + #[cfg(test)] mod test; @@ -38,12 +40,18 @@ use crate::state::ReplayableAction; /// Default: false pub struct VimModeSetting(pub bool); +/// An Action to Switch between modes #[derive(Clone, Deserialize, PartialEq)] pub struct SwitchMode(pub Mode); +/// PushOperator is used to put vim into a "minor" mode, +/// where it's waiting for a specific next set of keystrokes. +/// For example 'd' needs a motion to complete. #[derive(Clone, Deserialize, PartialEq)] pub struct PushOperator(pub Operator); +/// Number is used to manage vim's count. Pushing a digit +/// multiplis the current value by 10 and adds the digit. #[derive(Clone, Deserialize, PartialEq)] struct Number(usize); @@ -51,11 +59,13 @@ actions!( vim, [Tab, Enter, Object, InnerObject, FindForward, FindBackward] ); + // in the workspace namespace so it's not filtered out when vim is disabled. actions!(workspace, [ToggleVimMode]); impl_actions!(vim, [SwitchMode, PushOperator, Number]); +/// Initializes the `vim` crate. pub fn init(cx: &mut AppContext) { cx.set_global(Vim::default()); VimModeSetting::register(cx); @@ -119,6 +129,7 @@ fn register(workspace: &mut Workspace, cx: &mut ViewContext) { visual::register(workspace, cx); } +/// Registers a keystroke observer to observe keystrokes for the Vim integration. pub fn observe_keystrokes(cx: &mut WindowContext) { cx.observe_keystrokes(|keystroke_event, cx| { if let Some(action) = keystroke_event @@ -160,6 +171,7 @@ pub fn observe_keystrokes(cx: &mut WindowContext) { .detach() } +/// The state pertaining to Vim mode. Stored as a global. #[derive(Default)] pub struct Vim { active_editor: Option>, @@ -251,6 +263,8 @@ impl Vim { Some(editor.update(cx, update)) } + /// When doing an action that modifies the buffer, we start recording so that `.` + /// will replay the action. pub fn start_recording(&mut self, cx: &mut WindowContext) { if !self.workspace_state.replaying { self.workspace_state.recording = true; @@ -295,12 +309,19 @@ impl Vim { } } + /// When finishing an action that modifies the buffer, stop recording. + /// as you usually call this within a keystroke handler we also ensure that + /// the current action is recorded. pub fn stop_recording(&mut self) { if self.workspace_state.recording { self.workspace_state.stop_recording_after_next_action = true; } } + /// Stops recording actions immediately rather than waiting until after the + /// next action to stop recording. + /// + /// This doesn't include the current action. pub fn stop_recording_immediately(&mut self, action: Box) { if self.workspace_state.recording { self.workspace_state @@ -311,6 +332,7 @@ impl Vim { } } + /// Explicitly record one action (equiavlent to start_recording and stop_recording) pub fn record_current_action(&mut self, cx: &mut WindowContext) { self.start_recording(cx); self.stop_recording(); @@ -516,6 +538,7 @@ impl Vim { } } + /// Returns the state of the active editor. pub fn state(&self) -> &EditorState { if let Some(active_editor) = self.active_editor.as_ref() { if let Some(state) = self.editor_states.get(&active_editor.entity_id()) { @@ -526,6 +549,7 @@ impl Vim { &self.default_state } + /// Updates the state of the active editor. pub fn update_state(&mut self, func: impl FnOnce(&mut EditorState) -> T) -> T { let mut state = self.state().clone(); let ret = func(&mut state); From 6734e528a845300c94675431c8b1d0d4d9728c5c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 17 Jan 2024 10:33:32 -0800 Subject: [PATCH 15/18] Revert "Bump livekit client" This reverts commit 5730d0ef2107f2a10fe01595ce34f14f97e289ad. --- crates/live_kit_client/LiveKitBridge/Package.resolved | 8 ++++---- crates/live_kit_client/LiveKitBridge/Package.swift | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/live_kit_client/LiveKitBridge/Package.resolved b/crates/live_kit_client/LiveKitBridge/Package.resolved index bf17ef24c57da952f27caee6c995af0800a73e2e..b925bc8f0d5ef290993fa0d49adcf221dd3570f6 100644 --- a/crates/live_kit_client/LiveKitBridge/Package.resolved +++ b/crates/live_kit_client/LiveKitBridge/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/livekit/client-sdk-swift.git", "state": { "branch": null, - "revision": "8b9cefed8d1669ec8fce41376b56dce3036a5f50", - "version": "1.1.4" + "revision": "7331b813a5ab8a95cfb81fb2b4ed10519428b9ff", + "version": "1.0.12" } }, { @@ -24,8 +24,8 @@ "repositoryURL": "https://github.com/webrtc-sdk/Specs.git", "state": { "branch": null, - "revision": "4fa8d6d647fc759cdd0265fd413d2f28ea2e0e08", - "version": "114.5735.8" + "revision": "2f6bab30c8df0fe59ab3e58bc99097f757f85f65", + "version": "104.5112.17" } }, { diff --git a/crates/live_kit_client/LiveKitBridge/Package.swift b/crates/live_kit_client/LiveKitBridge/Package.swift index abb38efca6a734c935ecdd7438570a3fa6f94230..d7b5c271b95496112b2fc368a851506d54b176b8 100644 --- a/crates/live_kit_client/LiveKitBridge/Package.swift +++ b/crates/live_kit_client/LiveKitBridge/Package.swift @@ -15,7 +15,7 @@ let package = Package( targets: ["LiveKitBridge"]), ], dependencies: [ - .package(url: "https://github.com/livekit/client-sdk-swift.git", .exact("1.1.4")), + .package(url: "https://github.com/livekit/client-sdk-swift.git", .exact("1.0.12")), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. From cf5dc099fb9282946372f067795ae0061b36c416 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 17 Jan 2024 13:38:12 -0500 Subject: [PATCH 16/18] Add more documentation to `collab` (#4095) This PR adds more documentation to the `collab` crate. Release Notes: - N/A --------- Co-authored-by: Conrad --- crates/collab/src/auth.rs | 7 +++ crates/collab/src/db.rs | 26 ++++++++ crates/collab/src/db/ids.rs | 20 ++++++ crates/collab/src/db/queries/access_tokens.rs | 2 + crates/collab/src/db/queries/buffers.rs | 10 ++- crates/collab/src/db/queries/channels.rs | 28 +++++++-- crates/collab/src/db/queries/contacts.rs | 8 +++ crates/collab/src/db/queries/messages.rs | 11 ++++ crates/collab/src/db/queries/notifications.rs | 4 ++ crates/collab/src/db/queries/projects.rs | 18 ++++++ crates/collab/src/db/queries/rooms.rs | 6 ++ crates/collab/src/db/queries/servers.rs | 6 ++ crates/collab/src/db/queries/users.rs | 15 +++++ crates/collab/src/db/tables/user.rs | 1 + crates/collab/src/rpc.rs | 62 +++++++++++++++++++ 15 files changed, 219 insertions(+), 5 deletions(-) diff --git a/crates/collab/src/auth.rs b/crates/collab/src/auth.rs index 9ce602c5778c25efe89017b58b3498108de38038..df3ded28e4a2f1f86ac1421e91dc6be31f7f8408 100644 --- a/crates/collab/src/auth.rs +++ b/crates/collab/src/auth.rs @@ -27,6 +27,8 @@ lazy_static! { .unwrap(); } +/// Validates the authorization header. This has two mechanisms, one for the ADMIN_TOKEN +/// and one for the access tokens that we issue. pub async fn validate_header(mut req: Request, next: Next) -> impl IntoResponse { let mut auth_header = req .headers() @@ -88,6 +90,8 @@ struct AccessTokenJson { token: String, } +/// Creates a new access token to identify the given user. before returning it, you should +/// encrypt it with the user's public key. pub async fn create_access_token(db: &db::Database, user_id: UserId) -> Result { const VERSION: usize = 1; let access_token = rpc::auth::random_token(); @@ -122,6 +126,8 @@ fn hash_access_token(token: &str) -> Result { .to_string()) } +/// Encrypts the given access token with the given public key to avoid leaking it on the way +/// to the client. pub fn encrypt_access_token(access_token: &str, public_key: String) -> Result { let native_app_public_key = rpc::auth::PublicKey::try_from(public_key).context("failed to parse app public key")?; @@ -131,6 +137,7 @@ pub fn encrypt_access_token(access_token: &str, public_key: String) -> Result) -> Result { let token: AccessTokenJson = serde_json::from_str(&token)?; diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index df33416a46b00cea970e05f22b48c08cc02230cf..480dcf6f8595143659069739104608dac4c15fd8 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -47,6 +47,8 @@ pub use ids::*; pub use sea_orm::ConnectOptions; pub use tables::user::Model as User; +/// Database gives you a handle that lets you access the database. +/// It handles pooling internally. pub struct Database { options: ConnectOptions, pool: DatabaseConnection, @@ -62,6 +64,7 @@ pub struct Database { // The `Database` type has so many methods that its impl blocks are split into // separate files in the `queries` folder. impl Database { + /// Connects to the database with the given options pub async fn new(options: ConnectOptions, executor: Executor) -> Result { sqlx::any::install_default_drivers(); Ok(Self { @@ -82,6 +85,7 @@ impl Database { self.rooms.clear(); } + /// Runs the database migrations. pub async fn migrate( &self, migrations_path: &Path, @@ -123,11 +127,15 @@ impl Database { Ok(new_migrations) } + /// Initializes static data that resides in the database by upserting it. pub async fn initialize_static_data(&mut self) -> Result<()> { self.initialize_notification_kinds().await?; Ok(()) } + /// Transaction runs things in a transaction. If you want to call other methods + /// and pass the transaction around you need to reborrow the transaction at each + /// call site with: `&*tx`. pub async fn transaction(&self, f: F) -> Result where F: Send + Fn(TransactionHandle) -> Fut, @@ -160,6 +168,7 @@ impl Database { self.run(body).await } + /// The same as room_transaction, but if you need to only optionally return a Room. async fn optional_room_transaction(&self, f: F) -> Result>> where F: Send + Fn(TransactionHandle) -> Fut, @@ -210,6 +219,9 @@ impl Database { self.run(body).await } + /// room_transaction runs the block in a transaction. It returns a RoomGuard, that keeps + /// the database locked until it is dropped. This ensures that updates sent to clients are + /// properly serialized with respect to database changes. async fn room_transaction(&self, room_id: RoomId, f: F) -> Result> where F: Send + Fn(TransactionHandle) -> Fut, @@ -330,6 +342,7 @@ fn is_serialization_error(error: &Error) -> bool { } } +/// A handle to a [`DatabaseTransaction`]. pub struct TransactionHandle(Arc>); impl Deref for TransactionHandle { @@ -340,6 +353,8 @@ impl Deref for TransactionHandle { } } +/// [`RoomGuard`] keeps a database transaction alive until it is dropped. +/// so that updates to rooms are serialized. pub struct RoomGuard { data: T, _guard: OwnedMutexGuard<()>, @@ -361,6 +376,7 @@ impl DerefMut for RoomGuard { } impl RoomGuard { + /// Returns the inner value of the guard. pub fn into_inner(self) -> T { self.data } @@ -420,12 +436,14 @@ pub struct WaitlistSummary { pub unknown_count: i64, } +/// The parameters to create a new user. #[derive(Debug, Serialize, Deserialize)] pub struct NewUserParams { pub github_login: String, pub github_user_id: i32, } +/// The result of creating a new user. #[derive(Debug)] pub struct NewUserResult { pub user_id: UserId, @@ -434,6 +452,7 @@ pub struct NewUserResult { pub signup_device_id: Option, } +/// The result of moving a channel. #[derive(Debug)] pub struct MoveChannelResult { pub participants_to_update: HashMap, @@ -441,18 +460,21 @@ pub struct MoveChannelResult { pub moved_channels: HashSet, } +/// The result of renaming a channel. #[derive(Debug)] pub struct RenameChannelResult { pub channel: Channel, pub participants_to_update: HashMap, } +/// The result of creating a channel. #[derive(Debug)] pub struct CreateChannelResult { pub channel: Channel, pub participants_to_update: Vec<(UserId, ChannelsForUser)>, } +/// The result of setting a channel's visibility. #[derive(Debug)] pub struct SetChannelVisibilityResult { pub participants_to_update: HashMap, @@ -460,6 +482,7 @@ pub struct SetChannelVisibilityResult { pub channels_to_remove: Vec, } +/// The result of updating a channel membership. #[derive(Debug)] pub struct MembershipUpdated { pub channel_id: ChannelId, @@ -467,12 +490,14 @@ pub struct MembershipUpdated { pub removed_channels: Vec, } +/// The result of setting a member's role. #[derive(Debug)] pub enum SetMemberRoleResult { InviteUpdated(Channel), MembershipUpdated(MembershipUpdated), } +/// The result of inviting a member to a channel. #[derive(Debug)] pub struct InviteMemberResult { pub channel: Channel, @@ -497,6 +522,7 @@ pub struct Channel { pub name: String, pub visibility: ChannelVisibility, pub role: ChannelRole, + /// parent_path is the channel ids from the root to this one (not including this one) pub parent_path: Vec, } diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs index 9dbbfaff8f8d0e94ea56b02eda37485ddc695fa8..a920265b5703e55ef482a88c03facea95194265b 100644 --- a/crates/collab/src/db/ids.rs +++ b/crates/collab/src/db/ids.rs @@ -19,19 +19,23 @@ macro_rules! id_type { Deserialize, DeriveValueType, )] + #[allow(missing_docs)] #[serde(transparent)] pub struct $name(pub i32); impl $name { #[allow(unused)] + #[allow(missing_docs)] pub const MAX: Self = Self(i32::MAX); #[allow(unused)] + #[allow(missing_docs)] pub fn from_proto(value: u64) -> Self { Self(value as i32) } #[allow(unused)] + #[allow(missing_docs)] pub fn to_proto(self) -> u64 { self.0 as u64 } @@ -84,21 +88,28 @@ id_type!(FlagId); id_type!(NotificationId); id_type!(NotificationKindId); +/// ChannelRole gives you permissions for both channels and calls. #[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default, Hash)] #[sea_orm(rs_type = "String", db_type = "String(None)")] pub enum ChannelRole { + /// Admin can read/write and change permissions. #[sea_orm(string_value = "admin")] Admin, + /// Member can read/write, but not change pemissions. #[sea_orm(string_value = "member")] #[default] Member, + /// Guest can read, but not write. + /// (thought they can use the channel chat) #[sea_orm(string_value = "guest")] Guest, + /// Banned may not read. #[sea_orm(string_value = "banned")] Banned, } impl ChannelRole { + /// Returns true if this role is more powerful than the other role. pub fn should_override(&self, other: Self) -> bool { use ChannelRole::*; match self { @@ -109,6 +120,7 @@ impl ChannelRole { } } + /// Returns the maximal role between the two pub fn max(&self, other: Self) -> Self { if self.should_override(other) { *self @@ -117,6 +129,7 @@ impl ChannelRole { } } + /// True if the role allows access to all descendant channels pub fn can_see_all_descendants(&self) -> bool { use ChannelRole::*; match self { @@ -125,6 +138,7 @@ impl ChannelRole { } } + /// True if the role only allows access to public descendant channels pub fn can_only_see_public_descendants(&self) -> bool { use ChannelRole::*; match self { @@ -133,6 +147,7 @@ impl ChannelRole { } } + /// True if the role can share screen/microphone/projects into rooms. pub fn can_publish_to_rooms(&self) -> bool { use ChannelRole::*; match self { @@ -141,6 +156,7 @@ impl ChannelRole { } } + /// True if the role can edit shared projects. pub fn can_edit_projects(&self) -> bool { use ChannelRole::*; match self { @@ -149,6 +165,7 @@ impl ChannelRole { } } + /// True if the role can read shared projects. pub fn can_read_projects(&self) -> bool { use ChannelRole::*; match self { @@ -187,11 +204,14 @@ impl Into for ChannelRole { } } +/// ChannelVisibility controls whether channels are public or private. #[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default, Hash)] #[sea_orm(rs_type = "String", db_type = "String(None)")] pub enum ChannelVisibility { + /// Public channels are visible to anyone with the link. People join with the Guest role by default. #[sea_orm(string_value = "public")] Public, + /// Members channels are only visible to members of this channel or its parents. #[sea_orm(string_value = "members")] #[default] Members, diff --git a/crates/collab/src/db/queries/access_tokens.rs b/crates/collab/src/db/queries/access_tokens.rs index 589b6483dfceb5df285ac67b03edbee493e4705b..e0338189708c41d63b168f37c89e2c62fe3521b9 100644 --- a/crates/collab/src/db/queries/access_tokens.rs +++ b/crates/collab/src/db/queries/access_tokens.rs @@ -2,6 +2,7 @@ use super::*; use sea_orm::sea_query::Query; impl Database { + /// Creates a new access token for the given user. pub async fn create_access_token( &self, user_id: UserId, @@ -39,6 +40,7 @@ impl Database { .await } + /// Retrieves the access token with the given ID. pub async fn get_access_token( &self, access_token_id: AccessTokenId, diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index 9eddb1f6187a80f4f88f8d13e9aff3f4c310941f..85e71d753b972db0515954406819f8c2fc48f9a9 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -9,6 +9,8 @@ pub struct LeftChannelBuffer { } impl Database { + /// Open a channel buffer. Returns the current contents, and adds you to the list of people + /// to notify on changes. pub async fn join_channel_buffer( &self, channel_id: ChannelId, @@ -121,6 +123,7 @@ impl Database { .await } + /// Rejoin a channel buffer (after a connection interruption) pub async fn rejoin_channel_buffers( &self, buffers: &[proto::ChannelBufferVersion], @@ -232,6 +235,7 @@ impl Database { .await } + /// Clear out any buffer collaborators who are no longer collaborating. pub async fn clear_stale_channel_buffer_collaborators( &self, channel_id: ChannelId, @@ -274,6 +278,7 @@ impl Database { .await } + /// Close the channel buffer, and stop receiving updates for it. pub async fn leave_channel_buffer( &self, channel_id: ChannelId, @@ -286,6 +291,7 @@ impl Database { .await } + /// Close the channel buffer, and stop receiving updates for it. pub async fn channel_buffer_connection_lost( &self, connection: ConnectionId, @@ -309,6 +315,7 @@ impl Database { Ok(()) } + /// Close all open channel buffers pub async fn leave_channel_buffers( &self, connection: ConnectionId, @@ -342,7 +349,7 @@ impl Database { .await } - pub async fn leave_channel_buffer_internal( + async fn leave_channel_buffer_internal( &self, channel_id: ChannelId, connection: ConnectionId, @@ -798,6 +805,7 @@ impl Database { Ok(changes) } + /// Returns the latest operations for the buffers with the specified IDs. pub async fn get_latest_operations_for_buffers( &self, buffer_ids: impl IntoIterator, diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 6243b03bf7ad2f331b3ede34c2390db574940546..7ff9f00bc119c12a17987b2b7dff24e354286c33 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -40,6 +40,7 @@ impl Database { .id) } + /// Creates a new channel. pub async fn create_channel( &self, name: &str, @@ -97,6 +98,7 @@ impl Database { .await } + /// Adds a user to the specified channel. pub async fn join_channel( &self, channel_id: ChannelId, @@ -179,6 +181,7 @@ impl Database { .await } + /// Sets the visibiltity of the given channel. pub async fn set_channel_visibility( &self, channel_id: ChannelId, @@ -258,6 +261,7 @@ impl Database { .await } + /// Deletes the channel with the specified ID. pub async fn delete_channel( &self, channel_id: ChannelId, @@ -294,6 +298,7 @@ impl Database { .await } + /// Invites a user to a channel as a member. pub async fn invite_channel_member( &self, channel_id: ChannelId, @@ -349,6 +354,7 @@ impl Database { Ok(new_name) } + /// Renames the specified channel. pub async fn rename_channel( &self, channel_id: ChannelId, @@ -387,6 +393,7 @@ impl Database { .await } + /// accept or decline an invite to join a channel pub async fn respond_to_channel_invite( &self, channel_id: ChannelId, @@ -486,6 +493,7 @@ impl Database { }) } + /// Removes a channel member. pub async fn remove_channel_member( &self, channel_id: ChannelId, @@ -530,6 +538,7 @@ impl Database { .await } + /// Returns all channel invites for the user with the given ID. pub async fn get_channel_invites_for_user(&self, user_id: UserId) -> Result> { self.transaction(|tx| async move { let mut role_for_channel: HashMap = HashMap::default(); @@ -565,6 +574,7 @@ impl Database { .await } + /// Returns all channels for the user with the given ID. pub async fn get_channels_for_user(&self, user_id: UserId) -> Result { self.transaction(|tx| async move { let tx = tx; @@ -574,6 +584,8 @@ impl Database { .await } + /// Returns all channels for the user with the given ID that are descendants + /// of the specified ancestor channel. pub async fn get_user_channels( &self, user_id: UserId, @@ -743,6 +755,7 @@ impl Database { Ok(results) } + /// Sets the role for the specified channel member. pub async fn set_channel_member_role( &self, channel_id: ChannelId, @@ -786,6 +799,7 @@ impl Database { .await } + /// Returns the details for the specified channel member. pub async fn get_channel_participant_details( &self, channel_id: ChannelId, @@ -911,6 +925,7 @@ impl Database { .collect()) } + /// Returns the participants in the given channel. pub async fn get_channel_participants( &self, channel: &channel::Model, @@ -925,6 +940,7 @@ impl Database { .collect()) } + /// Returns whether the given user is an admin in the specified channel. pub async fn check_user_is_channel_admin( &self, channel: &channel::Model, @@ -943,6 +959,7 @@ impl Database { } } + /// Returns whether the given user is a member of the specified channel. pub async fn check_user_is_channel_member( &self, channel: &channel::Model, @@ -958,6 +975,7 @@ impl Database { } } + /// Returns whether the given user is a participant in the specified channel. pub async fn check_user_is_channel_participant( &self, channel: &channel::Model, @@ -975,6 +993,7 @@ impl Database { } } + /// Returns a user's pending invite for the given channel, if one exists. pub async fn pending_invite_for_channel( &self, channel: &channel::Model, @@ -991,7 +1010,7 @@ impl Database { Ok(row) } - pub async fn public_parent_channel( + async fn public_parent_channel( &self, channel: &channel::Model, tx: &DatabaseTransaction, @@ -1003,7 +1022,7 @@ impl Database { Ok(path.pop()) } - pub async fn public_ancestors_including_self( + pub(crate) async fn public_ancestors_including_self( &self, channel: &channel::Model, tx: &DatabaseTransaction, @@ -1018,6 +1037,7 @@ impl Database { Ok(visible_channels) } + /// Returns the role for a user in the given channel. pub async fn channel_role_for_user( &self, channel: &channel::Model, @@ -1143,7 +1163,7 @@ impl Database { .await?) } - /// Returns the channel with the given ID + /// Returns the channel with the given ID. pub async fn get_channel(&self, channel_id: ChannelId, user_id: UserId) -> Result { self.transaction(|tx| async move { let channel = self.get_channel_internal(channel_id, &*tx).await?; @@ -1156,7 +1176,7 @@ impl Database { .await } - pub async fn get_channel_internal( + pub(crate) async fn get_channel_internal( &self, channel_id: ChannelId, tx: &DatabaseTransaction, diff --git a/crates/collab/src/db/queries/contacts.rs b/crates/collab/src/db/queries/contacts.rs index f31f1addbd2b4a210abfa9810f062585a7b656e4..c66c33b80dfd795c79f8ef002b0ee122954232cb 100644 --- a/crates/collab/src/db/queries/contacts.rs +++ b/crates/collab/src/db/queries/contacts.rs @@ -1,6 +1,7 @@ use super::*; impl Database { + /// Retrieves the contacts for the user with the given ID. pub async fn get_contacts(&self, user_id: UserId) -> Result> { #[derive(Debug, FromQueryResult)] struct ContactWithUserBusyStatuses { @@ -86,6 +87,7 @@ impl Database { .await } + /// Returns whether the given user is a busy (on a call). pub async fn is_user_busy(&self, user_id: UserId) -> Result { self.transaction(|tx| async move { let participant = room_participant::Entity::find() @@ -97,6 +99,9 @@ impl Database { .await } + /// Returns whether the user with `user_id_1` has the user with `user_id_2` as a contact. + /// + /// In order for this to return `true`, `user_id_2` must have an accepted invite from `user_id_1`. pub async fn has_contact(&self, user_id_1: UserId, user_id_2: UserId) -> Result { self.transaction(|tx| async move { let (id_a, id_b) = if user_id_1 < user_id_2 { @@ -119,6 +124,7 @@ impl Database { .await } + /// Invite the user with `receiver_id` to be a contact of the user with `sender_id`. pub async fn send_contact_request( &self, sender_id: UserId, @@ -231,6 +237,7 @@ impl Database { .await } + /// Dismisses a contact notification for the given user. pub async fn dismiss_contact_notification( &self, user_id: UserId, @@ -272,6 +279,7 @@ impl Database { .await } + /// Accept or decline a contact request pub async fn respond_to_contact_request( &self, responder_id: UserId, diff --git a/crates/collab/src/db/queries/messages.rs b/crates/collab/src/db/queries/messages.rs index 96942c052df75c9406272da3a563533342f64406..9ee313d91b4f916c69789b2048c1867be207f676 100644 --- a/crates/collab/src/db/queries/messages.rs +++ b/crates/collab/src/db/queries/messages.rs @@ -4,6 +4,7 @@ use sea_orm::TryInsertResult; use time::OffsetDateTime; impl Database { + /// Inserts a record representing a user joining the chat for a given channel. pub async fn join_channel_chat( &self, channel_id: ChannelId, @@ -28,6 +29,7 @@ impl Database { .await } + /// Removes `channel_chat_participant` records associated with the given connection ID. pub async fn channel_chat_connection_lost( &self, connection_id: ConnectionId, @@ -47,6 +49,8 @@ impl Database { Ok(()) } + /// Removes `channel_chat_participant` records associated with the given user ID so they + /// will no longer get chat notifications. pub async fn leave_channel_chat( &self, channel_id: ChannelId, @@ -72,6 +76,9 @@ impl Database { .await } + /// Retrieves the messages in the specified channel. + /// + /// Use `before_message_id` to paginate through the channel's messages. pub async fn get_channel_messages( &self, channel_id: ChannelId, @@ -103,6 +110,7 @@ impl Database { .await } + /// Returns the channel messages with the given IDs. pub async fn get_channel_messages_by_id( &self, user_id: UserId, @@ -190,6 +198,7 @@ impl Database { Ok(messages) } + /// Creates a new channel message. pub async fn create_channel_message( &self, channel_id: ChannelId, @@ -376,6 +385,7 @@ impl Database { Ok(()) } + /// Returns the unseen messages for the given user in the specified channels. pub async fn unseen_channel_messages( &self, user_id: UserId, @@ -449,6 +459,7 @@ impl Database { Ok(changes) } + /// Removes the channel message with the given ID. pub async fn remove_channel_message( &self, channel_id: ChannelId, diff --git a/crates/collab/src/db/queries/notifications.rs b/crates/collab/src/db/queries/notifications.rs index 6f2511c23e7cd383760aa29ec62a65ca30c636d8..57685e141b78e5aa6c4a541c385bf056b1802000 100644 --- a/crates/collab/src/db/queries/notifications.rs +++ b/crates/collab/src/db/queries/notifications.rs @@ -2,6 +2,7 @@ use super::*; use rpc::Notification; impl Database { + /// Initializes the different kinds of notifications by upserting records for them. pub async fn initialize_notification_kinds(&mut self) -> Result<()> { notification_kind::Entity::insert_many(Notification::all_variant_names().iter().map( |kind| notification_kind::ActiveModel { @@ -28,6 +29,7 @@ impl Database { Ok(()) } + /// Returns the notifications for the given recipient. pub async fn get_notifications( &self, recipient_id: UserId, @@ -140,6 +142,7 @@ impl Database { .await } + /// Marks the given notification as read. pub async fn mark_notification_as_read( &self, recipient_id: UserId, @@ -150,6 +153,7 @@ impl Database { .await } + /// Marks the notification with the given ID as read. pub async fn mark_notification_as_read_by_id( &self, recipient_id: UserId, diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs index d82a597038d063405a22aeddd0623d7f9bfc9eb0..f81403a796b0e2402eef5c46124c8b51bc68d2b5 100644 --- a/crates/collab/src/db/queries/projects.rs +++ b/crates/collab/src/db/queries/projects.rs @@ -1,6 +1,7 @@ use super::*; impl Database { + /// Returns the count of all projects, excluding ones marked as admin. pub async fn project_count_excluding_admins(&self) -> Result { #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] enum QueryAs { @@ -21,6 +22,7 @@ impl Database { .await } + /// Shares a project with the given room. pub async fn share_project( &self, room_id: RoomId, @@ -100,6 +102,7 @@ impl Database { .await } + /// Unshares the given project. pub async fn unshare_project( &self, project_id: ProjectId, @@ -126,6 +129,7 @@ impl Database { .await } + /// Updates the worktrees associated with the given project. pub async fn update_project( &self, project_id: ProjectId, @@ -346,6 +350,7 @@ impl Database { .await } + /// Updates the diagnostic summary for the given connection. pub async fn update_diagnostic_summary( &self, update: &proto::UpdateDiagnosticSummary, @@ -401,6 +406,7 @@ impl Database { .await } + /// Starts the language server for the given connection. pub async fn start_language_server( &self, update: &proto::StartLanguageServer, @@ -447,6 +453,7 @@ impl Database { .await } + /// Updates the worktree settings for the given connection. pub async fn update_worktree_settings( &self, update: &proto::UpdateWorktreeSettings, @@ -499,6 +506,7 @@ impl Database { .await } + /// Adds the given connection to the specified project. pub async fn join_project( &self, project_id: ProjectId, @@ -704,6 +712,7 @@ impl Database { .await } + /// Removes the given connection from the specified project. pub async fn leave_project( &self, project_id: ProjectId, @@ -805,6 +814,7 @@ impl Database { .map(|guard| guard.into_inner()) } + /// Returns the host connection for a read-only request to join a shared project. pub async fn host_for_read_only_project_request( &self, project_id: ProjectId, @@ -842,6 +852,7 @@ impl Database { .map(|guard| guard.into_inner()) } + /// Returns the host connection for a request to join a shared project. pub async fn host_for_mutating_project_request( &self, project_id: ProjectId, @@ -927,6 +938,10 @@ impl Database { .await } + /// Returns the connection IDs in the given project. + /// + /// The provided `connection_id` must also be a collaborator in the project, + /// otherwise an error will be returned. pub async fn project_connection_ids( &self, project_id: ProjectId, @@ -976,6 +991,7 @@ impl Database { Ok(guest_connection_ids) } + /// Returns the [`RoomId`] for the given project. pub async fn room_id_for_project(&self, project_id: ProjectId) -> Result { self.transaction(|tx| async move { let project = project::Entity::find_by_id(project_id) @@ -1020,6 +1036,7 @@ impl Database { .await } + /// Adds the given follower connection as a follower of the given leader connection. pub async fn follow( &self, room_id: RoomId, @@ -1050,6 +1067,7 @@ impl Database { .await } + /// Removes the given follower connection as a follower of the given leader connection. pub async fn unfollow( &self, room_id: RoomId, diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index 178cb712ed5df6f0bcaaed45e5fd4d43996d88b1..7434e2d20d006242ef572a5605c597fa13d71ade 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -1,6 +1,7 @@ use super::*; impl Database { + /// Clears all room participants in rooms attached to a stale server. pub async fn clear_stale_room_participants( &self, room_id: RoomId, @@ -78,6 +79,7 @@ impl Database { .await } + /// Returns the incoming calls for user with the given ID. pub async fn incoming_call_for_user( &self, user_id: UserId, @@ -102,6 +104,7 @@ impl Database { .await } + /// Creates a new room. pub async fn create_room( &self, user_id: UserId, @@ -394,6 +397,7 @@ impl Database { Ok(participant_index) } + /// Returns the channel ID for the given room, if it has one. pub async fn channel_id_for_room(&self, room_id: RoomId) -> Result> { self.transaction(|tx| async move { let room: Option = room::Entity::find() @@ -944,6 +948,7 @@ impl Database { .await } + /// Updates the location of a participant in the given room. pub async fn update_room_participant_location( &self, room_id: RoomId, @@ -1004,6 +1009,7 @@ impl Database { .await } + /// Sets the role of a participant in the given room. pub async fn set_room_participant_role( &self, admin_id: UserId, diff --git a/crates/collab/src/db/queries/servers.rs b/crates/collab/src/db/queries/servers.rs index e5ceee88873e0e89ecf8a29beef587b00c9baaf9..c79b00eee88654fbbe16ae6c85db159aee4b1a0e 100644 --- a/crates/collab/src/db/queries/servers.rs +++ b/crates/collab/src/db/queries/servers.rs @@ -1,6 +1,7 @@ use super::*; impl Database { + /// Creates a new server in the given environment. pub async fn create_server(&self, environment: &str) -> Result { self.transaction(|tx| async move { let server = server::ActiveModel { @@ -14,6 +15,10 @@ impl Database { .await } + /// Returns the IDs of resources associated with stale servers. + /// + /// A server is stale if it is in the specified `environment` and does not + /// match the provided `new_server_id`. pub async fn stale_server_resource_ids( &self, environment: &str, @@ -61,6 +66,7 @@ impl Database { .await } + /// Deletes any stale servers in the environment that don't match the `new_server_id`. pub async fn delete_stale_servers( &self, environment: &str, diff --git a/crates/collab/src/db/queries/users.rs b/crates/collab/src/db/queries/users.rs index 27e64e25981ecdbd31e6aa337875e1ff81852b9c..954ec5f0d80c5b58149cd935abca648fa40a82ef 100644 --- a/crates/collab/src/db/queries/users.rs +++ b/crates/collab/src/db/queries/users.rs @@ -1,6 +1,7 @@ use super::*; impl Database { + /// Creates a new user. pub async fn create_user( &self, email_address: &str, @@ -35,11 +36,13 @@ impl Database { .await } + /// Returns a user by ID. There are no access checks here, so this should only be used internally. pub async fn get_user_by_id(&self, id: UserId) -> Result> { self.transaction(|tx| async move { Ok(user::Entity::find_by_id(id).one(&*tx).await?) }) .await } + /// Returns all users by ID. There are no access checks here, so this should only be used internally. pub async fn get_users_by_ids(&self, ids: Vec) -> Result> { self.transaction(|tx| async { let tx = tx; @@ -51,6 +54,7 @@ impl Database { .await } + /// Returns a user by GitHub login. There are no access checks here, so this should only be used internally. pub async fn get_user_by_github_login(&self, github_login: &str) -> Result> { self.transaction(|tx| async move { Ok(user::Entity::find() @@ -111,6 +115,8 @@ impl Database { .await } + /// get_all_users returns the next page of users. To get more call again with + /// the same limit and the page incremented by 1. pub async fn get_all_users(&self, page: u32, limit: u32) -> Result> { self.transaction(|tx| async move { Ok(user::Entity::find() @@ -123,6 +129,7 @@ impl Database { .await } + /// Returns the metrics id for the user. pub async fn get_user_metrics_id(&self, id: UserId) -> Result { #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] enum QueryAs { @@ -142,6 +149,7 @@ impl Database { .await } + /// Set "connected_once" on the user for analytics. pub async fn set_user_connected_once(&self, id: UserId, connected_once: bool) -> Result<()> { self.transaction(|tx| async move { user::Entity::update_many() @@ -157,6 +165,7 @@ impl Database { .await } + /// hard delete the user. pub async fn destroy_user(&self, id: UserId) -> Result<()> { self.transaction(|tx| async move { access_token::Entity::delete_many() @@ -169,6 +178,7 @@ impl Database { .await } + /// Find users where github_login ILIKE name_query. pub async fn fuzzy_search_users(&self, name_query: &str, limit: u32) -> Result> { self.transaction(|tx| async { let tx = tx; @@ -193,6 +203,8 @@ impl Database { .await } + /// fuzzy_like_string creates a string for matching in-order using fuzzy_search_users. + /// e.g. "cir" would become "%c%i%r%" pub fn fuzzy_like_string(string: &str) -> String { let mut result = String::with_capacity(string.len() * 2 + 1); for c in string.chars() { @@ -205,6 +217,7 @@ impl Database { result } + /// Creates a new feature flag. pub async fn create_user_flag(&self, flag: &str) -> Result { self.transaction(|tx| async move { let flag = feature_flag::Entity::insert(feature_flag::ActiveModel { @@ -220,6 +233,7 @@ impl Database { .await } + /// Add the given user to the feature flag pub async fn add_user_flag(&self, user: UserId, flag: FlagId) -> Result<()> { self.transaction(|tx| async move { user_feature::Entity::insert(user_feature::ActiveModel { @@ -234,6 +248,7 @@ impl Database { .await } + /// Return the active flags for the user. pub async fn get_user_flags(&self, user: UserId) -> Result> { self.transaction(|tx| async move { #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] diff --git a/crates/collab/src/db/tables/user.rs b/crates/collab/src/db/tables/user.rs index 739693527f00a594f3376a6093dc8c0b1d270a8f..53866b5c54f96a2e3b42c06515acba4a341bead3 100644 --- a/crates/collab/src/db/tables/user.rs +++ b/crates/collab/src/db/tables/user.rs @@ -2,6 +2,7 @@ use crate::db::UserId; use sea_orm::entity::prelude::*; use serde::Serialize; +/// A user model. #[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel, Serialize)] #[sea_orm(table_name = "users")] pub struct Model { diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 5d7f68caac9013bf491d1cd37929b1268d075aea..9406b4938a8f2795a89cd8f10238964710eb61f3 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -932,11 +932,13 @@ async fn connection_lost( Ok(()) } +/// Acknowledges a ping from a client, used to keep the connection alive. async fn ping(_: proto::Ping, response: Response, _session: Session) -> Result<()> { response.send(proto::Ack {})?; Ok(()) } +/// Create a new room for calling (outside of channels) async fn create_room( _request: proto::CreateRoom, response: Response, @@ -984,6 +986,7 @@ async fn create_room( Ok(()) } +/// Join a room from an invitation. Equivalent to joining a channel if there is one. async fn join_room( request: proto::JoinRoom, response: Response, @@ -1058,6 +1061,7 @@ async fn join_room( Ok(()) } +/// Rejoin room is used to reconnect to a room after connection errors. async fn rejoin_room( request: proto::RejoinRoom, response: Response, @@ -1249,6 +1253,7 @@ async fn rejoin_room( Ok(()) } +/// leave room disonnects from the room. async fn leave_room( _: proto::LeaveRoom, response: Response, @@ -1259,6 +1264,7 @@ async fn leave_room( Ok(()) } +/// Update the permissions of someone else in the room. async fn set_room_participant_role( request: proto::SetRoomParticipantRole, response: Response, @@ -1303,6 +1309,7 @@ async fn set_room_participant_role( Ok(()) } +/// Call someone else into the current room async fn call( request: proto::Call, response: Response, @@ -1371,6 +1378,7 @@ async fn call( Err(anyhow!("failed to ring user"))? } +/// Cancel an outgoing call. async fn cancel_call( request: proto::CancelCall, response: Response, @@ -1408,6 +1416,7 @@ async fn cancel_call( Ok(()) } +/// Decline an incoming call. async fn decline_call(message: proto::DeclineCall, session: Session) -> Result<()> { let room_id = RoomId::from_proto(message.room_id); { @@ -1439,6 +1448,7 @@ async fn decline_call(message: proto::DeclineCall, session: Session) -> Result<( Ok(()) } +/// Update other participants in the room with your current location. async fn update_participant_location( request: proto::UpdateParticipantLocation, response: Response, @@ -1459,6 +1469,7 @@ async fn update_participant_location( Ok(()) } +/// Share a project into the room. async fn share_project( request: proto::ShareProject, response: Response, @@ -1481,6 +1492,7 @@ async fn share_project( Ok(()) } +/// Unshare a project from the room. async fn unshare_project(message: proto::UnshareProject, session: Session) -> Result<()> { let project_id = ProjectId::from_proto(message.project_id); @@ -1500,6 +1512,7 @@ async fn unshare_project(message: proto::UnshareProject, session: Session) -> Re Ok(()) } +/// Join someone elses shared project. async fn join_project( request: proto::JoinProject, response: Response, @@ -1625,6 +1638,7 @@ async fn join_project( Ok(()) } +/// Leave someone elses shared project. async fn leave_project(request: proto::LeaveProject, session: Session) -> Result<()> { let sender_id = session.connection_id; let project_id = ProjectId::from_proto(request.project_id); @@ -1647,6 +1661,7 @@ async fn leave_project(request: proto::LeaveProject, session: Session) -> Result Ok(()) } +/// Update other participants with changes to the project async fn update_project( request: proto::UpdateProject, response: Response, @@ -1673,6 +1688,7 @@ async fn update_project( Ok(()) } +/// Update other participants with changes to the worktree async fn update_worktree( request: proto::UpdateWorktree, response: Response, @@ -1697,6 +1713,7 @@ async fn update_worktree( Ok(()) } +/// Update other participants with changes to the diagnostics async fn update_diagnostic_summary( message: proto::UpdateDiagnosticSummary, session: Session, @@ -1720,6 +1737,7 @@ async fn update_diagnostic_summary( Ok(()) } +/// Update other participants with changes to the worktree settings async fn update_worktree_settings( message: proto::UpdateWorktreeSettings, session: Session, @@ -1743,6 +1761,7 @@ async fn update_worktree_settings( Ok(()) } +/// Notify other participants that a language server has started. async fn start_language_server( request: proto::StartLanguageServer, session: Session, @@ -1765,6 +1784,7 @@ async fn start_language_server( Ok(()) } +/// Notify other participants that a language server has changed. async fn update_language_server( request: proto::UpdateLanguageServer, session: Session, @@ -1787,6 +1807,8 @@ async fn update_language_server( Ok(()) } +/// forward a project request to the host. These requests should be read only +/// as guests are allowed to send them. async fn forward_read_only_project_request( request: T, response: Response, @@ -1809,6 +1831,8 @@ where Ok(()) } +/// forward a project request to the host. These requests are disallowed +/// for guests. async fn forward_mutating_project_request( request: T, response: Response, @@ -1831,6 +1855,7 @@ where Ok(()) } +/// Notify other participants that a new buffer has been created async fn create_buffer_for_peer( request: proto::CreateBufferForPeer, session: Session, @@ -1850,6 +1875,8 @@ async fn create_buffer_for_peer( Ok(()) } +/// Notify other participants that a buffer has been updated. This is +/// allowed for guests as long as the update is limited to selections. async fn update_buffer( request: proto::UpdateBuffer, response: Response, @@ -1909,6 +1936,7 @@ async fn update_buffer( Ok(()) } +/// Notify other participants that a project has been updated. async fn broadcast_project_message_from_host>( request: T, session: Session, @@ -1932,6 +1960,7 @@ async fn broadcast_project_message_from_host, @@ -1969,6 +1998,7 @@ async fn follow( Ok(()) } +/// Stop following another user in a call. async fn unfollow(request: proto::Unfollow, session: Session) -> Result<()> { let room_id = RoomId::from_proto(request.room_id); let project_id = request.project_id.map(ProjectId::from_proto); @@ -2000,6 +2030,7 @@ async fn unfollow(request: proto::Unfollow, session: Session) -> Result<()> { Ok(()) } +/// Notify everyone following you of your current location. async fn update_followers(request: proto::UpdateFollowers, session: Session) -> Result<()> { let room_id = RoomId::from_proto(request.room_id); let database = session.db.lock().await; @@ -2036,6 +2067,7 @@ async fn update_followers(request: proto::UpdateFollowers, session: Session) -> Ok(()) } +/// Get public data about users. async fn get_users( request: proto::GetUsers, response: Response, @@ -2062,6 +2094,7 @@ async fn get_users( Ok(()) } +/// Search for users (to invite) buy Github login async fn fuzzy_search_users( request: proto::FuzzySearchUsers, response: Response, @@ -2092,6 +2125,7 @@ async fn fuzzy_search_users( Ok(()) } +/// Send a contact request to another user. async fn request_contact( request: proto::RequestContact, response: Response, @@ -2138,6 +2172,7 @@ async fn request_contact( Ok(()) } +/// Accept or decline a contact request async fn respond_to_contact_request( request: proto::RespondToContactRequest, response: Response, @@ -2195,6 +2230,7 @@ async fn respond_to_contact_request( Ok(()) } +/// Remove a contact. async fn remove_contact( request: proto::RemoveContact, response: Response, @@ -2245,6 +2281,7 @@ async fn remove_contact( Ok(()) } +/// Create a new channel. async fn create_channel( request: proto::CreateChannel, response: Response, @@ -2279,6 +2316,7 @@ async fn create_channel( Ok(()) } +/// Delete a channel async fn delete_channel( request: proto::DeleteChannel, response: Response, @@ -2308,6 +2346,7 @@ async fn delete_channel( Ok(()) } +/// Invite someone to join a channel. async fn invite_channel_member( request: proto::InviteChannelMember, response: Response, @@ -2344,6 +2383,7 @@ async fn invite_channel_member( Ok(()) } +/// remove someone from a channel async fn remove_channel_member( request: proto::RemoveChannelMember, response: Response, @@ -2385,6 +2425,7 @@ async fn remove_channel_member( Ok(()) } +/// Toggle the channel between public and private async fn set_channel_visibility( request: proto::SetChannelVisibility, response: Response, @@ -2423,6 +2464,7 @@ async fn set_channel_visibility( Ok(()) } +/// Alter the role for a user in the channel async fn set_channel_member_role( request: proto::SetChannelMemberRole, response: Response, @@ -2470,6 +2512,7 @@ async fn set_channel_member_role( Ok(()) } +/// Change the name of a channel async fn rename_channel( request: proto::RenameChannel, response: Response, @@ -2503,6 +2546,7 @@ async fn rename_channel( Ok(()) } +/// Move a channel to a new parent. async fn move_channel( request: proto::MoveChannel, response: Response, @@ -2555,6 +2599,7 @@ async fn notify_channel_moved(result: Option, session: Sessio Ok(()) } +/// Get the list of channel members async fn get_channel_members( request: proto::GetChannelMembers, response: Response, @@ -2569,6 +2614,7 @@ async fn get_channel_members( Ok(()) } +/// Accept or decline a channel invitation. async fn respond_to_channel_invite( request: proto::RespondToChannelInvite, response: Response, @@ -2609,6 +2655,7 @@ async fn respond_to_channel_invite( Ok(()) } +/// Join the channels' room async fn join_channel( request: proto::JoinChannel, response: Response, @@ -2713,6 +2760,7 @@ async fn join_channel_internal( Ok(()) } +/// Start editing the channel notes async fn join_channel_buffer( request: proto::JoinChannelBuffer, response: Response, @@ -2744,6 +2792,7 @@ async fn join_channel_buffer( Ok(()) } +/// Edit the channel notes async fn update_channel_buffer( request: proto::UpdateChannelBuffer, session: Session, @@ -2790,6 +2839,7 @@ async fn update_channel_buffer( Ok(()) } +/// Rejoin the channel notes after a connection blip async fn rejoin_channel_buffers( request: proto::RejoinChannelBuffers, response: Response, @@ -2824,6 +2874,7 @@ async fn rejoin_channel_buffers( Ok(()) } +/// Stop editing the channel notes async fn leave_channel_buffer( request: proto::LeaveChannelBuffer, response: Response, @@ -2885,6 +2936,7 @@ fn send_notifications( } } +/// Send a message to the channel async fn send_channel_message( request: proto::SendChannelMessage, response: Response, @@ -2973,6 +3025,7 @@ async fn send_channel_message( Ok(()) } +/// Delete a channel message async fn remove_channel_message( request: proto::RemoveChannelMessage, response: Response, @@ -2992,6 +3045,7 @@ async fn remove_channel_message( Ok(()) } +/// Mark a channel message as read async fn acknowledge_channel_message( request: proto::AckChannelMessage, session: Session, @@ -3011,6 +3065,7 @@ async fn acknowledge_channel_message( Ok(()) } +/// Mark a buffer version as synced async fn acknowledge_buffer_version( request: proto::AckBufferOperation, session: Session, @@ -3029,6 +3084,7 @@ async fn acknowledge_buffer_version( Ok(()) } +/// Start receiving chat updates for a channel async fn join_channel_chat( request: proto::JoinChannelChat, response: Response, @@ -3049,6 +3105,7 @@ async fn join_channel_chat( Ok(()) } +/// Stop receiving chat updates for a channel async fn leave_channel_chat(request: proto::LeaveChannelChat, session: Session) -> Result<()> { let channel_id = ChannelId::from_proto(request.channel_id); session @@ -3059,6 +3116,7 @@ async fn leave_channel_chat(request: proto::LeaveChannelChat, session: Session) Ok(()) } +/// Retrive the chat history for a channel async fn get_channel_messages( request: proto::GetChannelMessages, response: Response, @@ -3082,6 +3140,7 @@ async fn get_channel_messages( Ok(()) } +/// Retrieve specific chat messages async fn get_channel_messages_by_id( request: proto::GetChannelMessagesById, response: Response, @@ -3104,6 +3163,7 @@ async fn get_channel_messages_by_id( Ok(()) } +/// Retrieve the current users notifications async fn get_notifications( request: proto::GetNotifications, response: Response, @@ -3127,6 +3187,7 @@ async fn get_notifications( Ok(()) } +/// Mark notifications as read async fn mark_notification_as_read( request: proto::MarkNotificationRead, response: Response, @@ -3148,6 +3209,7 @@ async fn mark_notification_as_read( Ok(()) } +/// Get the current users information async fn get_private_user_info( _request: proto::GetPrivateUserInfo, response: Response, From d7503a7d47093ae973d18d0a1cd7ec85ec116e77 Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 17 Jan 2024 13:39:37 -0500 Subject: [PATCH 17/18] Document LSP crate Co-Authored-By: Thorsten Ball --- crates/lsp/src/lsp.rs | 67 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 58 insertions(+), 9 deletions(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 788c424373deca7c1490dd954fa005e0943d8a99..0c1574f5aaff21253c1d1eda695939c801283f2e 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -39,6 +39,7 @@ type NotificationHandler = Box, &str, AsyncAppCon type ResponseHandler = Box)>; type IoHandler = Box; +/// Kind of language server stdio given to an IO handler. #[derive(Debug, Clone, Copy)] pub enum IoKind { StdOut, @@ -46,12 +47,15 @@ pub enum IoKind { StdErr, } +/// Represents a launchable language server. This can either be a standalone binary or the path +/// to a runtime with arguments to instruct it to launch the actual language server file. #[derive(Debug, Clone, Deserialize)] pub struct LanguageServerBinary { pub path: PathBuf, pub arguments: Vec, } +/// A running language server process. pub struct LanguageServer { server_id: LanguageServerId, next_id: AtomicUsize, @@ -70,10 +74,12 @@ pub struct LanguageServer { _server: Option>, } +/// Identifies a running language server. #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[repr(transparent)] pub struct LanguageServerId(pub usize); +/// Handle to a language server RPC activity subscription. pub enum Subscription { Notification { method: &'static str, @@ -85,6 +91,9 @@ pub enum Subscription { }, } +/// Language server protocol RPC request message. +/// +/// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#requestMessage) #[derive(Serialize, Deserialize)] pub struct Request<'a, T> { jsonrpc: &'static str, @@ -93,6 +102,7 @@ pub struct Request<'a, T> { params: T, } +/// Language server protocol RPC request response message before it is deserialized into a concrete type. #[derive(Serialize, Deserialize)] struct AnyResponse<'a> { jsonrpc: &'a str, @@ -103,6 +113,9 @@ struct AnyResponse<'a> { result: Option<&'a RawValue>, } +/// Language server protocol RPC request response message. +/// +/// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#responseMessage) #[derive(Serialize)] struct Response { jsonrpc: &'static str, @@ -111,6 +124,9 @@ struct Response { error: Option, } +/// Language server protocol RPC notification message. +/// +/// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#notificationMessage) #[derive(Serialize, Deserialize)] struct Notification<'a, T> { jsonrpc: &'static str, @@ -119,6 +135,7 @@ struct Notification<'a, T> { params: T, } +/// Language server RPC notification message before it is deserialized into a concrete type. #[derive(Debug, Clone, Deserialize)] struct AnyNotification<'a> { #[serde(default)] @@ -135,6 +152,7 @@ struct Error { } impl LanguageServer { + /// Starts a language server process. pub fn new( stderr_capture: Arc>>, server_id: LanguageServerId, @@ -277,6 +295,7 @@ impl LanguageServer { } } + /// List of code action kinds this language server reports being able to emit. pub fn code_action_kinds(&self) -> Option> { self.code_action_kinds.clone() } @@ -427,9 +446,10 @@ impl LanguageServer { Ok(()) } - /// Initializes a language server. - /// Note that `options` is used directly to construct [`InitializeParams`], - /// which is why it is owned. + /// Initializes a language server by sending the `Initialize` request. + /// Note that `options` is used directly to construct [`InitializeParams`], which is why it is owned. + /// + /// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize) pub async fn initialize(mut self, options: Option) -> Result> { let root_uri = Url::from_file_path(&self.root_path).unwrap(); #[allow(deprecated)] @@ -564,6 +584,7 @@ impl LanguageServer { Ok(Arc::new(self)) } + /// Sends a shutdown request to the language server process and prepares the `LanguageServer` to be dropped. pub fn shutdown(&self) -> Option>> { if let Some(tasks) = self.io_tasks.lock().take() { let response_handlers = self.response_handlers.clone(); @@ -598,6 +619,9 @@ impl LanguageServer { } } + /// Register a handler to handle incoming LSP notifications. + /// + /// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#notificationMessage) #[must_use] pub fn on_notification(&self, f: F) -> Subscription where @@ -607,6 +631,9 @@ impl LanguageServer { self.on_custom_notification(T::METHOD, f) } + /// Register a handler to handle incoming LSP requests. + /// + /// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#requestMessage) #[must_use] pub fn on_request(&self, f: F) -> Subscription where @@ -618,6 +645,7 @@ impl LanguageServer { self.on_custom_request(T::METHOD, f) } + /// Register a handler to inspect all language server process stdio. #[must_use] pub fn on_io(&self, f: F) -> Subscription where @@ -631,20 +659,23 @@ impl LanguageServer { } } + /// Removes a request handler registers via [Self::on_request]. pub fn remove_request_handler(&self) { self.notification_handlers.lock().remove(T::METHOD); } + /// Removes a notification handler registers via [Self::on_notification]. pub fn remove_notification_handler(&self) { self.notification_handlers.lock().remove(T::METHOD); } + /// Checks if a notification handler has been registered via [Self::on_notification]. pub fn has_notification_handler(&self) -> bool { self.notification_handlers.lock().contains_key(T::METHOD) } #[must_use] - pub fn on_custom_notification(&self, method: &'static str, mut f: F) -> Subscription + fn on_custom_notification(&self, method: &'static str, mut f: F) -> Subscription where F: 'static + FnMut(Params, AsyncAppContext) + Send, Params: DeserializeOwned, @@ -668,11 +699,7 @@ impl LanguageServer { } #[must_use] - pub fn on_custom_request( - &self, - method: &'static str, - mut f: F, - ) -> Subscription + fn on_custom_request(&self, method: &'static str, mut f: F) -> Subscription where F: 'static + FnMut(Params, AsyncAppContext) -> Fut + Send, Fut: 'static + Future>, @@ -750,22 +777,29 @@ impl LanguageServer { } } + /// Get the name of the running language server. pub fn name(&self) -> &str { &self.name } + /// Get the reported capabilities of the running language server. pub fn capabilities(&self) -> &ServerCapabilities { &self.capabilities } + /// Get the id of the running language server. pub fn server_id(&self) -> LanguageServerId { self.server_id } + /// Get the root path of the project the language server is running against. pub fn root_path(&self) -> &PathBuf { &self.root_path } + /// Sends a RPC request to the language server. + /// + /// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#requestMessage) pub fn request( &self, params: T::Params, @@ -851,6 +885,9 @@ impl LanguageServer { } } + /// Sends a RPC notification to the language server. + /// + /// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#notificationMessage) pub fn notify(&self, params: T::Params) -> Result<()> { Self::notify_internal::(&self.outbound_tx, params) } @@ -879,6 +916,7 @@ impl Drop for LanguageServer { } impl Subscription { + /// Detaching a subscription handle prevents it from unsubscribing on drop. pub fn detach(&mut self) { match self { Subscription::Notification { @@ -925,6 +963,7 @@ impl Drop for Subscription { } } +/// Mock language server for use in tests. #[cfg(any(test, feature = "test-support"))] #[derive(Clone)] pub struct FakeLanguageServer { @@ -946,6 +985,7 @@ impl LanguageServer { } } + /// Construct a fake language server. pub fn fake( name: String, capabilities: ServerCapabilities, @@ -1015,10 +1055,12 @@ impl LanguageServer { #[cfg(any(test, feature = "test-support"))] impl FakeLanguageServer { + /// See [LanguageServer::notify] pub fn notify(&self, params: T::Params) { self.server.notify::(params).ok(); } + /// See [LanguageServer::request] pub async fn request(&self, params: T::Params) -> Result where T: request::Request, @@ -1028,11 +1070,13 @@ impl FakeLanguageServer { self.server.request::(params).await } + /// Attempts [try_receive_notification], unwrapping if it has not received the specified type yet. pub async fn receive_notification(&mut self) -> T::Params { self.server.executor.start_waiting(); self.try_receive_notification::().await.unwrap() } + /// Consumes the notification channel until it finds a notification for the specified type. pub async fn try_receive_notification( &mut self, ) -> Option { @@ -1048,6 +1092,7 @@ impl FakeLanguageServer { } } + /// Registers a handler for a specific kind of request. Removes any existing handler for specified request type. pub fn handle_request( &self, mut handler: F, @@ -1076,6 +1121,7 @@ impl FakeLanguageServer { responded_rx } + /// Registers a handler for a specific kind of notification. Removes any existing handler for specified notification type. pub fn handle_notification( &self, mut handler: F, @@ -1096,6 +1142,7 @@ impl FakeLanguageServer { handled_rx } + /// Removes any existing handler for specified notification type. pub fn remove_request_handler(&mut self) where T: 'static + request::Request, @@ -1103,6 +1150,7 @@ impl FakeLanguageServer { self.server.remove_request_handler::(); } + /// Simulate that the server has started work and notifies about its progress with the specified token. pub async fn start_progress(&self, token: impl Into) { let token = token.into(); self.request::(WorkDoneProgressCreateParams { @@ -1116,6 +1164,7 @@ impl FakeLanguageServer { }); } + /// Simulate that the server has completed work and notifies about that with the specified token. pub fn end_progress(&self, token: impl Into) { self.notify::(ProgressParams { token: NumberOrString::String(token.into()), From 552d2c26f5096e89632e4b6b1b51c23258f49b2d Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 17 Jan 2024 11:41:42 -0700 Subject: [PATCH 18/18] Also update chat location when opening a new workspace This happens a lot in guest workflows where they open the call with a link and are jumped straight to a shared workspace. --- crates/collab_ui/src/chat_panel.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index 140d5be4c6548664d1518ff3b6eff5583d3e22ab..44b0669c307bcb491e53e0f07ab48ae461d7f6b3 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -125,6 +125,23 @@ impl ChatPanel { open_context_menu: None, }; + if let Some(channel_id) = ActiveCall::global(cx) + .read(cx) + .room() + .and_then(|room| room.read(cx).channel_id()) + { + this.select_channel(channel_id, None, cx) + .detach_and_log_err(cx); + + if ActiveCall::global(cx) + .read(cx) + .room() + .is_some_and(|room| room.read(cx).contains_guests()) + { + cx.emit(PanelEvent::Activate) + } + } + this.subscriptions.push(cx.subscribe( &ActiveCall::global(cx), move |this: &mut Self, call, event: &room::Event, cx| match event {