Compare commits

..

1 Commits

Author SHA1 Message Date
Michael Sloan
2a55062dd1 Refactor multibuffer methods for multibuffer range -> buffer range
* Renames `excerpts_for_ranges` to `disjoint_ranges_to_buffer_ranges`,
and modifies it to return `MultiBufferExcerpt` since it provides more
information and was already being built.

* `range_to_buffer_ranges` now returns an iterator, should improve
efficiency in a few places.  Now implemented by just calling
`disjoint_ranges_to_buffer_ranges`.
2025-01-06 19:05:57 -07:00
205 changed files with 2750 additions and 6109 deletions

View File

@@ -39,13 +39,10 @@ jobs:
# When we're running in a merge queue, never assume that the changes
# are docs-only, as there could be other PRs in the group that
# contain non-docs changes.
echo "Running in the merge queue"
echo "docs_only=false" >> $GITHUB_OUTPUT
elif git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.sha }} | grep -qvE '^docs/'; then
echo "Detected non-docs changes"
echo "docs_only=false" >> $GITHUB_OUTPUT
else
echo "Docs-only change"
echo "docs_only=true" >> $GITHUB_OUTPUT
fi
@@ -322,6 +319,9 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Generate license file
run: script/generate-licenses
- name: Create macOS app bundle
run: script/bundle-mac

View File

@@ -86,6 +86,9 @@ jobs:
echo "Publishing version: ${version} on release channel nightly"
echo "nightly" > crates/zed/RELEASE_CHANNEL
- name: Generate license file
run: script/generate-licenses
- name: Create macOS app bundle
run: script/bundle-mac

786
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -391,7 +391,7 @@ ignore = "0.4.22"
image = "0.25.1"
indexmap = { version = "2.7.0", features = ["serde"] }
indoc = "2"
itertools = "0.14.0"
itertools = "0.13.0"
jsonwebtoken = "9.3"
jupyter-protocol = { version = "0.5.0" }
jupyter-websocket-client = { version = "0.8.0" }
@@ -409,13 +409,12 @@ ordered-float = "2.1.1"
palette = { version = "0.7.5", default-features = false, features = ["std"] }
parking_lot = "0.12.1"
pathdiff = "0.2"
pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1abe5cec5ebfbe97ca71746a4cfc7fe89bddf8e0" }
pet-fs = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1abe5cec5ebfbe97ca71746a4cfc7fe89bddf8e0" }
pet-pixi = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1abe5cec5ebfbe97ca71746a4cfc7fe89bddf8e0" }
pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1abe5cec5ebfbe97ca71746a4cfc7fe89bddf8e0" }
pet-core = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1abe5cec5ebfbe97ca71746a4cfc7fe89bddf8e0" }
pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1abe5cec5ebfbe97ca71746a4cfc7fe89bddf8e0" }
pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1abe5cec5ebfbe97ca71746a4cfc7fe89bddf8e0" }
pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
pet-fs = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
pet-core = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
postage = { version = "0.5", features = ["futures-traits"] }
pretty_assertions = { version = "1.3.0", features = ["unstable"] }
profiling = "1"
@@ -512,14 +511,14 @@ url = "2.2"
uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] }
wasmparser = "0.215"
wasm-encoder = "0.215"
wasmtime = { version = "26", default-features = false, features = [
wasmtime = { version = "24", default-features = false, features = [
"async",
"demangle",
"runtime",
"cranelift",
"component-model",
] }
wasmtime-wasi = "26"
wasmtime-wasi = "24"
which = "6.0.0"
wit-component = "0.201"
zstd = "0.11"

View File

@@ -1,4 +1 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.4286 9H10.5714C9.70355 9 9 9.70355 9 10.5714V18.4286C9 19.2964 9.70355 20 10.5714 20H18.4286C19.2964 20 20 19.2964 20 18.4286V10.5714C20 9.70355 19.2964 9 18.4286 9Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.57143 15C4.70714 15 4 14.2929 4 13.4286V5.57143C4 4.70714 4.70714 4 5.57143 4H13.4286C14.2929 4 15 4.70714 15 5.57143" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>

Before

Width:  |  Height:  |  Size: 576 B

After

Width:  |  Height:  |  Size: 338 B

View File

@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.9416 2.99643C13.08 2.79636 12.9568 2.5 12.7352 2.5H3.26475C3.04317 2.5 2.91999 2.79636 3.0584 2.99643L6.04033 7.30646C6.24713 7.60535 6.35981 7.97674 6.35981 8.3596C6.35981 9.18422 6.35981 11.4639 6.35981 12.891C6.35981 13.2285 6.59643 13.5 6.88831 13.5H9.11168C9.40357 13.5 9.64019 13.2285 9.64019 12.891C9.64019 11.4639 9.64019 9.18422 9.64019 8.3596C9.64019 7.97674 9.75289 7.60535 9.95969 7.30646L12.9416 2.99643Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.6749 2.40608C11.8058 2.24239 11.6893 1.99991 11.4796 1.99991H2.51996C2.31033 1.99991 2.19379 2.24239 2.32474 2.40608L5.14583 5.93246C5.34148 6.17701 5.44808 6.48087 5.44808 6.79412C5.44808 7.46881 5.44808 10.334 5.44808 11.5016C5.44808 11.7778 5.67194 11.9999 5.94808 11.9999H8.05153C8.32767 11.9999 8.55153 11.7778 8.55153 11.5016C8.55153 10.334 8.55153 7.46881 8.55153 6.79412C8.55153 6.48087 8.65815 6.17701 8.8538 5.93246L11.6749 2.40608Z" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 618 B

After

Width:  |  Height:  |  Size: 644 B

View File

@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13 13L10.4138 10.4138M3 7.31034C3 4.92981 4.92981 3 7.31034 3C9.6909 3 11.6207 4.92981 11.6207 7.31034C11.6207 9.6909 9.6909 11.6207 7.31034 11.6207C4.92981 11.6207 3 9.6909 3 7.31034Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 12L9.41379 9.41379M2 6.31034C2 3.92981 3.92981 2 6.31034 2C8.6909 2 10.6207 3.92981 10.6207 6.31034C10.6207 8.6909 8.6909 10.6207 6.31034 10.6207C3.92981 10.6207 2 8.6909 2 6.31034Z" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 382 B

After

Width:  |  Height:  |  Size: 383 B

View File

@@ -172,7 +172,6 @@
"context": "AssistantPanel",
"bindings": {
"ctrl-k c": "assistant::CopyCode",
"ctrl-shift-e": "project_panel::ToggleFocus",
"ctrl-g": "search::SelectNextMatch",
"ctrl-shift-g": "search::SelectPrevMatch",
"ctrl-shift-m": "assistant::ToggleModelSelector",
@@ -265,7 +264,7 @@
"ctrl-k t": ["pane::CloseItemsToTheRight", { "close_pinned": false }],
"ctrl-k u": ["pane::CloseCleanItems", { "close_pinned": false }],
"ctrl-k w": ["pane::CloseAllItems", { "close_pinned": false }],
"ctrl-shift-f": "pane::DeploySearch",
"ctrl-shift-f": "project_search::ToggleFocus",
"ctrl-alt-g": "search::SelectNextMatch",
"ctrl-alt-shift-g": "search::SelectPrevMatch",
"ctrl-alt-shift-h": "search::ToggleReplace",
@@ -412,7 +411,7 @@
"ctrl-shift-p": "command_palette::Toggle",
"f1": "command_palette::Toggle",
"ctrl-shift-m": "diagnostics::Deploy",
"ctrl-shift-e": "project_panel::ToggleFocus",
"ctrl-shift-e": "pane::RevealInProjectPanel",
"ctrl-shift-b": "outline_panel::ToggleFocus",
"ctrl-?": "assistant::ToggleFocus",
"ctrl-alt-s": "workspace::SaveAll",
@@ -533,7 +532,6 @@
"alt-enter": "editor::OpenExcerpts",
"shift-enter": "editor::ExpandExcerpts",
"ctrl-k enter": "editor::OpenExcerptsSplit",
"ctrl-shift-e": "pane::RevealInProjectPanel",
"ctrl-f8": "editor::GoToHunk",
"ctrl-shift-f8": "editor::GoToPrevHunk",
"ctrl-enter": "assistant::InlineAssist"
@@ -614,6 +612,7 @@
"ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }],
"alt-ctrl-r": "project_panel::RevealInFileManager",
"ctrl-shift-enter": "project_panel::OpenWithSystem",
"ctrl-shift-e": "project_panel::ToggleFocus",
"ctrl-shift-f": "project_panel::NewSearchInDirectory",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrev",

View File

@@ -196,7 +196,6 @@
"use_key_equivalents": true,
"bindings": {
"cmd-k c": "assistant::CopyCode",
"cmd-shift-e": "project_panel::ToggleFocus",
"cmd-g": "search::SelectNextMatch",
"cmd-shift-g": "search::SelectPrevMatch",
"cmd-shift-m": "assistant::ToggleModelSelector",
@@ -227,8 +226,7 @@
"cmd-n": "assistant2::NewThread",
"cmd-shift-h": "assistant2::OpenHistory",
"cmd-shift-m": "assistant2::ToggleModelSelector",
"cmd-shift-a": "assistant2::ToggleContextPicker",
"cmd-alt-e": "assistant2::RemoveAllContext"
"cmd-shift-a": "assistant2::ToggleContextPicker"
}
},
{
@@ -436,7 +434,7 @@
"ctrl--": "pane::GoBack",
"ctrl-shift--": "pane::GoForward",
"cmd-shift-t": "pane::ReopenClosedItem",
"cmd-shift-f": "pane::DeploySearch"
"cmd-shift-f": "project_search::ToggleFocus"
}
},
{
@@ -477,7 +475,7 @@
"ctrl-shift-tab": ["tab_switcher::Toggle", { "select_last": true }],
"cmd-shift-p": "command_palette::Toggle",
"cmd-shift-m": "diagnostics::Deploy",
"cmd-shift-e": "project_panel::ToggleFocus",
"cmd-shift-e": "pane::RevealInProjectPanel",
"cmd-shift-b": "outline_panel::ToggleFocus",
"cmd-?": "assistant::ToggleFocus",
"cmd-alt-s": "workspace::SaveAll",
@@ -597,7 +595,6 @@
"alt-enter": "editor::OpenExcerpts",
"shift-enter": "editor::ExpandExcerpts",
"cmd-k enter": "editor::OpenExcerptsSplit",
"cmd-shift-e": "pane::RevealInProjectPanel",
"cmd-f8": "editor::GoToHunk",
"cmd-shift-f8": "editor::GoToPrevHunk",
"ctrl-enter": "assistant::InlineAssist"
@@ -616,7 +613,6 @@
"use_key_equivalents": true,
"bindings": {
"cmd-shift-a": "assistant2::ToggleContextPicker",
"cmd-alt-e": "assistant2::RemoveAllContext",
"ctrl-[": "assistant::CyclePreviousInlineAssist",
"ctrl-]": "assistant::CycleNextInlineAssist"
}
@@ -667,6 +663,7 @@
"cmd-delete": ["project_panel::Delete", { "skip_prompt": false }],
"alt-cmd-r": "project_panel::RevealInFileManager",
"ctrl-shift-enter": "project_panel::OpenWithSystem",
"cmd-shift-e": "project_panel::ToggleFocus",
"cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }],
"cmd-shift-f": "project_panel::NewSearchInDirectory",
"shift-down": "menu::SelectNext",

View File

@@ -57,9 +57,7 @@
{
"context": "Workspace && !Terminal",
"bindings": {
"ctrl-x ctrl-c": "zed::Quit", // save-buffers-kill-terminal
"ctrl-x 5 0": "workspace::CloseWindow", // delete-frame
"ctrl-x 5 2": "workspace::NewWindow", // make-frame-command
"ctrl-x ctrl-c": "workspace::CloseWindow", // kill-emacs
"ctrl-x o": "workspace::ActivateNextPane", // other-window
"ctrl-x k": "pane::CloseActiveItem", // kill-buffer
"ctrl-x 0": "pane::CloseActiveItem", // delete-window

View File

@@ -55,11 +55,9 @@
}
},
{
"context": "Workspace",
"context": "Workspace && !Terminal",
"bindings": {
"ctrl-x ctrl-c": "zed::Quit", // save-buffers-kill-terminal
"ctrl-x 5 0": "workspace::CloseWindow", // delete-frame
"ctrl-x 5 2": "workspace::NewWindow", // make-frame-command
"ctrl-x ctrl-c": "workspace::CloseWindow", // kill-emacs
"ctrl-x o": "workspace::ActivateNextPane", // other-window
"ctrl-x k": "pane::CloseActiveItem", // kill-buffer
"ctrl-x 0": "pane::CloseActiveItem", // delete-window
@@ -72,18 +70,6 @@
"ctrl-x s": "workspace::SaveAll" // save-some-buffers
}
},
{
// Workaround to enable using emacs in the Zed terminal.
// Unbind so Zed ignores these keys and lets emacs handle them.
"context": "Terminal",
"bindings": {
"ctrl-x ctrl-c": null, // save-buffers-kill-terminal
"ctrl-x ctrl-f": null, // find-file
"ctrl-x ctrl-s": null, // save-buffer
"ctrl-x ctrl-w": null, // write-file
"ctrl-x s": null // save-some-buffers
}
},
{
"context": "BufferSearchBar > Editor",
"bindings": {

View File

@@ -24,8 +24,8 @@
"ctrl-g": ["editor::SelectNext", { "replace_newest": false }],
"ctrl-cmd-g": ["editor::SelectPrevious", { "replace_newest": false }],
"cmd-/": ["editor::ToggleComments", { "advance_downwards": true }],
"alt-up": "editor::SelectLargerSyntaxNode",
"alt-down": "editor::SelectSmallerSyntaxNode",
"cmd-up": "editor::SelectLargerSyntaxNode",
"cmd-down": "editor::SelectSmallerSyntaxNode",
"shift-alt-up": "editor::MoveLineUp",
"shift-alt-down": "editor::MoveLineDown",
"cmd-alt-l": "editor::Format",

View File

@@ -110,7 +110,7 @@
"g y": "editor::GoToTypeDefinition",
"g shift-i": "editor::GoToImplementation",
"g x": "editor::OpenUrl",
"g f": "editor::OpenSelectedFilename",
"g f": "editor::OpenFile",
"g n": "vim::SelectNextMatch",
"g shift-n": "vim::SelectPreviousMatch",
"g l": "vim::SelectNext",
@@ -197,7 +197,6 @@
"d": ["vim::PushOperator", "Delete"],
"shift-d": "vim::DeleteToEndOfLine",
"shift-j": "vim::JoinLines",
"g shift-j": "vim::JoinLinesNoWhitespace",
"y": ["vim::PushOperator", "Yank"],
"shift-y": "vim::YankLine",
"i": "vim::InsertBefore",
@@ -260,7 +259,7 @@
"shift-d": "vim::VisualDeleteLine",
"shift-x": "vim::VisualDeleteLine",
"y": "vim::VisualYank",
"shift-y": "vim::VisualYankLine",
"shift-y": "vim::VisualYank",
"p": "vim::Paste",
"shift-p": ["vim::Paste", { "preserveClipboard": true }],
"s": "vim::Substitute",
@@ -279,7 +278,6 @@
"g shift-i": "vim::VisualInsertFirstNonWhiteSpace",
"g shift-a": "vim::VisualInsertEndOfLine",
"shift-j": "vim::JoinLines",
"g shift-j": "vim::JoinLinesNoWhitespace",
"r": ["vim::PushOperator", "Replace"],
"ctrl-c": ["vim::SwitchMode", "Normal"],
"escape": ["vim::SwitchMode", "Normal"],
@@ -397,7 +395,6 @@
"'": "vim::Quotes",
"`": "vim::BackQuotes",
"\"": "vim::DoubleQuotes",
"q": "vim::AnyQuotes",
"|": "vim::VerticalBars",
"(": "vim::Parentheses",
")": "vim::Parentheses",

View File

@@ -13,15 +13,15 @@ You must describe the change using the following XML structure:
- <description> (optional) - An arbitrarily-long comment that describes the purpose
of this edit.
- <old_text> (optional) - An excerpt from the file's current contents that uniquely
identifies a range within the file where the edit should occur. Required for all operations
except `create`.
identifies a range within the file where the edit should occur. If this tag is not
specified, then the entire file will be used as the range.
- <new_text> (required) - The new text to insert into the file.
- <operation> (required) - The type of change that should occur at the given range
of the file. Must be one of the following values:
- `update`: Replaces the entire range with the new text.
- `insert_before`: Inserts the new text before the range.
- `insert_after`: Inserts new text after the range.
- `create`: Creates or overwrites a file with the given path and the new text.
- `create`: Creates a new file with the given path and the new text.
- `delete`: Deletes the specified range from the file.
<guidelines>

View File

@@ -372,8 +372,6 @@
"default_width": 240,
// Where to dock the project panel. Can be 'left' or 'right'.
"dock": "left",
// Spacing between worktree entries in the project panel. Can be 'comfortable' or 'standard'.
"entry_spacing": "comfortable",
// Whether to show file icons in the project panel.
"file_icons": true,
// Whether to show folder icons or chevrons for directories in the project panel.

View File

@@ -103,7 +103,6 @@ pretty_assertions.workspace = true
project = { workspace = true, features = ["test-support"] }
rand.workspace = true
serde_json_lenient.workspace = true
terminal_view = { workspace = true, features = ["test-support"] }
text = { workspace = true, features = ["test-support"] }
tree-sitter-md.workspace = true
unindent.workspace = true

View File

@@ -595,7 +595,7 @@ impl AssistantPanel {
true
}
pane::Event::ActivateItem { local, .. } => {
pane::Event::ActivateItem { local } => {
if *local {
self.workspace
.update(cx, |workspace, cx| {
@@ -3654,7 +3654,7 @@ impl ContextEditor {
let (style, tooltip) = match token_state(&self.context, cx) {
Some(TokenState::NoTokensLeft { .. }) => (
ButtonStyle::Tinted(TintColor::Error),
ButtonStyle::Tinted(TintColor::Negative),
Some(Tooltip::text("Token limit reached", cx)),
),
Some(TokenState::HasMoreTokens {
@@ -3711,7 +3711,7 @@ impl ContextEditor {
let (style, tooltip) = match token_state(&self.context, cx) {
Some(TokenState::NoTokensLeft { .. }) => (
ButtonStyle::Tinted(TintColor::Error),
ButtonStyle::Tinted(TintColor::Negative),
Some(Tooltip::text("Token limit reached", cx)),
),
Some(TokenState::HasMoreTokens {
@@ -4272,10 +4272,6 @@ impl Item for ContextEditor {
None
}
}
fn include_in_nav_history() -> bool {
false
}
}
impl SearchableItem for ContextEditor {

View File

@@ -228,19 +228,20 @@ impl InlineAssistant {
let newest_selection = newest_selection.unwrap();
let mut codegen_ranges = Vec::new();
for (excerpt_id, buffer, buffer_range) in
snapshot.excerpts_in_ranges(selections.iter().map(|selection| {
for (excerpt, buffer_range) in
snapshot.disjoint_ranges_to_buffer_ranges(selections.iter().map(|selection| {
snapshot.anchor_before(selection.start)..snapshot.anchor_after(selection.end)
}))
{
let buffer = excerpt.buffer();
let start = Anchor {
buffer_id: Some(buffer.remote_id()),
excerpt_id,
excerpt_id: excerpt.id(),
text_anchor: buffer.anchor_before(buffer_range.start),
};
let end = Anchor {
buffer_id: Some(buffer.remote_id()),
excerpt_id,
excerpt_id: excerpt.id(),
text_anchor: buffer.anchor_after(buffer_range.end),
};
codegen_ranges.push(start..end);
@@ -798,9 +799,10 @@ impl InlineAssistant {
let language_name = assist.editor.upgrade().and_then(|editor| {
let multibuffer = editor.read(cx).buffer().read(cx);
let multibuffer_snapshot = multibuffer.snapshot(cx);
let ranges = multibuffer_snapshot.range_to_buffer_ranges(assist.range.clone());
let mut ranges =
multibuffer_snapshot.range_to_buffer_ranges(assist.range.clone());
ranges
.first()
.next()
.and_then(|(excerpt, _)| excerpt.buffer().language())
.map(|language| language.name())
});
@@ -2625,9 +2627,10 @@ impl CodegenAlternative {
) -> Self {
let snapshot = multi_buffer.read(cx).snapshot(cx);
// TODO: Could be made more efficient by using a reverse iterator.
let (old_excerpt, _) = snapshot
.range_to_buffer_ranges(range.clone())
.pop()
.last()
.unwrap();
let old_buffer = cx.new_model(|cx| {
let text = old_excerpt.buffer().as_rope().clone();
@@ -2872,9 +2875,9 @@ impl CodegenAlternative {
let language_name = {
let multibuffer = self.buffer.read(cx);
let snapshot = multibuffer.snapshot(cx);
let ranges = snapshot.range_to_buffer_ranges(self.range.clone());
let mut ranges = snapshot.range_to_buffer_ranges(self.range.clone());
ranges
.first()
.next()
.and_then(|(excerpt, _)| excerpt.buffer().language())
.map(|language| language.name())
};

View File

@@ -19,7 +19,6 @@ assets.workspace = true
assistant_tool.workspace = true
async-watch.workspace = true
client.workspace = true
clock.workspace = true
chrono.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
@@ -34,7 +33,6 @@ gpui.workspace = true
handlebars.workspace = true
html_to_markdown.workspace = true
http_client.workspace = true
itertools.workspace = true
language.workspace = true
language_model.workspace = true
language_model_selector.workspace = true

View File

@@ -137,7 +137,7 @@ impl ActiveThread {
inline_code: TextStyleRefinement {
font_family: Some(theme_settings.buffer_font.family.clone()),
font_size: Some(buffer_font_size.into()),
background_color: Some(colors.editor_foreground.opacity(0.1)),
background_color: Some(colors.editor_foreground.opacity(0.01)),
..Default::default()
},
link: TextStyleRefinement {
@@ -284,9 +284,9 @@ impl ActiveThread {
if !context.is_empty() {
parent.child(
h_flex().flex_wrap().gap_1().px_1p5().pb_1p5().children(
context.into_iter().map(|context| {
ContextPill::new_added(context, false, None)
}),
context
.iter()
.map(|context| ContextPill::new(context.clone())),
),
)
} else {

View File

@@ -41,7 +41,6 @@ actions!(
NewThread,
ToggleContextPicker,
ToggleModelSelector,
RemoveAllContext,
OpenHistory,
Chat,
CycleNextInlineAssist,

View File

@@ -300,30 +300,20 @@ impl AssistantPanel {
fn render_toolbar(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let focus_handle = self.focus_handle(cx);
let title = if self.thread.read(cx).is_empty() {
SharedString::from("New Thread")
} else {
self.thread
.read(cx)
.summary(cx)
.unwrap_or_else(|| SharedString::from("Loading Summary…"))
};
h_flex()
.id("assistant-toolbar")
.px(DynamicSpacing::Base08.rems(cx))
.h(Tab::container_height(cx))
.flex_none()
.justify_between()
.gap(DynamicSpacing::Base08.rems(cx))
.h(Tab::container_height(cx))
.px(DynamicSpacing::Base08.rems(cx))
.bg(cx.theme().colors().tab_bar_background)
.border_b_1()
.border_color(cx.theme().colors().border)
.child(h_flex().child(Label::new(title)))
.child(h_flex().children(self.thread.read(cx).summary(cx).map(Label::new)))
.child(
h_flex()
.h_full()
.pl_1p5()
.pl_1()
.border_l_1()
.border_color(cx.theme().colors().border)
.gap(DynamicSpacing::Base02.rems(cx))

View File

@@ -257,9 +257,10 @@ impl CodegenAlternative {
) -> Self {
let snapshot = buffer.read(cx).snapshot(cx);
// TODO: Could be more efficient by using a reverse iterator.
let (old_excerpt, _) = snapshot
.range_to_buffer_ranges(range.clone())
.pop()
.last()
.unwrap();
let old_buffer = cx.new_model(|cx| {
let text = old_excerpt.buffer().as_rope().clone();
@@ -421,7 +422,8 @@ impl CodegenAlternative {
};
if let Some(context_store) = &self.context_store {
attach_context_to_message(&mut request_message, context_store.read(cx).snapshot(cx));
let context = context_store.update(cx, |this, _cx| this.context().clone());
attach_context_to_message(&mut request_message, context);
}
request_message.content.push(prompt.into());
@@ -474,9 +476,9 @@ impl CodegenAlternative {
let language_name = {
let multibuffer = self.buffer.read(cx);
let snapshot = multibuffer.snapshot(cx);
let ranges = snapshot.range_to_buffer_ranges(self.range.clone());
let mut ranges = snapshot.range_to_buffer_ranges(self.range.clone());
ranges
.first()
.next()
.and_then(|(excerpt, _)| excerpt.buffer().language())
.map(|language| language.name())
};
@@ -1052,7 +1054,7 @@ mod tests {
stream::{self},
Stream,
};
use gpui::TestAppContext;
use gpui::{Context, TestAppContext};
use indoc::indoc;
use language::{
language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, LanguageMatcher,

View File

@@ -1,16 +1,10 @@
use std::path::Path;
use std::rc::Rc;
use std::sync::Arc;
use collections::BTreeMap;
use gpui::{AppContext, Model, SharedString};
use language::Buffer;
use gpui::SharedString;
use language_model::{LanguageModelRequestMessage, MessageContent};
use project::ProjectEntryId;
use serde::{Deserialize, Serialize};
use text::BufferId;
use util::post_inc;
use crate::thread::Thread;
use crate::thread::ThreadId;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
pub struct ContextId(pub(crate) usize);
@@ -23,174 +17,33 @@ impl ContextId {
/// Some context attached to a message in a thread.
#[derive(Debug, Clone)]
pub struct ContextSnapshot {
pub struct Context {
pub id: ContextId,
pub name: SharedString,
pub parent: Option<SharedString>,
pub tooltip: Option<SharedString>,
pub kind: ContextKind,
/// Text to send to the model. This is not refreshed by `snapshot`.
pub text: SharedString,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ContextKind {
File,
File(ProjectEntryId),
Directory,
FetchedUrl,
Thread,
}
#[derive(Debug)]
pub enum Context {
File(FileContext),
Directory(DirectoryContext),
FetchedUrl(FetchedUrlContext),
Thread(ThreadContext),
}
impl Context {
pub fn id(&self) -> ContextId {
match self {
Self::File(file) => file.id,
Self::Directory(directory) => directory.snapshot.id,
Self::FetchedUrl(url) => url.id,
Self::Thread(thread) => thread.id,
}
}
}
// TODO: Model<Buffer> holds onto the buffer even if the file is deleted and closed. Should remove
// the context from the message editor in this case.
#[derive(Debug)]
pub struct FileContext {
pub id: ContextId,
pub buffer: Model<Buffer>,
#[allow(unused)]
pub version: clock::Global,
pub text: SharedString,
}
#[derive(Debug)]
pub struct DirectoryContext {
#[allow(unused)]
pub path: Rc<Path>,
// TODO: The choice to make this a BTreeMap was a result of use in a version of
// ContextStore::will_include_buffer before I realized that the path logic should be used there
// too.
#[allow(unused)]
pub buffers: BTreeMap<BufferId, (Model<Buffer>, clock::Global)>,
pub snapshot: ContextSnapshot,
}
#[derive(Debug)]
pub struct FetchedUrlContext {
pub id: ContextId,
pub url: SharedString,
pub text: SharedString,
}
// TODO: Model<Thread> holds onto the thread even if the thread is deleted. Can either handle this
// explicitly or have a WeakModel<Thread> and remove during snapshot.
#[derive(Debug)]
pub struct ThreadContext {
pub id: ContextId,
pub thread: Model<Thread>,
pub text: SharedString,
}
impl Context {
pub fn snapshot(&self, cx: &AppContext) -> Option<ContextSnapshot> {
match &self {
Self::File(file_context) => file_context.snapshot(cx),
Self::Directory(directory_context) => Some(directory_context.snapshot()),
Self::FetchedUrl(fetched_url_context) => Some(fetched_url_context.snapshot()),
Self::Thread(thread_context) => Some(thread_context.snapshot(cx)),
}
}
}
impl FileContext {
pub fn path(&self, cx: &AppContext) -> Option<Arc<Path>> {
let buffer = self.buffer.read(cx);
if let Some(file) = buffer.file() {
Some(file.path().clone())
} else {
log::error!("Buffer that had a path unexpectedly no longer has a path.");
None
}
}
pub fn snapshot(&self, cx: &AppContext) -> Option<ContextSnapshot> {
let path = self.path(cx)?;
let full_path: SharedString = path.to_string_lossy().into_owned().into();
let name = match path.file_name() {
Some(name) => name.to_string_lossy().into_owned().into(),
None => full_path.clone(),
};
let parent = path
.parent()
.and_then(|p| p.file_name())
.map(|p| p.to_string_lossy().into_owned().into());
Some(ContextSnapshot {
id: self.id,
name,
parent,
tooltip: Some(full_path),
kind: ContextKind::File,
text: self.text.clone(),
})
}
}
impl DirectoryContext {
pub fn snapshot(&self) -> ContextSnapshot {
self.snapshot.clone()
}
}
impl FetchedUrlContext {
pub fn snapshot(&self) -> ContextSnapshot {
ContextSnapshot {
id: self.id,
name: self.url.clone(),
parent: None,
tooltip: None,
kind: ContextKind::FetchedUrl,
text: self.text.clone(),
}
}
}
impl ThreadContext {
pub fn snapshot(&self, cx: &AppContext) -> ContextSnapshot {
let thread = self.thread.read(cx);
ContextSnapshot {
id: self.id,
name: thread.summary().unwrap_or("New thread".into()),
parent: None,
tooltip: None,
kind: ContextKind::Thread,
text: self.text.clone(),
}
}
Thread(ThreadId),
}
pub fn attach_context_to_message(
message: &mut LanguageModelRequestMessage,
contexts: impl Iterator<Item = ContextSnapshot>,
context: impl IntoIterator<Item = Context>,
) {
let mut file_context = String::new();
let mut directory_context = String::new();
let mut fetch_context = String::new();
let mut thread_context = String::new();
for context in contexts {
for context in context.into_iter() {
match context.kind {
ContextKind::File => {
ContextKind::File(_) => {
file_context.push_str(&context.text);
file_context.push('\n');
}
@@ -204,7 +57,7 @@ pub fn attach_context_to_message(
fetch_context.push_str(&context.text);
fetch_context.push('\n');
}
ContextKind::Thread { .. } => {
ContextKind::Thread(_) => {
thread_context.push_str(&context.name);
thread_context.push('\n');
thread_context.push_str(&context.text);

View File

@@ -14,7 +14,6 @@ use ui::{prelude::*, ListItem, ListItemSpacing};
use util::ResultExt;
use workspace::Workspace;
use crate::context::ContextKind;
use crate::context_picker::directory_context_picker::DirectoryContextPicker;
use crate::context_picker::fetch_context_picker::FetchContextPicker;
use crate::context_picker::file_context_picker::FileContextPicker;
@@ -53,24 +52,24 @@ impl ContextPicker {
let mut entries = Vec::new();
entries.push(ContextPickerEntry {
name: "File".into(),
kind: ContextKind::File,
kind: ContextPickerEntryKind::File,
icon: IconName::File,
});
entries.push(ContextPickerEntry {
name: "Folder".into(),
kind: ContextKind::Directory,
kind: ContextPickerEntryKind::Directory,
icon: IconName::Folder,
});
entries.push(ContextPickerEntry {
name: "Fetch".into(),
kind: ContextKind::FetchedUrl,
kind: ContextPickerEntryKind::FetchedUrl,
icon: IconName::Globe,
});
if thread_store.is_some() {
entries.push(ContextPickerEntry {
name: "Thread".into(),
kind: ContextKind::Thread,
kind: ContextPickerEntryKind::Thread,
icon: IconName::MessageCircle,
});
}
@@ -134,10 +133,18 @@ impl Render for ContextPicker {
#[derive(Clone)]
struct ContextPickerEntry {
name: SharedString,
kind: ContextKind,
kind: ContextPickerEntryKind,
icon: IconName,
}
#[derive(Debug, Clone)]
enum ContextPickerEntryKind {
File,
Directory,
FetchedUrl,
Thread,
}
pub(crate) struct ContextPickerDelegate {
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
@@ -177,7 +184,7 @@ impl PickerDelegate for ContextPickerDelegate {
self.context_picker
.update(cx, |this, cx| {
match entry.kind {
ContextKind::File => {
ContextPickerEntryKind::File => {
this.mode = ContextPickerMode::File(cx.new_view(|cx| {
FileContextPicker::new(
self.context_picker.clone(),
@@ -188,7 +195,7 @@ impl PickerDelegate for ContextPickerDelegate {
)
}));
}
ContextKind::Directory => {
ContextPickerEntryKind::Directory => {
this.mode = ContextPickerMode::Directory(cx.new_view(|cx| {
DirectoryContextPicker::new(
self.context_picker.clone(),
@@ -199,7 +206,7 @@ impl PickerDelegate for ContextPickerDelegate {
)
}));
}
ContextKind::FetchedUrl => {
ContextPickerEntryKind::FetchedUrl => {
this.mode = ContextPickerMode::Fetch(cx.new_view(|cx| {
FetchContextPicker::new(
self.context_picker.clone(),
@@ -210,7 +217,7 @@ impl PickerDelegate for ContextPickerDelegate {
)
}));
}
ContextKind::Thread => {
ContextPickerEntryKind::Thread => {
if let Some(thread_store) = self.thread_store.as_ref() {
this.mode = ContextPickerMode::Thread(cx.new_view(|cx| {
ThreadContextPicker::new(

View File

@@ -2,14 +2,17 @@ use std::path::Path;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use anyhow::anyhow;
use fuzzy::PathMatch;
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakModel, WeakView};
use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, ProjectPath, WorktreeId};
use project::{PathMatchCandidateSet, ProjectPath, Worktree, WorktreeId};
use ui::{prelude::*, ListItem};
use util::ResultExt as _;
use workspace::Workspace;
use crate::context::ContextKind;
use crate::context_picker::file_context_picker::codeblock_fence_for_path;
use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_store::ContextStore;
@@ -178,45 +181,90 @@ impl PickerDelegate for DirectoryContextPickerDelegate {
return;
};
let project_path = ProjectPath {
worktree_id: WorktreeId::from_usize(mat.worktree_id),
path: mat.path.clone(),
};
let Some(task) = self
.context_store
.update(cx, |context_store, cx| {
context_store.add_directory(project_path, cx)
})
.ok()
let workspace = self.workspace.clone();
let Some(project) = workspace
.upgrade()
.map(|workspace| workspace.read(cx).project().clone())
else {
return;
};
let workspace = self.workspace.clone();
let path = mat.path.clone();
let worktree_id = WorktreeId::from_usize(mat.worktree_id);
let confirm_behavior = self.confirm_behavior;
cx.spawn(|this, mut cx| async move {
match task.await {
Ok(()) => {
this.update(&mut cx, |this, cx| match confirm_behavior {
ConfirmBehavior::KeepOpen => {}
ConfirmBehavior::Close => this.delegate.dismissed(cx),
})?;
}
Err(err) => {
let Some(workspace) = workspace.upgrade() else {
return anyhow::Ok(());
};
let worktree = project.update(&mut cx, |project, cx| {
project
.worktree_for_id(worktree_id, cx)
.ok_or_else(|| anyhow!("no worktree found for {worktree_id:?}"))
})??;
workspace.update(&mut cx, |workspace, cx| {
workspace.show_error(&err, cx);
})?;
let files = worktree.update(&mut cx, |worktree, _cx| {
collect_files_in_path(worktree, &path)
})?;
let open_buffer_tasks = project.update(&mut cx, |project, cx| {
files
.into_iter()
.map(|file_path| {
project.open_buffer(
ProjectPath {
worktree_id,
path: file_path.clone(),
},
cx,
)
})
.collect::<Vec<_>>()
})?;
let open_all_buffers_tasks = cx.background_executor().spawn(async move {
let mut buffers = Vec::with_capacity(open_buffer_tasks.len());
for open_buffer_task in open_buffer_tasks {
let buffer = open_buffer_task.await?;
buffers.push(buffer);
}
}
anyhow::Ok(buffers)
});
let buffers = open_all_buffers_tasks.await?;
this.update(&mut cx, |this, cx| {
let mut text = String::new();
for buffer in buffers {
text.push_str(&codeblock_fence_for_path(Some(&path), None));
text.push_str(&buffer.read(cx).text());
if !text.ends_with('\n') {
text.push('\n');
}
text.push_str("```\n");
}
this.delegate
.context_store
.update(cx, |context_store, _cx| {
context_store.insert_context(
ContextKind::Directory,
path.to_string_lossy().to_string(),
text,
);
})?;
match confirm_behavior {
ConfirmBehavior::KeepOpen => {}
ConfirmBehavior::Close => this.delegate.dismissed(cx),
}
anyhow::Ok(())
})??;
anyhow::Ok(())
})
.detach_and_log_err(cx);
.detach_and_log_err(cx)
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
@@ -232,35 +280,30 @@ impl PickerDelegate for DirectoryContextPickerDelegate {
&self,
ix: usize,
selected: bool,
cx: &mut ViewContext<Picker<Self>>,
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let path_match = &self.matches[ix];
let directory_name = path_match.path.to_string_lossy().to_string();
let added = self.context_store.upgrade().map_or(false, |context_store| {
context_store
.read(cx)
.includes_directory(&path_match.path)
.is_some()
});
Some(
ListItem::new(ix)
.inset(true)
.toggle_state(selected)
.child(h_flex().gap_2().child(Label::new(directory_name)))
.when(added, |el| {
el.end_slot(
h_flex()
.gap_1()
.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success),
)
.child(Label::new("Added").size(LabelSize::Small)),
)
}),
.child(h_flex().gap_2().child(Label::new(directory_name))),
)
}
}
fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec<Arc<Path>> {
let mut files = Vec::new();
for entry in worktree.child_entries(path) {
if entry.is_dir() {
files.extend(collect_files_in_path(worktree, &entry.path));
} else if entry.is_file() {
files.push(entry.path.clone());
}
}
files
}

View File

@@ -11,6 +11,7 @@ use picker::{Picker, PickerDelegate};
use ui::{prelude::*, ListItem, ViewContext};
use workspace::Workspace;
use crate::context::ContextKind;
use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_store::ContextStore;
@@ -82,12 +83,10 @@ impl FetchContextPickerDelegate {
}
async fn build_message(http_client: Arc<HttpClientWithUrl>, url: &str) -> Result<String> {
let prefixed_url = if !url.starts_with("https://") && !url.starts_with("http://") {
Some(format!("https://{url}"))
} else {
None
};
let url = prefixed_url.as_deref().unwrap_or(url);
let mut url = url.to_owned();
if !url.starts_with("https://") && !url.starts_with("http://") {
url = format!("https://{url}");
}
let mut response = http_client.get(&url, AsyncBody::default(), true).await?;
@@ -202,9 +201,7 @@ impl PickerDelegate for FetchContextPickerDelegate {
this.delegate
.context_store
.update(cx, |context_store, _cx| {
if context_store.includes_url(&url).is_none() {
context_store.insert_fetched_url(url, text);
}
context_store.insert_context(ContextKind::FetchedUrl, url, text);
})?;
match confirm_behavior {
@@ -233,29 +230,13 @@ impl PickerDelegate for FetchContextPickerDelegate {
&self,
ix: usize,
selected: bool,
cx: &mut ViewContext<Picker<Self>>,
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let added = self.context_store.upgrade().map_or(false, |context_store| {
context_store.read(cx).includes_url(&self.url).is_some()
});
Some(
ListItem::new(ix)
.inset(true)
.toggle_state(selected)
.child(Label::new(self.url.clone()))
.when(added, |child| {
child.disabled(true).end_slot(
h_flex()
.gap_1()
.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success),
)
.child(Label::new("Added").size(LabelSize::Small)),
)
}),
.child(Label::new(self.url.clone())),
)
}
}

View File

@@ -1,3 +1,5 @@
use std::fmt::Write as _;
use std::ops::RangeInclusive;
use std::path::Path;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
@@ -6,12 +8,13 @@ use fuzzy::PathMatch;
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakModel, WeakView};
use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, ProjectPath, WorktreeId};
use ui::{prelude::*, ListItem, Tooltip};
use ui::{prelude::*, ListItem};
use util::ResultExt as _;
use workspace::Workspace;
use crate::context::ContextKind;
use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_store::{ContextStore, FileInclusion};
use crate::context_store::ContextStore;
pub struct FileContextPicker {
picker: View<Picker<FileContextPickerDelegate>>,
@@ -193,41 +196,64 @@ impl PickerDelegate for FileContextPickerDelegate {
return;
};
let project_path = ProjectPath {
worktree_id: WorktreeId::from_usize(mat.worktree_id),
path: mat.path.clone(),
};
let Some(task) = self
.context_store
.update(cx, |context_store, cx| {
context_store.add_file(project_path, cx)
})
.ok()
let workspace = self.workspace.clone();
let Some(project) = workspace
.upgrade()
.map(|workspace| workspace.read(cx).project().clone())
else {
return;
};
let workspace = self.workspace.clone();
let path = mat.path.clone();
let worktree_id = WorktreeId::from_usize(mat.worktree_id);
let confirm_behavior = self.confirm_behavior;
cx.spawn(|this, mut cx| async move {
match task.await {
Ok(()) => {
this.update(&mut cx, |this, cx| match confirm_behavior {
ConfirmBehavior::KeepOpen => {}
ConfirmBehavior::Close => this.delegate.dismissed(cx),
})?;
}
Err(err) => {
let Some(workspace) = workspace.upgrade() else {
return anyhow::Ok(());
let Some((entry_id, open_buffer_task)) = project
.update(&mut cx, |project, cx| {
let project_path = ProjectPath {
worktree_id,
path: path.clone(),
};
workspace.update(&mut cx, |workspace, cx| {
workspace.show_error(&err, cx);
let entry_id = project.entry_for_path(&project_path, cx)?.id;
let task = project.open_buffer(project_path, cx);
Some((entry_id, task))
})
.ok()
.flatten()
else {
return anyhow::Ok(());
};
let buffer = open_buffer_task.await?;
this.update(&mut cx, |this, cx| {
this.delegate
.context_store
.update(cx, |context_store, cx| {
let mut text = String::new();
text.push_str(&codeblock_fence_for_path(Some(&path), None));
text.push_str(&buffer.read(cx).text());
if !text.ends_with('\n') {
text.push('\n');
}
text.push_str("```\n");
context_store.insert_context(
ContextKind::File(entry_id),
path.to_string_lossy().to_string(),
text,
);
})?;
match confirm_behavior {
ConfirmBehavior::KeepOpen => {}
ConfirmBehavior::Close => this.delegate.dismissed(cx),
}
}
anyhow::Ok(())
})??;
anyhow::Ok(())
})
@@ -247,7 +273,7 @@ impl PickerDelegate for FileContextPickerDelegate {
&self,
ix: usize,
selected: bool,
cx: &mut ViewContext<Picker<Self>>,
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let path_match = &self.matches[ix];
@@ -275,53 +301,42 @@ impl PickerDelegate for FileContextPickerDelegate {
(file_name, Some(directory))
};
let added = self.context_store.upgrade().and_then(|context_store| {
context_store
.read(cx)
.will_include_file_path(&path_match.path, cx)
});
Some(
ListItem::new(ix)
.inset(true)
.toggle_state(selected)
.child(
h_flex()
.gap_2()
.child(Label::new(file_name))
.children(directory.map(|directory| {
Label::new(directory)
.size(LabelSize::Small)
.color(Color::Muted)
})),
)
.when_some(added, |el, added| match added {
FileInclusion::Direct(_) => el.end_slot(
h_flex()
.gap_1()
.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success),
)
.child(Label::new("Added").size(LabelSize::Small)),
),
FileInclusion::InDirectory(dir_name) => {
let dir_name = dir_name.to_string_lossy().into_owned();
el.end_slot(
h_flex()
.gap_1()
.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success),
)
.child(Label::new("Included").size(LabelSize::Small)),
)
.tooltip(move |cx| Tooltip::text(format!("in {dir_name}"), cx))
}
}),
ListItem::new(ix).inset(true).toggle_state(selected).child(
h_flex()
.gap_2()
.child(Label::new(file_name))
.children(directory.map(|directory| {
Label::new(directory)
.size(LabelSize::Small)
.color(Color::Muted)
})),
),
)
}
}
pub(crate) fn codeblock_fence_for_path(
path: Option<&Path>,
row_range: Option<RangeInclusive<u32>>,
) -> String {
let mut text = String::new();
write!(text, "```").unwrap();
if let Some(path) = path {
if let Some(extension) = path.extension().and_then(|ext| ext.to_str()) {
write!(text, "{} ", extension).unwrap();
}
write!(text, "{}", path.display()).unwrap();
} else {
write!(text, "untitled").unwrap();
}
if let Some(row_range) = row_range {
write!(text, ":{}-{}", row_range.start() + 1, row_range.end() + 1).unwrap();
}
text.push('\n');
text
}

View File

@@ -5,6 +5,7 @@ use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, Wea
use picker::{Picker, PickerDelegate};
use ui::{prelude::*, ListItem};
use crate::context::ContextKind;
use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_store;
use crate::thread::ThreadId;
@@ -167,7 +168,13 @@ impl PickerDelegate for ThreadContextPickerDelegate {
};
self.context_store
.update(cx, |context_store, cx| context_store.add_thread(thread, cx))
.update(cx, |context_store, cx| {
context_store.insert_context(
ContextKind::Thread(thread.read(cx).id().clone()),
entry.summary.clone(),
thread.read(cx).text(),
);
})
.ok();
match self.confirm_behavior {
@@ -189,31 +196,15 @@ impl PickerDelegate for ThreadContextPickerDelegate {
&self,
ix: usize,
selected: bool,
cx: &mut ViewContext<Picker<Self>>,
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let thread = &self.matches[ix];
let added = self.context_store.upgrade().map_or(false, |context_store| {
context_store.read(cx).includes_thread(&thread.id).is_some()
});
Some(
ListItem::new(ix)
.inset(true)
.toggle_state(selected)
.child(Label::new(thread.summary.clone()))
.when(added, |el| {
el.end_slot(
h_flex()
.gap_1()
.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success),
)
.child(Label::new("Added").size(LabelSize::Small)),
)
}),
.child(Label::new(thread.summary.clone())),
)
}
}

View File

@@ -1,393 +1,65 @@
use std::fmt::Write as _;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use gpui::SharedString;
use project::ProjectEntryId;
use anyhow::{anyhow, bail, Result};
use collections::{BTreeMap, HashMap};
use gpui::{AppContext, Model, ModelContext, SharedString, Task, WeakView};
use language::Buffer;
use project::{ProjectPath, Worktree};
use text::BufferId;
use workspace::Workspace;
use crate::context::{
Context, ContextId, ContextKind, ContextSnapshot, DirectoryContext, FetchedUrlContext,
FileContext, ThreadContext,
use crate::{
context::{Context, ContextId, ContextKind},
thread::ThreadId,
};
use crate::thread::{Thread, ThreadId};
pub struct ContextStore {
workspace: WeakView<Workspace>,
context: Vec<Context>,
// TODO: If an EntityId is used for all context types (like BufferId), can remove ContextId.
next_context_id: ContextId,
files: BTreeMap<BufferId, ContextId>,
directories: HashMap<PathBuf, ContextId>,
threads: HashMap<ThreadId, ContextId>,
fetched_urls: HashMap<String, ContextId>,
}
impl ContextStore {
pub fn new(workspace: WeakView<Workspace>) -> Self {
pub fn new() -> Self {
Self {
workspace,
context: Vec::new(),
next_context_id: ContextId(0),
files: BTreeMap::default(),
directories: HashMap::default(),
threads: HashMap::default(),
fetched_urls: HashMap::default(),
}
}
pub fn snapshot<'a>(
&'a self,
cx: &'a AppContext,
) -> impl Iterator<Item = ContextSnapshot> + 'a {
self.context()
.iter()
.flat_map(|context| context.snapshot(cx))
}
pub fn context(&self) -> &Vec<Context> {
&self.context
}
pub fn drain(&mut self) -> Vec<Context> {
self.context.drain(..).collect()
}
pub fn clear(&mut self) {
self.context.clear();
self.files.clear();
self.directories.clear();
self.threads.clear();
self.fetched_urls.clear();
}
pub fn add_file(
pub fn insert_context(
&mut self,
project_path: ProjectPath,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let workspace = self.workspace.clone();
let Some(project) = workspace
.upgrade()
.map(|workspace| workspace.read(cx).project().clone())
else {
return Task::ready(Err(anyhow!("failed to read project")));
};
cx.spawn(|this, mut cx| async move {
let open_buffer_task = project.update(&mut cx, |project, cx| {
project.open_buffer(project_path.clone(), cx)
})?;
let buffer = open_buffer_task.await?;
let buffer_id = buffer.update(&mut cx, |buffer, _cx| buffer.remote_id())?;
let already_included = this.update(&mut cx, |this, _cx| {
match this.will_include_buffer(buffer_id, &project_path.path) {
Some(FileInclusion::Direct(context_id)) => {
this.remove_context(context_id);
true
}
Some(FileInclusion::InDirectory(_)) => true,
None => false,
}
})?;
if already_included {
return anyhow::Ok(());
}
this.update(&mut cx, |this, cx| {
this.insert_file(buffer, cx);
})?;
anyhow::Ok(())
})
}
pub fn insert_file(&mut self, buffer_model: Model<Buffer>, cx: &AppContext) {
let buffer = buffer_model.read(cx);
let Some(file) = buffer.file() else {
return;
};
let mut text = String::new();
push_fenced_codeblock(file.path(), buffer.text(), &mut text);
let id = self.next_context_id.post_inc();
self.files.insert(buffer.remote_id(), id);
self.context.push(Context::File(FileContext {
id,
buffer: buffer_model,
version: buffer.version.clone(),
text: text.into(),
}));
}
pub fn add_directory(
&mut self,
project_path: ProjectPath,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let workspace = self.workspace.clone();
let Some(project) = workspace
.upgrade()
.map(|workspace| workspace.read(cx).project().clone())
else {
return Task::ready(Err(anyhow!("failed to read project")));
};
let already_included = if let Some(context_id) = self.includes_directory(&project_path.path)
{
self.remove_context(context_id);
true
} else {
false
};
if already_included {
return Task::ready(Ok(()));
}
let worktree_id = project_path.worktree_id;
cx.spawn(|this, mut cx| async move {
let worktree = project.update(&mut cx, |project, cx| {
project
.worktree_for_id(worktree_id, cx)
.ok_or_else(|| anyhow!("no worktree found for {worktree_id:?}"))
})??;
let files = worktree.update(&mut cx, |worktree, _cx| {
collect_files_in_path(worktree, &project_path.path)
})?;
let open_buffer_tasks = project.update(&mut cx, |project, cx| {
files
.into_iter()
.map(|file_path| {
project.open_buffer(
ProjectPath {
worktree_id,
path: file_path.clone(),
},
cx,
)
})
.collect::<Vec<_>>()
})?;
let buffers = futures::future::join_all(open_buffer_tasks).await;
this.update(&mut cx, |this, cx| {
let mut text = String::new();
let mut directory_buffers = BTreeMap::new();
for buffer_model in buffers {
let buffer_model = buffer_model?;
let buffer = buffer_model.read(cx);
let path = buffer.file().map_or(&project_path.path, |file| file.path());
push_fenced_codeblock(&path, buffer.text(), &mut text);
directory_buffers
.insert(buffer.remote_id(), (buffer_model, buffer.version.clone()));
}
if directory_buffers.is_empty() {
bail!(
"could not read any text files from {}",
&project_path.path.display()
);
}
this.insert_directory(&project_path.path, directory_buffers, text);
anyhow::Ok(())
})??;
anyhow::Ok(())
})
}
pub fn insert_directory(
&mut self,
path: &Path,
buffers: BTreeMap<BufferId, (Model<Buffer>, clock::Global)>,
kind: ContextKind,
name: impl Into<SharedString>,
text: impl Into<SharedString>,
) {
let id = self.next_context_id.post_inc();
self.directories.insert(path.to_path_buf(), id);
let full_path: SharedString = path.to_string_lossy().into_owned().into();
let name = match path.file_name() {
Some(name) => name.to_string_lossy().into_owned().into(),
None => full_path.clone(),
};
let parent = path
.parent()
.and_then(|p| p.file_name())
.map(|p| p.to_string_lossy().into_owned().into());
self.context.push(Context::Directory(DirectoryContext {
path: path.into(),
buffers,
snapshot: ContextSnapshot {
id,
name,
parent,
tooltip: Some(full_path),
kind: ContextKind::Directory,
text: text.into(),
},
}));
}
pub fn add_thread(&mut self, thread: Model<Thread>, cx: &mut ModelContext<Self>) {
if let Some(context_id) = self.includes_thread(&thread.read(cx).id()) {
self.remove_context(context_id);
} else {
self.insert_thread(thread, cx);
}
}
pub fn insert_thread(&mut self, thread: Model<Thread>, cx: &AppContext) {
let id = self.next_context_id.post_inc();
let thread_ref = thread.read(cx);
let text = thread_ref.text().into();
self.threads.insert(thread_ref.id().clone(), id);
self.context
.push(Context::Thread(ThreadContext { id, thread, text }));
}
pub fn insert_fetched_url(&mut self, url: String, text: impl Into<SharedString>) {
let id = self.next_context_id.post_inc();
self.fetched_urls.insert(url.clone(), id);
self.context.push(Context::FetchedUrl(FetchedUrlContext {
id,
url: url.into(),
self.context.push(Context {
id: self.next_context_id.post_inc(),
name: name.into(),
kind,
text: text.into(),
}));
});
}
pub fn remove_context(&mut self, id: ContextId) {
let Some(ix) = self.context.iter().position(|context| context.id() == id) else {
return;
};
match self.context.remove(ix) {
Context::File(_) => {
self.files.retain(|_, context_id| *context_id != id);
}
Context::Directory(_) => {
self.directories.retain(|_, context_id| *context_id != id);
}
Context::FetchedUrl(_) => {
self.fetched_urls.retain(|_, context_id| *context_id != id);
}
Context::Thread(_) => {
self.threads.retain(|_, context_id| *context_id != id);
}
}
pub fn remove_context(&mut self, id: &ContextId) {
self.context.retain(|context| context.id != *id);
}
/// Returns whether the buffer is already included directly in the context, or if it will be
/// included in the context via a directory. Directory inclusion is based on paths rather than
/// buffer IDs as the directory will be re-scanned.
pub fn will_include_buffer(&self, buffer_id: BufferId, path: &Path) -> Option<FileInclusion> {
if let Some(context_id) = self.files.get(&buffer_id) {
return Some(FileInclusion::Direct(*context_id));
}
self.will_include_file_path_via_directory(path)
pub fn contains_project_entry(&self, entry_id: ProjectEntryId) -> bool {
self.context.iter().any(|probe| match probe.kind {
ContextKind::File(probe_entry_id) => probe_entry_id == entry_id,
ContextKind::Directory | ContextKind::FetchedUrl | ContextKind::Thread(_) => false,
})
}
/// Returns whether this file path is already included directly in the context, or if it will be
/// included in the context via a directory.
pub fn will_include_file_path(&self, path: &Path, cx: &AppContext) -> Option<FileInclusion> {
if !self.files.is_empty() {
let found_file_context = self.context.iter().find(|context| match &context {
Context::File(file_context) => {
if let Some(file_path) = file_context.path(cx) {
*file_path == *path
} else {
false
}
}
_ => false,
});
if let Some(context) = found_file_context {
return Some(FileInclusion::Direct(context.id()));
}
}
self.will_include_file_path_via_directory(path)
}
fn will_include_file_path_via_directory(&self, path: &Path) -> Option<FileInclusion> {
if self.directories.is_empty() {
return None;
}
let mut buf = path.to_path_buf();
while buf.pop() {
if let Some(_) = self.directories.get(&buf) {
return Some(FileInclusion::InDirectory(buf));
}
}
None
}
pub fn includes_directory(&self, path: &Path) -> Option<ContextId> {
self.directories.get(path).copied()
}
pub fn includes_thread(&self, thread_id: &ThreadId) -> Option<ContextId> {
self.threads.get(thread_id).copied()
}
pub fn includes_url(&self, url: &str) -> Option<ContextId> {
self.fetched_urls.get(url).copied()
pub fn contains_thread(&self, thread_id: &ThreadId) -> bool {
self.context.iter().any(|probe| match probe.kind {
ContextKind::Thread(ref probe_thread_id) => probe_thread_id == thread_id,
ContextKind::File(_) | ContextKind::Directory | ContextKind::FetchedUrl => false,
})
}
}
pub enum FileInclusion {
Direct(ContextId),
InDirectory(PathBuf),
}
pub(crate) fn push_fenced_codeblock(path: &Path, content: String, buffer: &mut String) {
buffer.reserve(content.len() + 64);
write!(buffer, "```").unwrap();
if let Some(extension) = path.extension().and_then(|ext| ext.to_str()) {
write!(buffer, "{} ", extension).unwrap();
}
write!(buffer, "{}", path.display()).unwrap();
buffer.push('\n');
buffer.push_str(&content);
if !buffer.ends_with('\n') {
buffer.push('\n');
}
buffer.push_str("```\n");
}
fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec<Arc<Path>> {
let mut files = Vec::new();
for entry in worktree.child_entries(path) {
if entry.is_dir() {
files.extend(collect_files_in_path(worktree, &entry.path));
} else if entry.is_file() {
files.push(entry.path.clone());
}
}
files
}

View File

@@ -1,23 +1,19 @@
use std::rc::Rc;
use collections::HashSet;
use editor::Editor;
use gpui::{
AppContext, DismissEvent, EventEmitter, FocusHandle, Model, Subscription, View, WeakModel,
WeakView,
};
use itertools::Itertools;
use gpui::{AppContext, FocusHandle, Model, View, WeakModel, WeakView};
use language::Buffer;
use project::ProjectEntryId;
use ui::{prelude::*, KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip};
use workspace::Workspace;
use crate::context::ContextKind;
use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_store::ContextStore;
use crate::thread::Thread;
use crate::thread::{Thread, ThreadId};
use crate::thread_store::ThreadStore;
use crate::ui::ContextPill;
use crate::{AssistantPanel, RemoveAllContext, ToggleContextPicker};
use crate::{AssistantPanel, ToggleContextPicker};
pub struct ContextStrip {
context_store: Model<ContextStore>,
@@ -26,7 +22,6 @@ pub struct ContextStrip {
focus_handle: FocusHandle,
suggest_context_kind: SuggestContextKind,
workspace: WeakView<Workspace>,
_context_picker_subscription: Subscription,
}
impl ContextStrip {
@@ -39,27 +34,21 @@ impl ContextStrip {
suggest_context_kind: SuggestContextKind,
cx: &mut ViewContext<Self>,
) -> Self {
let context_picker = cx.new_view(|cx| {
ContextPicker::new(
workspace.clone(),
thread_store.clone(),
context_store.downgrade(),
ConfirmBehavior::KeepOpen,
cx,
)
});
let context_picker_subscription =
cx.subscribe(&context_picker, Self::handle_context_picker_event);
Self {
context_store: context_store.clone(),
context_picker,
context_picker: cx.new_view(|cx| {
ContextPicker::new(
workspace.clone(),
thread_store.clone(),
context_store.downgrade(),
ConfirmBehavior::KeepOpen,
cx,
)
}),
context_picker_menu_handle,
focus_handle,
suggest_context_kind,
workspace,
_context_picker_subscription: context_picker_subscription,
}
}
@@ -73,30 +62,22 @@ impl ContextStrip {
fn suggested_file(&self, cx: &ViewContext<Self>) -> Option<SuggestedContext> {
let workspace = self.workspace.upgrade()?;
let active_item = workspace.read(cx).active_item(cx)?;
let entry_id = *active_item.project_entry_ids(cx).first()?;
let editor = active_item.to_any().downcast::<Editor>().ok()?.read(cx);
let active_buffer_model = editor.buffer().read(cx).as_singleton()?;
let active_buffer = active_buffer_model.read(cx);
let path = active_buffer.file()?.path();
if self
.context_store
.read(cx)
.will_include_buffer(active_buffer.remote_id(), path)
.is_some()
{
if self.context_store.read(cx).contains_project_entry(entry_id) {
return None;
}
let name = match path.file_name() {
Some(name) => name.to_string_lossy().into_owned().into(),
None => path.to_string_lossy().into_owned().into(),
};
let editor = active_item.to_any().downcast::<Editor>().ok()?.read(cx);
let active_buffer = editor.buffer().read(cx).as_singleton()?;
let file = active_buffer.read(cx).file()?;
let title = file.path().to_string_lossy().into_owned().into();
Some(SuggestedContext::File {
name,
buffer: active_buffer_model.downgrade(),
entry_id,
title,
buffer: active_buffer.downgrade(),
})
}
@@ -114,50 +95,28 @@ impl ContextStrip {
if self
.context_store
.read(cx)
.includes_thread(active_thread.id())
.is_some()
.contains_thread(active_thread.id())
{
return None;
}
Some(SuggestedContext::Thread {
name: active_thread.summary().unwrap_or("New Thread".into()),
id: active_thread.id().clone(),
title: active_thread.summary().unwrap_or("Active Thread".into()),
thread: weak_active_thread,
})
}
fn handle_context_picker_event(
&mut self,
_picker: View<ContextPicker>,
_event: &DismissEvent,
cx: &mut ViewContext<Self>,
) {
cx.emit(ContextStripEvent::PickerDismissed);
}
}
impl Render for ContextStrip {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let context_store = self.context_store.read(cx);
let context = context_store
.context()
.iter()
.flat_map(|context| context.snapshot(cx))
.collect::<Vec<_>>();
let context = context_store.context().clone();
let context_picker = self.context_picker.clone();
let focus_handle = self.focus_handle.clone();
let suggested_context = self.suggested_context(cx);
let dupe_names = context
.iter()
.map(|context| context.name.clone())
.sorted()
.tuple_windows()
.filter(|(a, b)| a == b)
.map(|(a, _)| a)
.collect::<HashSet<SharedString>>();
h_flex()
.flex_wrap()
.gap_1()
@@ -209,71 +168,60 @@ impl Render for ContextStrip {
}
})
.children(context.iter().map(|context| {
ContextPill::new_added(
context.clone(),
dupe_names.contains(&context.name),
Some({
let id = context.id;
let context_store = self.context_store.clone();
Rc::new(cx.listener(move |_this, _event, cx| {
context_store.update(cx, |this, _cx| {
this.remove_context(id);
});
cx.notify();
}))
}),
)
ContextPill::new(context.clone()).on_remove({
let context = context.clone();
let context_store = self.context_store.clone();
Rc::new(cx.listener(move |_this, _event, cx| {
context_store.update(cx, |this, _cx| {
this.remove_context(&context.id);
});
cx.notify();
}))
})
}))
.when_some(suggested_context, |el, suggested| {
el.child(ContextPill::new_suggested(
suggested.name().clone(),
suggested.kind(),
{
let context_store = self.context_store.clone();
Rc::new(cx.listener(move |_this, _event, cx| {
context_store.update(cx, |context_store, cx| {
suggested.accept(context_store, cx);
});
el.child(
Button::new("add-suggested-context", suggested.title().clone())
.on_click({
let context_store = self.context_store.clone();
cx.notify();
}))
},
))
cx.listener(move |_this, _event, cx| {
context_store.update(cx, |context_store, cx| {
suggested.accept(context_store, cx);
});
cx.notify();
})
})
.icon(IconName::Plus)
.icon_position(IconPosition::Start)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.label_size(LabelSize::Small)
.style(ButtonStyle::Filled)
.tooltip(|cx| {
Tooltip::with_meta("Suggested Context", None, "Click to add it", cx)
}),
)
})
.when(!context.is_empty(), {
move |parent| {
parent.child(
IconButton::new("remove-all-context", IconName::Eraser)
.icon_size(IconSize::Small)
.tooltip({
let focus_handle = focus_handle.clone();
move |cx| {
Tooltip::for_action_in(
"Remove All Context",
&RemoveAllContext,
&focus_handle,
cx,
)
}
})
.on_click(cx.listener({
let focus_handle = focus_handle.clone();
move |_this, _event, cx| {
focus_handle.dispatch_action(&RemoveAllContext, cx);
}
})),
.tooltip(move |cx| Tooltip::text("Remove All Context", cx))
.on_click({
let context_store = self.context_store.clone();
cx.listener(move |_this, _event, cx| {
context_store.update(cx, |this, _cx| this.clear());
cx.notify();
})
}),
)
}
})
}
}
pub enum ContextStripEvent {
PickerDismissed,
}
impl EventEmitter<ContextStripEvent> for ContextStrip {}
pub enum SuggestContextKind {
File,
Thread,
@@ -282,42 +230,54 @@ pub enum SuggestContextKind {
#[derive(Clone)]
pub enum SuggestedContext {
File {
name: SharedString,
entry_id: ProjectEntryId,
title: SharedString,
buffer: WeakModel<Buffer>,
},
Thread {
name: SharedString,
id: ThreadId,
title: SharedString,
thread: WeakModel<Thread>,
},
}
impl SuggestedContext {
pub fn name(&self) -> &SharedString {
pub fn title(&self) -> &SharedString {
match self {
Self::File { name, .. } => name,
Self::Thread { name, .. } => name,
Self::File { title, .. } => title,
Self::Thread { title, .. } => title,
}
}
pub fn accept(&self, context_store: &mut ContextStore, cx: &mut AppContext) {
match self {
Self::File { buffer, name: _ } => {
if let Some(buffer) = buffer.upgrade() {
context_store.insert_file(buffer, cx);
Self::File {
entry_id,
title,
buffer,
} => {
let Some(buffer) = buffer.upgrade() else {
return;
};
}
Self::Thread { thread, name: _ } => {
if let Some(thread) = thread.upgrade() {
context_store.insert_thread(thread, cx);
};
}
}
}
let text = buffer.read(cx).text();
pub fn kind(&self) -> ContextKind {
match self {
Self::File { .. } => ContextKind::File,
Self::Thread { .. } => ContextKind::Thread,
context_store.insert_context(
ContextKind::File(*entry_id),
title.clone(),
text.clone(),
);
}
Self::Thread { id, title, thread } => {
let Some(thread) = thread.upgrade() else {
return;
};
context_store.insert_context(
ContextKind::Thread(id.clone()),
title.clone(),
thread.read(cx).text(),
);
}
}
}
}

View File

@@ -294,19 +294,20 @@ impl InlineAssistant {
let newest_selection = newest_selection.unwrap();
let mut codegen_ranges = Vec::new();
for (excerpt_id, buffer, buffer_range) in
snapshot.excerpts_in_ranges(selections.iter().map(|selection| {
for (excerpt, buffer_range) in
snapshot.disjoint_ranges_to_buffer_ranges(selections.iter().map(|selection| {
snapshot.anchor_before(selection.start)..snapshot.anchor_after(selection.end)
}))
{
let buffer = excerpt.buffer();
let start = Anchor {
buffer_id: Some(buffer.remote_id()),
excerpt_id,
excerpt_id: excerpt.id(),
text_anchor: buffer.anchor_before(buffer_range.start),
};
let end = Anchor {
buffer_id: Some(buffer.remote_id()),
excerpt_id,
excerpt_id: excerpt.id(),
text_anchor: buffer.anchor_after(buffer_range.end),
};
codegen_ranges.push(start..end);
@@ -335,7 +336,7 @@ impl InlineAssistant {
let mut assist_to_focus = None;
for range in codegen_ranges {
let assist_id = self.next_assist_id.post_inc();
let context_store = cx.new_model(|_cx| ContextStore::new(workspace.clone()));
let context_store = cx.new_model(|_cx| ContextStore::new());
let codegen = cx.new_model(|cx| {
BufferCodegen::new(
editor.read(cx).buffer().clone(),
@@ -445,7 +446,7 @@ impl InlineAssistant {
range.end = range.end.bias_right(&snapshot);
}
let context_store = cx.new_model(|_cx| ContextStore::new(workspace.clone()));
let context_store = cx.new_model(|_cx| ContextStore::new());
let codegen = cx.new_model(|cx| {
BufferCodegen::new(
@@ -872,9 +873,9 @@ impl InlineAssistant {
let language_name = assist.editor.upgrade().and_then(|editor| {
let multibuffer = editor.read(cx).buffer().read(cx);
let snapshot = multibuffer.snapshot(cx);
let ranges = snapshot.range_to_buffer_ranges(assist.range.clone());
let mut ranges = snapshot.range_to_buffer_ranges(assist.range.clone());
ranges
.first()
.next()
.and_then(|(excerpt, _)| excerpt.buffer().language())
.map(|language| language.name())
});

View File

@@ -2,11 +2,11 @@ use crate::assistant_model_selector::AssistantModelSelector;
use crate::buffer_codegen::BufferCodegen;
use crate::context_picker::ContextPicker;
use crate::context_store::ContextStore;
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
use crate::context_strip::{ContextStrip, SuggestContextKind};
use crate::terminal_codegen::TerminalCodegen;
use crate::thread_store::ThreadStore;
use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist};
use crate::{RemoveAllContext, ToggleContextPicker, ToggleModelSelector};
use crate::{ToggleContextPicker, ToggleModelSelector};
use client::ErrorExt;
use collections::VecDeque;
use editor::{
@@ -27,7 +27,6 @@ use settings::Settings;
use std::cmp;
use std::sync::Arc;
use theme::ThemeSettings;
use ui::utils::WithRemSize;
use ui::{
prelude::*, CheckboxWithLabel, IconButtonShape, KeyBinding, Popover, PopoverMenuHandle, Tooltip,
};
@@ -37,7 +36,6 @@ use workspace::Workspace;
pub struct PromptEditor<T> {
pub editor: View<Editor>,
mode: PromptEditorMode,
context_store: Model<ContextStore>,
context_strip: View<ContextStrip>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
model_selector: View<AssistantModelSelector>,
@@ -48,7 +46,6 @@ pub struct PromptEditor<T> {
pending_prompt: String,
_codegen_subscription: Subscription,
editor_subscriptions: Vec<Subscription>,
_context_strip_subscription: Subscription,
show_rate_limit_notice: bool,
_phantom: std::marker::PhantomData<T>,
}
@@ -57,10 +54,9 @@ impl<T: 'static> EventEmitter<PromptEditorEvent> for PromptEditor<T> {}
impl<T: 'static> Render for PromptEditor<T> {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let ui_font_size = ThemeSettings::get_global(cx).ui_font_size;
let mut buttons = Vec::new();
let left_gutter_width = match &self.mode {
let left_gutter_spacing = match &self.mode {
PromptEditorMode::Buffer {
id: _,
codegen,
@@ -110,17 +106,12 @@ impl<T: 'static> Render for PromptEditor<T> {
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::move_up))
.on_action(cx.listener(Self::move_down))
.on_action(cx.listener(Self::remove_all_context))
.capture_action(cx.listener(Self::cycle_prev))
.capture_action(cx.listener(Self::cycle_next))
.child(
WithRemSize::new(ui_font_size)
.flex()
.flex_row()
.flex_shrink_0()
.items_center()
h_flex()
.h_full()
.w(left_gutter_width)
.w(left_gutter_spacing)
.justify_center()
.gap_2()
.child(self.render_close_button(cx))
@@ -180,31 +171,19 @@ impl<T: 'static> Render for PromptEditor<T> {
.w_full()
.justify_between()
.child(div().flex_1().child(self.render_editor(cx)))
.child(
WithRemSize::new(ui_font_size)
.flex()
.flex_row()
.items_center()
.gap_1()
.children(buttons),
),
.child(h_flex().gap_1().children(buttons)),
),
)
.child(
WithRemSize::new(ui_font_size)
.flex()
.flex_row()
.items_center()
.child(h_flex().flex_shrink_0().w(left_gutter_width))
.child(
h_flex()
.w_full()
.pl_1()
.items_start()
.justify_between()
.child(self.context_strip.clone())
.child(self.model_selector.clone()),
),
h_flex().child(div().w(left_gutter_spacing)).child(
h_flex()
.w_full()
.pl_1()
.items_start()
.justify_between()
.child(self.context_strip.clone())
.child(self.model_selector.clone()),
),
)
}
}
@@ -341,11 +320,6 @@ impl<T: 'static> PromptEditor<T> {
self.model_selector_menu_handle.toggle(cx);
}
pub fn remove_all_context(&mut self, _: &RemoveAllContext, cx: &mut ViewContext<Self>) {
self.context_store.update(cx, |store, _cx| store.clear());
cx.notify();
}
fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
match self.codegen_status(cx) {
CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => {
@@ -706,10 +680,9 @@ impl<T: 'static> PromptEditor<T> {
let line_height = font_size.to_pixels(cx.rem_size()) * 1.3;
div()
.key_context("InlineAssistEditor")
.key_context("MessageEditor")
.size_full()
.p_2()
.pl_1()
.bg(cx.theme().colors().editor_background)
.child({
let settings = ThemeSettings::get_global(cx);
@@ -734,16 +707,6 @@ impl<T: 'static> PromptEditor<T> {
})
.into_any_element()
}
fn handle_context_strip_event(
&mut self,
_context_strip: View<ContextStrip>,
ContextStripEvent::PickerDismissed: &ContextStripEvent,
cx: &mut ViewContext<Self>,
) {
let editor_focus_handle = self.editor.focus_handle(cx);
cx.focus(&editor_focus_handle);
}
}
pub enum PromptEditorMode {
@@ -821,25 +784,19 @@ impl PromptEditor<BufferCodegen> {
let context_picker_menu_handle = PopoverMenuHandle::default();
let model_selector_menu_handle = PopoverMenuHandle::default();
let context_strip = cx.new_view(|cx| {
ContextStrip::new(
context_store.clone(),
workspace.clone(),
thread_store.clone(),
prompt_editor.focus_handle(cx),
context_picker_menu_handle.clone(),
SuggestContextKind::Thread,
cx,
)
});
let context_strip_subscription =
cx.subscribe(&context_strip, Self::handle_context_strip_event);
let mut this: PromptEditor<BufferCodegen> = PromptEditor {
editor: prompt_editor.clone(),
context_store,
context_strip,
context_strip: cx.new_view(|cx| {
ContextStrip::new(
context_store,
workspace.clone(),
thread_store.clone(),
prompt_editor.focus_handle(cx),
context_picker_menu_handle.clone(),
SuggestContextKind::Thread,
cx,
)
}),
context_picker_menu_handle,
model_selector: cx.new_view(|cx| {
AssistantModelSelector::new(fs, model_selector_menu_handle.clone(), cx)
@@ -851,7 +808,6 @@ impl PromptEditor<BufferCodegen> {
pending_prompt: String::new(),
_codegen_subscription: codegen_subscription,
editor_subscriptions: Vec::new(),
_context_strip_subscription: context_strip_subscription,
show_rate_limit_notice: false,
mode,
_phantom: Default::default(),
@@ -968,25 +924,19 @@ impl PromptEditor<TerminalCodegen> {
let context_picker_menu_handle = PopoverMenuHandle::default();
let model_selector_menu_handle = PopoverMenuHandle::default();
let context_strip = cx.new_view(|cx| {
ContextStrip::new(
context_store.clone(),
workspace.clone(),
thread_store.clone(),
prompt_editor.focus_handle(cx),
context_picker_menu_handle.clone(),
SuggestContextKind::Thread,
cx,
)
});
let context_strip_subscription =
cx.subscribe(&context_strip, Self::handle_context_strip_event);
let mut this = Self {
editor: prompt_editor.clone(),
context_store,
context_strip,
context_strip: cx.new_view(|cx| {
ContextStrip::new(
context_store,
workspace.clone(),
thread_store.clone(),
prompt_editor.focus_handle(cx),
context_picker_menu_handle.clone(),
SuggestContextKind::Thread,
cx,
)
}),
context_picker_menu_handle,
model_selector: cx.new_view(|cx| {
AssistantModelSelector::new(fs, model_selector_menu_handle.clone(), cx)
@@ -998,7 +948,6 @@ impl PromptEditor<TerminalCodegen> {
pending_prompt: String::new(),
_codegen_subscription: codegen_subscription,
editor_subscriptions: Vec::new(),
_context_strip_subscription: context_strip_subscription,
mode,
show_rate_limit_notice: false,
_phantom: Default::default(),

View File

@@ -10,7 +10,7 @@ use language_model::{LanguageModelRegistry, LanguageModelRequestTool};
use language_model_selector::LanguageModelSelector;
use rope::Point;
use settings::Settings;
use theme::{get_ui_font_size, ThemeSettings};
use theme::ThemeSettings;
use ui::{
prelude::*, ButtonLike, ElevationIndex, KeyBinding, PopoverMenu, PopoverMenuHandle,
SwitchWithLabel,
@@ -20,10 +20,10 @@ use workspace::Workspace;
use crate::assistant_model_selector::AssistantModelSelector;
use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_store::ContextStore;
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
use crate::context_strip::{ContextStrip, SuggestContextKind};
use crate::thread::{RequestKind, Thread};
use crate::thread_store::ThreadStore;
use crate::{Chat, RemoveAllContext, ToggleContextPicker, ToggleModelSelector};
use crate::{Chat, ToggleContextPicker, ToggleModelSelector};
pub struct MessageEditor {
thread: Model<Thread>,
@@ -47,7 +47,7 @@ impl MessageEditor {
thread: Model<Thread>,
cx: &mut ViewContext<Self>,
) -> Self {
let context_store = cx.new_model(|_cx| ContextStore::new(workspace.clone()));
let context_store = cx.new_model(|_cx| ContextStore::new());
let context_picker_menu_handle = PopoverMenuHandle::default();
let inline_context_picker_menu_handle = PopoverMenuHandle::default();
let model_selector_menu_handle = PopoverMenuHandle::default();
@@ -59,7 +59,6 @@ impl MessageEditor {
editor
});
let inline_context_picker = cx.new_view(|cx| {
ContextPicker::new(
workspace.clone(),
@@ -69,33 +68,29 @@ impl MessageEditor {
cx,
)
});
let context_strip = cx.new_view(|cx| {
ContextStrip::new(
context_store.clone(),
workspace.clone(),
Some(thread_store.clone()),
editor.focus_handle(cx),
context_picker_menu_handle.clone(),
SuggestContextKind::File,
cx,
)
});
let subscriptions = vec![
cx.subscribe(&editor, Self::handle_editor_event),
cx.subscribe(
&inline_context_picker,
Self::handle_inline_context_picker_event,
),
cx.subscribe(&context_strip, Self::handle_context_strip_event),
];
Self {
thread,
editor: editor.clone(),
context_store,
context_strip,
context_store: context_store.clone(),
context_strip: cx.new_view(|cx| {
ContextStrip::new(
context_store,
workspace.clone(),
Some(thread_store.clone()),
editor.focus_handle(cx),
context_picker_menu_handle.clone(),
SuggestContextKind::File,
cx,
)
}),
context_picker_menu_handle,
inline_context_picker,
inline_context_picker_menu_handle,
@@ -116,11 +111,6 @@ impl MessageEditor {
self.context_picker_menu_handle.toggle(cx);
}
pub fn remove_all_context(&mut self, _: &RemoveAllContext, cx: &mut ViewContext<Self>) {
self.context_store.update(cx, |store, _cx| store.clear());
cx.notify();
}
fn chat(&mut self, _: &Chat, cx: &mut ViewContext<Self>) {
self.send_to_model(RequestKind::Chat, cx);
}
@@ -147,10 +137,9 @@ impl MessageEditor {
editor.clear(cx);
text
});
let context = self.context_store.update(cx, |this, _cx| this.drain());
let thread = self.thread.clone();
thread.update(cx, |thread, cx| {
let context = self.context_store.read(cx).snapshot(cx).collect::<Vec<_>>();
self.thread.update(cx, |thread, cx| {
thread.insert_user_message(user_message, context, cx);
let mut request = thread.to_completion_request(request_kind, cx);
@@ -206,16 +195,6 @@ impl MessageEditor {
let editor_focus_handle = self.editor.focus_handle(cx);
cx.focus(&editor_focus_handle);
}
fn handle_context_strip_event(
&mut self,
_context_strip: View<ContextStrip>,
ContextStripEvent::PickerDismissed: &ContextStripEvent,
cx: &mut ViewContext<Self>,
) {
let editor_focus_handle = self.editor.focus_handle(cx);
cx.focus(&editor_focus_handle);
}
}
impl FocusableView for MessageEditor {
@@ -237,7 +216,6 @@ impl Render for MessageEditor {
.on_action(cx.listener(Self::chat))
.on_action(cx.listener(Self::toggle_model_selector))
.on_action(cx.listener(Self::toggle_context_picker))
.on_action(cx.listener(Self::remove_all_context))
.size_full()
.gap_2()
.p_2()
@@ -275,7 +253,7 @@ impl Render for MessageEditor {
.anchor(gpui::Corner::BottomLeft)
.offset(gpui::Point {
x: px(0.0),
y: (-get_ui_font_size(cx) * 2) - px(4.0),
y: px(-16.0),
})
.with_handle(self.inline_context_picker_menu_handle.clone()),
)
@@ -284,7 +262,7 @@ impl Render for MessageEditor {
.justify_between()
.child(SwitchWithLabel::new(
"use-tools",
Label::new("Tools").size(LabelSize::Small),
Label::new("Tools"),
self.use_tools.into(),
cx.listener(|this, selection, _cx| {
this.use_tools = match selection {
@@ -300,7 +278,7 @@ impl Render for MessageEditor {
ButtonLike::new("chat")
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.child(Label::new("Submit").size(LabelSize::Small))
.child(Label::new("Submit"))
.children(
KeyBinding::for_action_in(&Chat, &focus_handle, cx)
.map(|binding| binding.into_any_element()),

View File

@@ -78,7 +78,7 @@ impl TerminalInlineAssistant {
let prompt_buffer = cx.new_model(|cx| {
MultiBuffer::singleton(cx.new_model(|cx| Buffer::local(String::new(), cx)), cx)
});
let context_store = cx.new_model(|_cx| ContextStore::new(workspace.clone()));
let context_store = cx.new_model(|_cx| ContextStore::new());
let codegen = cx.new_model(|_| TerminalCodegen::new(terminal, self.telemetry.clone()));
let prompt_editor = cx.new_view(|cx| {
@@ -245,10 +245,10 @@ impl TerminalInlineAssistant {
cache: false,
};
attach_context_to_message(
&mut request_message,
assist.context_store.read(cx).snapshot(cx),
);
let context = assist
.context_store
.update(cx, |this, _cx| this.context().clone());
attach_context_to_message(&mut request_message, context);
request_message.content.push(prompt.into());

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
use anyhow::Result;
use assistant_tool::ToolWorkingSet;
use chrono::{DateTime, Utc};
use collections::{BTreeMap, HashMap, HashSet};
use collections::HashMap;
use futures::future::Shared;
use futures::{FutureExt as _, StreamExt as _};
use gpui::{AppContext, EventEmitter, ModelContext, SharedString, Task};
@@ -17,7 +17,7 @@ use serde::{Deserialize, Serialize};
use util::{post_inc, TryFutureExt as _};
use uuid::Uuid;
use crate::context::{attach_context_to_message, ContextId, ContextSnapshot};
use crate::context::{attach_context_to_message, Context};
#[derive(Debug, Clone, Copy)]
pub enum RequestKind {
@@ -64,8 +64,7 @@ pub struct Thread {
pending_summary: Task<Option<()>>,
messages: Vec<Message>,
next_message_id: MessageId,
context: BTreeMap<ContextId, ContextSnapshot>,
context_by_message: HashMap<MessageId, Vec<ContextId>>,
context_by_message: HashMap<MessageId, Vec<Context>>,
completion_count: usize,
pending_completions: Vec<PendingCompletion>,
tools: Arc<ToolWorkingSet>,
@@ -83,7 +82,6 @@ impl Thread {
pending_summary: Task::ready(None),
messages: Vec::new(),
next_message_id: MessageId(0),
context: BTreeMap::default(),
context_by_message: HashMap::default(),
completion_count: 0,
pending_completions: Vec::new(),
@@ -131,15 +129,8 @@ impl Thread {
&self.tools
}
pub fn context_for_message(&self, id: MessageId) -> Option<Vec<ContextSnapshot>> {
let context = self.context_by_message.get(&id)?;
Some(
context
.into_iter()
.filter_map(|context_id| self.context.get(&context_id))
.cloned()
.collect::<Vec<_>>(),
)
pub fn context_for_message(&self, id: MessageId) -> Option<&Vec<Context>> {
self.context_by_message.get(&id)
}
pub fn pending_tool_uses(&self) -> Vec<&PendingToolUse> {
@@ -149,14 +140,11 @@ impl Thread {
pub fn insert_user_message(
&mut self,
text: impl Into<String>,
context: Vec<ContextSnapshot>,
context: Vec<Context>,
cx: &mut ModelContext<Self>,
) {
let message_id = self.insert_message(Role::User, text, cx);
let context_ids = context.iter().map(|context| context.id).collect::<Vec<_>>();
self.context
.extend(context.into_iter().map(|context| (context.id, context)));
self.context_by_message.insert(message_id, context_ids);
self.context_by_message.insert(message_id, context);
}
pub fn insert_message(
@@ -209,13 +197,7 @@ impl Thread {
temperature: None,
};
let mut referenced_context_ids = HashSet::default();
for message in &self.messages {
if let Some(context_ids) = self.context_by_message.get(&message.id) {
referenced_context_ids.extend(context_ids);
}
let mut request_message = LanguageModelRequestMessage {
role: message.role,
content: Vec::new(),
@@ -230,6 +212,10 @@ impl Thread {
}
}
if let Some(context) = self.context_for_message(message.id) {
attach_context_to_message(&mut request_message, context.clone());
}
if !message.text.is_empty() {
request_message
.content
@@ -247,22 +233,6 @@ impl Thread {
request.messages.push(request_message);
}
if !referenced_context_ids.is_empty() {
let mut context_message = LanguageModelRequestMessage {
role: Role::User,
content: Vec::new(),
cache: false,
};
let referenced_context = referenced_context_ids
.into_iter()
.filter_map(|context_id| self.context.get(context_id))
.cloned();
attach_context_to_message(&mut context_message, referenced_context);
request.messages.push(context_message);
}
request
}

View File

@@ -1,153 +1,65 @@
use std::rc::Rc;
use gpui::ClickEvent;
use ui::{prelude::*, IconButtonShape, Tooltip};
use ui::{prelude::*, IconButtonShape};
use crate::context::{ContextKind, ContextSnapshot};
use crate::context::{Context, ContextKind};
#[derive(IntoElement)]
pub enum ContextPill {
Added {
context: ContextSnapshot,
dupe_name: bool,
on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext)>>,
},
Suggested {
name: SharedString,
kind: ContextKind,
on_add: Rc<dyn Fn(&ClickEvent, &mut WindowContext)>,
},
pub struct ContextPill {
context: Context,
on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext)>>,
}
impl ContextPill {
pub fn new_added(
context: ContextSnapshot,
dupe_name: bool,
on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext)>>,
) -> Self {
Self::Added {
pub fn new(context: Context) -> Self {
Self {
context,
dupe_name,
on_remove,
on_remove: None,
}
}
pub fn new_suggested(
name: SharedString,
kind: ContextKind,
on_add: Rc<dyn Fn(&ClickEvent, &mut WindowContext)>,
) -> Self {
Self::Suggested { name, kind, on_add }
}
pub fn id(&self) -> ElementId {
match self {
Self::Added { context, .. } => {
ElementId::NamedInteger("context-pill".into(), context.id.0)
}
Self::Suggested { .. } => "suggested-context-pill".into(),
}
}
pub fn kind(&self) -> ContextKind {
match self {
Self::Added { context, .. } => context.kind,
Self::Suggested { kind, .. } => *kind,
}
pub fn on_remove(mut self, on_remove: Rc<dyn Fn(&ClickEvent, &mut WindowContext)>) -> Self {
self.on_remove = Some(on_remove);
self
}
}
impl RenderOnce for ContextPill {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let icon = match &self.kind() {
ContextKind::File => IconName::File,
let padding_right = if self.on_remove.is_some() {
px(2.)
} else {
px(4.)
};
let icon = match self.context.kind {
ContextKind::File(_) => IconName::File,
ContextKind::Directory => IconName::Folder,
ContextKind::FetchedUrl => IconName::Globe,
ContextKind::Thread => IconName::MessageCircle,
ContextKind::Thread(_) => IconName::MessageCircle,
};
let color = cx.theme().colors();
let base_pill = h_flex()
.id(self.id())
h_flex()
.gap_1()
.pl_1()
.pr(padding_right)
.pb(px(1.))
.border_1()
.border_color(cx.theme().colors().border.opacity(0.5))
.bg(cx.theme().colors().element_background)
.rounded_md()
.gap_1()
.child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted));
match &self {
ContextPill::Added {
context,
dupe_name,
on_remove,
} => base_pill
.bg(color.element_background)
.border_color(color.border.opacity(0.5))
.pr(if on_remove.is_some() { px(2.) } else { px(4.) })
.child(
h_flex()
.id("context-data")
.gap_1()
.child(Label::new(context.name.clone()).size(LabelSize::Small))
.when_some(context.parent.as_ref(), |element, parent_name| {
if *dupe_name {
element.child(
Label::new(parent_name.clone())
.size(LabelSize::XSmall)
.color(Color::Muted),
)
} else {
element
}
})
.when_some(context.tooltip.clone(), |element, tooltip| {
element.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
.child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted))
.child(Label::new(self.context.name.clone()).size(LabelSize::Small))
.when_some(self.on_remove, |parent, on_remove| {
parent.child(
IconButton::new(("remove", self.context.id.0), IconName::Close)
.shape(IconButtonShape::Square)
.icon_size(IconSize::XSmall)
.on_click({
let on_remove = on_remove.clone();
move |event, cx| on_remove(event, cx)
}),
)
.when_some(on_remove.as_ref(), |element, on_remove| {
element.child(
IconButton::new(("remove", context.id.0), IconName::Close)
.shape(IconButtonShape::Square)
.icon_size(IconSize::XSmall)
.tooltip(|cx| Tooltip::text("Remove Context", cx))
.on_click({
let on_remove = on_remove.clone();
move |event, cx| on_remove(event, cx)
}),
)
}),
ContextPill::Suggested { name, kind, on_add } => base_pill
.cursor_pointer()
.pr_1()
.border_color(color.border_variant.opacity(0.5))
.hover(|style| style.bg(color.element_hover.opacity(0.5)))
.child(
Label::new(name.clone())
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(
Label::new(match kind {
ContextKind::File => "Open File",
ContextKind::Thread | ContextKind::Directory | ContextKind::FetchedUrl => {
"Active"
}
})
.size(LabelSize::XSmall)
.color(Color::Muted),
)
.child(
Icon::new(IconName::Plus)
.size(IconSize::XSmall)
.into_any_element(),
)
.tooltip(|cx| Tooltip::with_meta("Suggested Context", None, "Click to add it", cx))
.on_click({
let on_add = on_add.clone();
move |event, cx| on_add(event, cx)
}),
}
})
}
}

View File

@@ -19,7 +19,10 @@ use tempfile::NamedTempFile;
use util::paths::PathWithPosition;
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
use std::io::IsTerminal;
use {
std::io::IsTerminal,
util::{load_login_shell_environment, load_shell_from_passwd, ResultExt},
};
struct Detect;
@@ -164,24 +167,15 @@ fn main() -> Result<()> {
None
};
let env = {
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
{
// On Linux, the desktop entry uses `cli` to spawn `zed`.
// We need to handle env vars correctly since std::env::vars() may not contain
// project-specific vars (e.g. those set by direnv).
// By setting env to None here, the LSP will use worktree env vars instead,
// which is what we want.
if !std::io::stdout().is_terminal() {
None
} else {
Some(std::env::vars().collect::<HashMap<_, _>>())
}
}
// On Linux, desktop entry uses `cli` to spawn `zed`, so we need to load env vars from the shell
// since it doesn't inherit env vars from the terminal.
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
if !std::io::stdout().is_terminal() {
load_shell_from_passwd().log_err();
load_login_shell_environment().log_err();
}
#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
Some(std::env::vars().collect::<HashMap<_, _>>())
};
let env = Some(std::env::vars().collect::<HashMap<_, _>>());
let exit_status = Arc::new(Mutex::new(None));
let mut paths = vec![];

View File

@@ -438,8 +438,7 @@ CREATE TABLE IF NOT EXISTS billing_subscriptions (
billing_customer_id INTEGER NOT NULL REFERENCES billing_customers(id),
stripe_subscription_id TEXT NOT NULL,
stripe_subscription_status TEXT NOT NULL,
stripe_cancel_at TIMESTAMP,
stripe_cancellation_reason TEXT
stripe_cancel_at TIMESTAMP
);
CREATE INDEX "ix_billing_subscriptions_on_billing_customer_id" ON billing_subscriptions (billing_customer_id);

View File

@@ -1,2 +0,0 @@
alter table billing_subscriptions
add column stripe_cancellation_reason text;

View File

@@ -12,8 +12,8 @@ use serde::{Deserialize, Serialize};
use serde_json::json;
use std::{str::FromStr, sync::Arc, time::Duration};
use stripe::{
BillingPortalSession, CancellationDetailsReason, CreateBillingPortalSession,
CreateBillingPortalSessionFlowData, CreateBillingPortalSessionFlowDataAfterCompletion,
BillingPortalSession, CreateBillingPortalSession, CreateBillingPortalSessionFlowData,
CreateBillingPortalSessionFlowDataAfterCompletion,
CreateBillingPortalSessionFlowDataAfterCompletionRedirect,
CreateBillingPortalSessionFlowDataType, CreateCustomer, Customer, CustomerId, EventObject,
EventType, Expandable, ListEvents, Subscription, SubscriptionId, SubscriptionStatus,
@@ -21,10 +21,8 @@ use stripe::{
use util::ResultExt;
use crate::api::events::SnowflakeRow;
use crate::db::billing_subscription::{StripeCancellationReason, StripeSubscriptionStatus};
use crate::llm::{DEFAULT_MAX_MONTHLY_SPEND, FREE_TIER_MONTHLY_SPENDING_LIMIT};
use crate::rpc::{ResultExt as _, Server};
use crate::{db::UserId, llm::db::LlmDatabase};
use crate::{
db::{
billing_customer, BillingSubscriptionId, CreateBillingCustomerParams,
@@ -34,6 +32,10 @@ use crate::{
},
stripe_billing::StripeBilling,
};
use crate::{
db::{billing_subscription::StripeSubscriptionStatus, UserId},
llm::db::LlmDatabase,
};
use crate::{AppState, Cents, Error, Result};
pub fn router() -> Router {
@@ -249,13 +251,6 @@ async fn create_billing_subscription(
));
}
if app.db.has_overdue_billing_subscriptions(user.id).await? {
return Err(Error::http(
StatusCode::PAYMENT_REQUIRED,
"user has overdue billing subscriptions".into(),
));
}
let customer_id =
if let Some(existing_customer) = app.db.get_billing_customer_by_user_id(user.id).await? {
CustomerId::from_str(&existing_customer.stripe_customer_id)
@@ -684,12 +679,6 @@ async fn handle_customer_subscription_event(
.and_then(|cancel_at| DateTime::from_timestamp(cancel_at, 0))
.map(|time| time.naive_utc()),
),
stripe_cancellation_reason: ActiveValue::set(
subscription
.cancellation_details
.and_then(|details| details.reason)
.map(|reason| reason.into()),
),
},
)
.await?;
@@ -726,10 +715,6 @@ async fn handle_customer_subscription_event(
billing_customer_id: billing_customer.id,
stripe_subscription_id: subscription.id.to_string(),
stripe_subscription_status: subscription.status.into(),
stripe_cancellation_reason: subscription
.cancellation_details
.and_then(|details| details.reason)
.map(|reason| reason.into()),
})
.await?;
}
@@ -806,16 +791,6 @@ impl From<SubscriptionStatus> for StripeSubscriptionStatus {
}
}
impl From<CancellationDetailsReason> for StripeCancellationReason {
fn from(value: CancellationDetailsReason) -> Self {
match value {
CancellationDetailsReason::CancellationRequested => Self::CancellationRequested,
CancellationDetailsReason::PaymentDisputed => Self::PaymentDisputed,
CancellationDetailsReason::PaymentFailed => Self::PaymentFailed,
}
}
}
/// Finds or creates a billing customer using the provided customer.
async fn find_or_create_billing_customer(
app: &Arc<AppState>,

View File

@@ -1,4 +1,4 @@
use crate::db::billing_subscription::{StripeCancellationReason, StripeSubscriptionStatus};
use crate::db::billing_subscription::StripeSubscriptionStatus;
use super::*;
@@ -7,7 +7,6 @@ pub struct CreateBillingSubscriptionParams {
pub billing_customer_id: BillingCustomerId,
pub stripe_subscription_id: String,
pub stripe_subscription_status: StripeSubscriptionStatus,
pub stripe_cancellation_reason: Option<StripeCancellationReason>,
}
#[derive(Debug, Default)]
@@ -16,7 +15,6 @@ pub struct UpdateBillingSubscriptionParams {
pub stripe_subscription_id: ActiveValue<String>,
pub stripe_subscription_status: ActiveValue<StripeSubscriptionStatus>,
pub stripe_cancel_at: ActiveValue<Option<DateTime>>,
pub stripe_cancellation_reason: ActiveValue<Option<StripeCancellationReason>>,
}
impl Database {
@@ -30,7 +28,6 @@ impl Database {
billing_customer_id: ActiveValue::set(params.billing_customer_id),
stripe_subscription_id: ActiveValue::set(params.stripe_subscription_id.clone()),
stripe_subscription_status: ActiveValue::set(params.stripe_subscription_status),
stripe_cancellation_reason: ActiveValue::set(params.stripe_cancellation_reason),
..Default::default()
})
.exec_without_returning(&*tx)
@@ -54,7 +51,6 @@ impl Database {
stripe_subscription_id: params.stripe_subscription_id.clone(),
stripe_subscription_status: params.stripe_subscription_status.clone(),
stripe_cancel_at: params.stripe_cancel_at.clone(),
stripe_cancellation_reason: params.stripe_cancellation_reason.clone(),
..Default::default()
})
.exec(&*tx)
@@ -170,40 +166,4 @@ impl Database {
})
.await
}
/// Returns whether the user has any overdue billing subscriptions.
pub async fn has_overdue_billing_subscriptions(&self, user_id: UserId) -> Result<bool> {
Ok(self.count_overdue_billing_subscriptions(user_id).await? > 0)
}
/// Returns the count of the overdue billing subscriptions for the user with the specified ID.
///
/// This includes subscriptions:
/// - Whose status is `past_due`
/// - Whose status is `canceled` and the cancellation reason is `payment_failed`
pub async fn count_overdue_billing_subscriptions(&self, user_id: UserId) -> Result<usize> {
self.transaction(|tx| async move {
let past_due = billing_subscription::Column::StripeSubscriptionStatus
.eq(StripeSubscriptionStatus::PastDue);
let payment_failed = billing_subscription::Column::StripeSubscriptionStatus
.eq(StripeSubscriptionStatus::Canceled)
.and(
billing_subscription::Column::StripeCancellationReason
.eq(StripeCancellationReason::PaymentFailed),
);
let count = billing_subscription::Entity::find()
.inner_join(billing_customer::Entity)
.filter(
billing_customer::Column::UserId
.eq(user_id)
.and(past_due.or(payment_failed)),
)
.count(&*tx)
.await?;
Ok(count as usize)
})
.await
}
}

View File

@@ -12,7 +12,6 @@ pub struct Model {
pub stripe_subscription_id: String,
pub stripe_subscription_status: StripeSubscriptionStatus,
pub stripe_cancel_at: Option<DateTime>,
pub stripe_cancellation_reason: Option<StripeCancellationReason>,
pub created_at: DateTime,
}
@@ -74,18 +73,3 @@ impl StripeSubscriptionStatus {
}
}
}
/// The cancellation reason for a Stripe subscription.
///
/// [Stripe docs](https://docs.stripe.com/api/subscriptions/object#subscription_object-cancellation_details-reason)
#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Hash, Serialize)]
#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")]
#[serde(rename_all = "snake_case")]
pub enum StripeCancellationReason {
#[sea_orm(string_value = "cancellation_requested")]
CancellationRequested,
#[sea_orm(string_value = "payment_disputed")]
PaymentDisputed,
#[sea_orm(string_value = "payment_failed")]
PaymentFailed,
}

View File

@@ -1,6 +1,6 @@
use std::sync::Arc;
use crate::db::billing_subscription::{StripeCancellationReason, StripeSubscriptionStatus};
use crate::db::billing_subscription::StripeSubscriptionStatus;
use crate::db::tests::new_test_user;
use crate::db::{CreateBillingCustomerParams, CreateBillingSubscriptionParams};
use crate::test_both_dbs;
@@ -41,7 +41,6 @@ async fn test_get_active_billing_subscriptions(db: &Arc<Database>) {
billing_customer_id: customer.id,
stripe_subscription_id: "sub_active_user".into(),
stripe_subscription_status: StripeSubscriptionStatus::Active,
stripe_cancellation_reason: None,
})
.await
.unwrap();
@@ -76,7 +75,6 @@ async fn test_get_active_billing_subscriptions(db: &Arc<Database>) {
billing_customer_id: customer.id,
stripe_subscription_id: "sub_past_due_user".into(),
stripe_subscription_status: StripeSubscriptionStatus::PastDue,
stripe_cancellation_reason: None,
})
.await
.unwrap();
@@ -88,113 +86,3 @@ async fn test_get_active_billing_subscriptions(db: &Arc<Database>) {
assert_eq!(subscription_count, 0);
}
}
test_both_dbs!(
test_count_overdue_billing_subscriptions,
test_count_overdue_billing_subscriptions_postgres,
test_count_overdue_billing_subscriptions_sqlite
);
async fn test_count_overdue_billing_subscriptions(db: &Arc<Database>) {
// A user with no subscription has no overdue billing subscriptions.
{
let user_id = new_test_user(db, "no-subscription-user@example.com").await;
let subscription_count = db
.count_overdue_billing_subscriptions(user_id)
.await
.unwrap();
assert_eq!(subscription_count, 0);
}
// A user with a past-due subscription has an overdue billing subscription.
{
let user_id = new_test_user(db, "past-due-user@example.com").await;
let customer = db
.create_billing_customer(&CreateBillingCustomerParams {
user_id,
stripe_customer_id: "cus_past_due_user".into(),
})
.await
.unwrap();
assert_eq!(customer.stripe_customer_id, "cus_past_due_user".to_string());
db.create_billing_subscription(&CreateBillingSubscriptionParams {
billing_customer_id: customer.id,
stripe_subscription_id: "sub_past_due_user".into(),
stripe_subscription_status: StripeSubscriptionStatus::PastDue,
stripe_cancellation_reason: None,
})
.await
.unwrap();
let subscription_count = db
.count_overdue_billing_subscriptions(user_id)
.await
.unwrap();
assert_eq!(subscription_count, 1);
}
// A user with a canceled subscription with a reason of `payment_failed` has an overdue billing subscription.
{
let user_id =
new_test_user(db, "canceled-subscription-payment-failed-user@example.com").await;
let customer = db
.create_billing_customer(&CreateBillingCustomerParams {
user_id,
stripe_customer_id: "cus_canceled_subscription_payment_failed_user".into(),
})
.await
.unwrap();
assert_eq!(
customer.stripe_customer_id,
"cus_canceled_subscription_payment_failed_user".to_string()
);
db.create_billing_subscription(&CreateBillingSubscriptionParams {
billing_customer_id: customer.id,
stripe_subscription_id: "sub_canceled_subscription_payment_failed_user".into(),
stripe_subscription_status: StripeSubscriptionStatus::Canceled,
stripe_cancellation_reason: Some(StripeCancellationReason::PaymentFailed),
})
.await
.unwrap();
let subscription_count = db
.count_overdue_billing_subscriptions(user_id)
.await
.unwrap();
assert_eq!(subscription_count, 1);
}
// A user with a canceled subscription with a reason of `cancellation_requested` has no overdue billing subscriptions.
{
let user_id = new_test_user(db, "canceled-subscription-user@example.com").await;
let customer = db
.create_billing_customer(&CreateBillingCustomerParams {
user_id,
stripe_customer_id: "cus_canceled_subscription_user".into(),
})
.await
.unwrap();
assert_eq!(
customer.stripe_customer_id,
"cus_canceled_subscription_user".to_string()
);
db.create_billing_subscription(&CreateBillingSubscriptionParams {
billing_customer_id: customer.id,
stripe_subscription_id: "sub_canceled_subscription_user".into(),
stripe_subscription_status: StripeSubscriptionStatus::Canceled,
stripe_cancellation_reason: Some(StripeCancellationReason::CancellationRequested),
})
.await
.unwrap();
let subscription_count = db
.count_overdue_billing_subscriptions(user_id)
.await
.unwrap();
assert_eq!(subscription_count, 0);
}
}

View File

@@ -459,15 +459,7 @@ async fn predict_edits(
.prediction_model
.as_ref()
.context("no PREDICTION_MODEL configured on the server")?;
let outline_prefix = params
.outline
.as_ref()
.map(|outline| format!("### Outline for current file:\n{}\n", outline))
.unwrap_or_default();
let prompt = include_str!("./llm/prediction_prompt.md")
.replace("<outline>", &outline_prefix)
.replace("<events>", &params.input_events)
.replace("<excerpt>", &params.input_excerpt);
let mut response = open_ai::complete_text(

View File

@@ -1,4 +1,3 @@
<outline>## Task
Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.
### Instruction:

View File

@@ -4065,7 +4065,7 @@ async fn test_collaborating_with_diagnostics(
DiagnosticEntry {
range: Point::new(0, 4)..Point::new(0, 7),
diagnostic: Diagnostic {
group_id: 2,
group_id: 3,
message: "message 1".to_string(),
severity: lsp::DiagnosticSeverity::ERROR,
is_primary: true,
@@ -4075,7 +4075,7 @@ async fn test_collaborating_with_diagnostics(
DiagnosticEntry {
range: Point::new(0, 10)..Point::new(0, 13),
diagnostic: Diagnostic {
group_id: 3,
group_id: 4,
severity: lsp::DiagnosticSeverity::WARNING,
message: "message 2".to_string(),
is_primary: true,

View File

@@ -1134,7 +1134,7 @@ impl RandomizedTest for ProjectCollaborationTest {
let end = PointUtf16::new(end_row, end_column);
let range = if start > end { end..start } else { start..end };
highlights.push(lsp::DocumentHighlight {
range: range_to_lsp(range.clone()).unwrap(),
range: range_to_lsp(range.clone()),
kind: Some(lsp::DocumentHighlightKind::READ),
});
}

View File

@@ -17,8 +17,8 @@ pub struct CopilotCompletionProvider {
completions: Vec<Completion>,
active_completion_index: usize,
file_extension: Option<String>,
pending_refresh: Option<Task<Result<()>>>,
pending_cycling_refresh: Option<Task<Result<()>>>,
pending_refresh: Task<Result<()>>,
pending_cycling_refresh: Task<Result<()>>,
copilot: Model<Copilot>,
}
@@ -30,8 +30,8 @@ impl CopilotCompletionProvider {
completions: Vec::new(),
active_completion_index: 0,
file_extension: None,
pending_refresh: None,
pending_cycling_refresh: None,
pending_refresh: Task::ready(Ok(())),
pending_cycling_refresh: Task::ready(Ok(())),
copilot,
}
}
@@ -63,14 +63,6 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
false
}
fn show_completions_in_normal_mode() -> bool {
false
}
fn is_refreshing(&self) -> bool {
self.pending_refresh.is_some()
}
fn is_enabled(
&self,
buffer: &Model<Buffer>,
@@ -96,7 +88,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
cx: &mut ModelContext<Self>,
) {
let copilot = self.copilot.clone();
self.pending_refresh = Some(cx.spawn(|this, mut cx| async move {
self.pending_refresh = cx.spawn(|this, mut cx| async move {
if debounce {
cx.background_executor()
.timer(COPILOT_DEBOUNCE_TIMEOUT)
@@ -112,8 +104,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
this.update(&mut cx, |this, cx| {
if !completions.is_empty() {
this.cycled = false;
this.pending_refresh = None;
this.pending_cycling_refresh = None;
this.pending_cycling_refresh = Task::ready(Ok(()));
this.completions.clear();
this.active_completion_index = 0;
this.buffer_id = Some(buffer.entity_id());
@@ -134,7 +125,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
})?;
Ok(())
}));
});
}
fn cycle(
@@ -166,7 +157,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
cx.notify();
} else {
let copilot = self.copilot.clone();
self.pending_cycling_refresh = Some(cx.spawn(|this, mut cx| async move {
self.pending_cycling_refresh = cx.spawn(|this, mut cx| async move {
let completions = copilot
.update(&mut cx, |copilot, cx| {
copilot.completions_cycling(&buffer, cursor_position, cx)
@@ -190,7 +181,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
})?;
Ok(())
}));
});
}
}

View File

@@ -18,6 +18,7 @@ collections.workspace = true
ctor.workspace = true
editor.workspace = true
env_logger.workspace = true
feature_flags.workspace = true
gpui.workspace = true
language.workspace = true
log.workspace = true

View File

@@ -14,6 +14,7 @@ use editor::{
scroll::Autoscroll,
Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, ToOffset,
};
use feature_flags::FeatureFlagAppExt;
use gpui::{
actions, div, svg, AnyElement, AnyView, AppContext, Context, EventEmitter, FocusHandle,
FocusableView, Global, HighlightStyle, InteractiveElement, IntoElement, Model, ParentElement,
@@ -840,61 +841,72 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
h_flex()
.id(DIAGNOSTIC_HEADER)
.block_mouse_down()
.h(2. * cx.line_height())
.w_full()
.px_9()
.justify_between()
.gap_2()
.relative()
.child(
div()
.top(px(0.))
.absolute()
.w_full()
.h_px()
.bg(color.border_variant),
)
.child(
h_flex()
.block_mouse_down()
.h(2. * cx.line_height())
.pl_10()
.pr_5()
.w_full()
.justify_between()
.gap_2()
.px_1()
.rounded_md()
.bg(color.surface_background.opacity(0.5))
.map(|stack| {
stack.child(
svg()
.size(cx.text_style().font_size)
.flex_none()
.map(|icon| {
if diagnostic.severity == DiagnosticSeverity::ERROR {
icon.path(IconName::XCircle.path())
.text_color(Color::Error.color(cx))
} else {
icon.path(IconName::Warning.path())
.text_color(Color::Warning.color(cx))
}
}),
)
})
.child(
h_flex()
.gap_1()
.gap_3()
.map(|stack| {
stack.child(svg().size(cx.text_style().font_size).flex_none().map(
|icon| {
if diagnostic.severity == DiagnosticSeverity::ERROR {
icon.path(IconName::XCircle.path())
.text_color(Color::Error.color(cx))
} else {
icon.path(IconName::Warning.path())
.text_color(Color::Warning.color(cx))
}
},
))
})
.child(
StyledText::new(message.clone()).with_highlights(
&cx.text_style(),
code_ranges
.iter()
.map(|range| (range.clone(), highlight_style)),
),
h_flex()
.gap_1()
.child(
StyledText::new(message.clone()).with_highlights(
&cx.text_style(),
code_ranges
.iter()
.map(|range| (range.clone(), highlight_style)),
),
)
.when_some(diagnostic.code.as_ref(), |stack, code| {
stack.child(
div()
.child(SharedString::from(format!("({code})")))
.text_color(cx.theme().colors().text_muted),
)
}),
),
)
.child(h_flex().gap_1().when_some(
diagnostic.source.as_ref(),
|stack, source| {
stack.child(
div()
.child(SharedString::from(source.clone()))
.text_color(cx.theme().colors().text_muted),
)
.when_some(diagnostic.code.as_ref(), |stack, code| {
stack.child(
div()
.child(SharedString::from(format!("({code})")))
.text_color(color.text_muted),
)
}),
),
},
)),
)
.when_some(diagnostic.source.as_ref(), |stack, source| {
stack.child(
div()
.child(SharedString::from(source.clone()))
.text_color(color.text_muted),
)
})
.into_any_element()
})
}
@@ -932,16 +944,18 @@ fn context_range_for_entry(
snapshot: &BufferSnapshot,
cx: &AppContext,
) -> Range<Point> {
if let Some(rows) = heuristic_syntactic_expand(
entry.range.clone(),
DIAGNOSTIC_EXPANSION_ROW_LIMIT,
snapshot,
cx,
) {
return Range {
start: Point::new(*rows.start(), 0),
end: snapshot.clip_point(Point::new(*rows.end(), u32::MAX), Bias::Left),
};
if cx.is_staff() {
if let Some(rows) = heuristic_syntactic_expand(
entry.range.clone(),
DIAGNOSTIC_EXPANSION_ROW_LIMIT,
snapshot,
cx,
) {
return Range {
start: Point::new(*rows.start(), 0),
end: snapshot.clip_point(Point::new(*rows.end(), u32::MAX), Bias::Left),
};
}
}
Range {
start: Point::new(entry.range.start.row.saturating_sub(context), 0),

View File

@@ -82,7 +82,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
severity: DiagnosticSeverity::INFORMATION,
is_primary: false,
is_disk_based: true,
group_id: 1,
group_id: 2,
..Default::default()
},
},
@@ -95,7 +95,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
severity: DiagnosticSeverity::INFORMATION,
is_primary: false,
is_disk_based: true,
group_id: 0,
group_id: 1,
..Default::default()
},
},
@@ -106,7 +106,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
severity: DiagnosticSeverity::INFORMATION,
is_primary: false,
is_disk_based: true,
group_id: 1,
group_id: 2,
..Default::default()
},
},
@@ -117,7 +117,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
severity: DiagnosticSeverity::INFORMATION,
is_primary: false,
is_disk_based: true,
group_id: 0,
group_id: 1,
..Default::default()
},
},
@@ -128,7 +128,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
severity: DiagnosticSeverity::ERROR,
is_primary: true,
is_disk_based: true,
group_id: 0,
group_id: 1,
..Default::default()
},
},
@@ -139,7 +139,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
severity: DiagnosticSeverity::ERROR,
is_primary: true,
is_disk_based: true,
group_id: 1,
group_id: 2,
..Default::default()
},
},
@@ -241,7 +241,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
severity: DiagnosticSeverity::ERROR,
is_primary: true,
is_disk_based: true,
group_id: 0,
group_id: 1,
..Default::default()
},
}],
@@ -348,7 +348,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
severity: DiagnosticSeverity::ERROR,
is_primary: true,
is_disk_based: true,
group_id: 0,
group_id: 1,
..Default::default()
},
},
@@ -359,7 +359,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
severity: DiagnosticSeverity::ERROR,
is_primary: true,
is_disk_based: true,
group_id: 1,
group_id: 2,
..Default::default()
},
},
@@ -775,7 +775,7 @@ async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) {
assert!(view.focus_handle.is_focused(cx));
});
let mut next_group_id = 0;
let mut next_group_id = 1;
let mut next_filename = 0;
let mut language_server_ids = vec![LanguageServerId(0)];
let mut updated_language_servers = HashSet::default();

View File

@@ -1,6 +1,6 @@
//! This module contains all actions supported by [`Editor`].
use super::*;
use gpui::{action_aliases, action_as};
use gpui::action_as;
use util::serde::default_true;
#[derive(PartialEq, Clone, Deserialize, Default)]
@@ -204,7 +204,7 @@ impl_actions!(
ToggleCodeActions,
ToggleComments,
UnfoldAt,
FoldAtLevel,
FoldAtLevel
]
);
@@ -311,6 +311,7 @@ gpui::actions!(
OpenExcerpts,
OpenExcerptsSplit,
OpenProposedChangesEditor,
OpenFile,
OpenDocs,
OpenPermalinkToLine,
OpenUrl,
@@ -388,5 +389,3 @@ gpui::actions!(
);
action_as!(go_to_line, ToggleGoToLine as Toggle);
action_aliases!(editor, OpenSelectedFilename, [OpenFile]);

View File

@@ -158,7 +158,7 @@ pub struct CompletionsMenu {
pub buffer: Model<Buffer>,
pub completions: Rc<RefCell<Box<[Completion]>>>,
match_candidates: Rc<[StringMatchCandidate]>,
pub entries: Rc<RefCell<Vec<CompletionEntry>>>,
pub entries: Rc<[CompletionEntry]>,
pub selected_item: usize,
scroll_handle: UniformListScrollHandle,
resolve_completions: bool,
@@ -195,7 +195,7 @@ impl CompletionsMenu {
show_completion_documentation,
completions: RefCell::new(completions).into(),
match_candidates,
entries: RefCell::new(Vec::new()).into(),
entries: Vec::new().into(),
selected_item: 0,
scroll_handle: UniformListScrollHandle::new(),
resolve_completions: true,
@@ -244,7 +244,7 @@ impl CompletionsMenu {
string: completion.clone(),
})
})
.collect::<Vec<_>>();
.collect();
Self {
id,
sort_completions,
@@ -252,7 +252,7 @@ impl CompletionsMenu {
buffer,
completions: RefCell::new(completions).into(),
match_candidates,
entries: RefCell::new(entries).into(),
entries,
selected_item: 0,
scroll_handle: UniformListScrollHandle::new(),
resolve_completions: false,
@@ -290,8 +290,7 @@ impl CompletionsMenu {
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
let index = self.entries.borrow().len() - 1;
self.update_selection_index(index, provider, cx);
self.update_selection_index(self.entries.len() - 1, provider, cx);
}
fn update_selection_index(
@@ -313,12 +312,12 @@ impl CompletionsMenu {
if self.selected_item > 0 {
self.selected_item - 1
} else {
self.entries.borrow().len() - 1
self.entries.len() - 1
}
}
fn next_match_index(&self) -> usize {
if self.selected_item + 1 < self.entries.borrow().len() {
if self.selected_item + 1 < self.entries.len() {
self.selected_item + 1
} else {
0
@@ -327,18 +326,24 @@ impl CompletionsMenu {
pub fn show_inline_completion_hint(&mut self, hint: InlineCompletionMenuHint) {
let hint = CompletionEntry::InlineCompletionHint(hint);
let mut entries = self.entries.borrow_mut();
match entries.first() {
self.entries = match self.entries.first() {
Some(CompletionEntry::InlineCompletionHint { .. }) => {
let mut entries = Vec::from(&*self.entries);
entries[0] = hint;
entries
}
_ => {
if self.selected_item != 0 {
self.selected_item += 1;
}
entries.insert(0, hint);
let mut entries = Vec::with_capacity(self.entries.len() + 1);
entries.push(hint);
entries.extend_from_slice(&self.entries);
entries
}
}
.into();
if self.selected_item != 0 && self.selected_item + 1 < self.entries.len() {
self.selected_item += 1;
}
}
pub fn resolve_visible_completions(
@@ -364,14 +369,13 @@ impl CompletionsMenu {
let visible_count = last_rendered_range
.clone()
.map_or(APPROXIMATE_VISIBLE_COUNT, |range| range.count());
let entries = self.entries.borrow();
let entry_range = if self.selected_item == 0 {
0..min(visible_count, entries.len())
} else if self.selected_item == entries.len() - 1 {
entries.len().saturating_sub(visible_count)..entries.len()
0..min(visible_count, self.entries.len())
} else if self.selected_item == self.entries.len() - 1 {
self.entries.len().saturating_sub(visible_count)..self.entries.len()
} else {
last_rendered_range.map_or(0..0, |range| {
min(range.start, entries.len())..min(range.end, entries.len())
min(range.start, self.entries.len())..min(range.end, self.entries.len())
})
};
@@ -382,25 +386,24 @@ impl CompletionsMenu {
entry_range.clone(),
EXTRA_TO_RESOLVE,
EXTRA_TO_RESOLVE,
entries.len(),
self.entries.len(),
);
// Avoid work by sometimes filtering out completions that already have documentation.
// This filtering doesn't happen if the completions are currently being updated.
let completions = self.completions.borrow();
let candidate_ids = entry_indices
.flat_map(|i| Self::entry_candidate_id(&entries[i]))
.flat_map(|i| Self::entry_candidate_id(&self.entries[i]))
.filter(|i| completions[*i].documentation.is_none());
// Current selection is always resolved even if it already has documentation, to handle
// out-of-spec language servers that return more results later.
let candidate_ids = match Self::entry_candidate_id(&entries[self.selected_item]) {
let candidate_ids = match Self::entry_candidate_id(&self.entries[self.selected_item]) {
None => candidate_ids.collect::<Vec<usize>>(),
Some(selected_candidate_id) => iter::once(selected_candidate_id)
.chain(candidate_ids.filter(|id| *id != selected_candidate_id))
.collect::<Vec<usize>>(),
};
drop(entries);
if candidate_ids.is_empty() {
return;
@@ -429,7 +432,7 @@ impl CompletionsMenu {
}
pub fn visible(&self) -> bool {
!self.entries.borrow().is_empty()
!self.entries.is_empty()
}
fn origin(&self, cursor_position: DisplayPoint) -> ContextMenuOrigin {
@@ -446,7 +449,6 @@ impl CompletionsMenu {
let show_completion_documentation = self.show_completion_documentation;
let widest_completion_ix = self
.entries
.borrow()
.iter()
.enumerate()
.max_by_key(|(_, mat)| match mat {
@@ -473,19 +475,19 @@ impl CompletionsMenu {
let selected_item = self.selected_item;
let completions = self.completions.clone();
let entries = self.entries.clone();
let matches = self.entries.clone();
let last_rendered_range = self.last_rendered_range.clone();
let style = style.clone();
let list = uniform_list(
cx.view().clone(),
"completions",
self.entries.borrow().len(),
matches.len(),
move |_editor, range, cx| {
last_rendered_range.borrow_mut().replace(range.clone());
let start_ix = range.start;
let completions_guard = completions.borrow_mut();
entries.borrow()[range]
matches[range]
.iter()
.enumerate()
.map(|(ix, mat)| {
@@ -621,7 +623,7 @@ impl CompletionsMenu {
return None;
}
let multiline_docs = match &self.entries.borrow()[self.selected_item] {
let multiline_docs = match &self.entries[self.selected_item] {
CompletionEntry::Match(mat) => {
match self.completions.borrow_mut()[mat.candidate_id]
.documentation
@@ -767,14 +769,12 @@ impl CompletionsMenu {
}
drop(completions);
let mut entries = self.entries.borrow_mut();
if let Some(CompletionEntry::InlineCompletionHint(_)) = entries.first() {
entries.truncate(1);
} else {
entries.truncate(0);
let mut new_entries: Vec<_> = matches.into_iter().map(CompletionEntry::Match).collect();
if let Some(CompletionEntry::InlineCompletionHint(hint)) = self.entries.first() {
new_entries.insert(0, CompletionEntry::InlineCompletionHint(hint.clone()));
}
entries.extend(matches.into_iter().map(CompletionEntry::Match));
self.entries = new_entries.into();
self.selected_item = 0;
}
}

View File

@@ -42,7 +42,7 @@ use fold_map::{FoldMap, FoldSnapshot};
use gpui::{
AnyElement, Font, HighlightStyle, LineLayout, Model, ModelContext, Pixels, UnderlineStyle,
};
pub use inlay_map::Inlay;
pub(crate) use inlay_map::Inlay;
use inlay_map::{InlayMap, InlaySnapshot};
pub use inlay_map::{InlayOffset, InlayPoint};
use invisibles::{is_invisible, replacement};

View File

@@ -33,7 +33,7 @@ enum Transform {
}
#[derive(Debug, Clone)]
pub struct Inlay {
pub(crate) struct Inlay {
pub(crate) id: InlayId,
pub position: Anchor,
pub text: text::Rope,

View File

@@ -109,7 +109,7 @@ pub use proposed_changes_editor::{
ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar,
};
use similar::{ChangeTag, TextDiff};
use std::iter::Peekable;
use std::iter::{self, Peekable};
use task::{ResolvedTask, TaskTemplate, TaskVariables};
use hover_links::{find_file, HoverLink, HoveredLinkState, InlayHighlight};
@@ -258,7 +258,7 @@ pub fn render_parsed_markdown(
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum InlayId {
pub(crate) enum InlayId {
InlineCompletion(usize),
Hint(usize),
}
@@ -1787,17 +1787,6 @@ impl Editor {
self.refresh_inline_completion(false, true, cx);
}
pub fn inline_completions_enabled(&self, cx: &AppContext) -> bool {
let cursor = self.selections.newest_anchor().head();
if let Some((buffer, buffer_position)) =
self.buffer.read(cx).text_anchor_for_position(cursor, cx)
{
self.should_show_inline_completions(&buffer, buffer_position, cx)
} else {
false
}
}
fn should_show_inline_completions(
&self,
buffer: &Model<Buffer>,
@@ -3561,8 +3550,7 @@ impl Editor {
);
let multi_buffer_visible_range = multi_buffer_visible_start..multi_buffer_visible_end;
multi_buffer_snapshot
.range_to_buffer_ranges(multi_buffer_visible_range)
.into_iter()
.disjoint_ranges_to_buffer_ranges(iter::once(multi_buffer_visible_range))
.filter(|(_, excerpt_visible_range)| !excerpt_visible_range.is_empty())
.filter_map(|(excerpt, excerpt_visible_range)| {
let buffer_file = project::File::from_dyn(excerpt.buffer().file())?;
@@ -3603,7 +3591,7 @@ impl Editor {
}
}
pub fn splice_inlays(
fn splice_inlays(
&self,
to_remove: Vec<InlayId>,
to_insert: Vec<Inlay>,
@@ -3826,8 +3814,10 @@ impl Editor {
return None;
};
let entries = completions_menu.entries.borrow();
let mat = entries.get(item_ix.unwrap_or(completions_menu.selected_item))?;
let mat = completions_menu
.entries
.get(item_ix.unwrap_or(completions_menu.selected_item))?;
let mat = match mat {
CompletionEntry::InlineCompletionHint { .. } => {
self.accept_inline_completion(&AcceptInlineCompletion, cx);
@@ -3841,14 +3831,12 @@ impl Editor {
mat
}
};
let candidate_id = mat.candidate_id;
drop(entries);
let buffer_handle = completions_menu.buffer;
let completion = completions_menu
.completions
.borrow()
.get(candidate_id)?
.get(mat.candidate_id)?
.clone();
cx.stop_propagation();
@@ -3997,7 +3985,7 @@ impl Editor {
let apply_edits = provider.apply_additional_edits_for_completion(
buffer_handle,
completions_menu.completions.clone(),
candidate_id,
mat.candidate_id,
true,
cx,
);
@@ -4582,23 +4570,6 @@ impl Editor {
_: &AcceptInlineCompletion,
cx: &mut ViewContext<Self>,
) {
let buffer = self.buffer.read(cx);
let snapshot = buffer.snapshot(cx);
let selection = self.selections.newest_adjusted(cx);
let cursor = selection.head();
let current_indent = snapshot.indent_size_for_line(MultiBufferRow(cursor.row));
let suggested_indents = snapshot.suggested_indents([cursor.row], cx);
if let Some(suggested_indent) = suggested_indents.get(&MultiBufferRow(cursor.row)).copied()
{
if cursor.column < suggested_indent.len
&& cursor.column <= current_indent.len
&& current_indent.len <= suggested_indent.len
{
self.tab(&Default::default(), cx);
return;
}
}
if self.show_inline_completions_in_menu(cx) {
self.hide_context_menu(cx);
}
@@ -4911,7 +4882,7 @@ impl Editor {
}
}
pub fn inline_completion_provider(&self) -> Option<Arc<dyn InlineCompletionProviderHandle>> {
fn inline_completion_provider(&self) -> Option<Arc<dyn InlineCompletionProviderHandle>> {
Some(self.inline_completion_provider.as_ref()?.provider.clone())
}
@@ -5138,11 +5109,9 @@ impl Editor {
.borrow()
.as_ref()
.map_or(false, |menu| match menu {
CodeContextMenu::Completions(menu) => {
menu.entries.borrow().first().map_or(false, |entry| {
matches!(entry, CompletionEntry::InlineCompletionHint(_))
})
}
CodeContextMenu::Completions(menu) => menu.entries.first().map_or(false, |entry| {
matches!(entry, CompletionEntry::InlineCompletionHint(_))
}),
CodeContextMenu::CodeActions(_) => false,
})
}
@@ -5881,7 +5850,7 @@ impl Editor {
});
}
pub fn join_lines_impl(&mut self, insert_whitespace: bool, cx: &mut ViewContext<Self>) {
pub fn join_lines(&mut self, _: &JoinLines, cx: &mut ViewContext<Self>) {
if self.read_only(cx) {
return;
}
@@ -5923,12 +5892,11 @@ impl Editor {
let indent = snapshot.indent_size_for_line(next_line_row);
let start_of_next_line = Point::new(next_line_row.0, indent.len);
let replace =
if snapshot.line_len(next_line_row) > indent.len && insert_whitespace {
" "
} else {
""
};
let replace = if snapshot.line_len(next_line_row) > indent.len {
" "
} else {
""
};
this.buffer.update(cx, |buffer, cx| {
buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx)
@@ -5942,10 +5910,6 @@ impl Editor {
});
}
pub fn join_lines(&mut self, _: &JoinLines, cx: &mut ViewContext<Self>) {
self.join_lines_impl(true, cx);
}
pub fn sort_lines_case_sensitive(
&mut self,
_: &SortLinesCaseSensitive,
@@ -9144,18 +9108,15 @@ impl Editor {
};
self.buffer.update(cx, |buffer, cx| {
let snapshot = buffer.snapshot(cx);
let mut excerpt_ids = selections
.iter()
.flat_map(|selection| {
snapshot
.excerpts_for_range(selection.range())
.map(|excerpt| excerpt.id())
})
.collect::<Vec<_>>();
excerpt_ids.sort();
excerpt_ids.dedup();
buffer.expand_excerpts(excerpt_ids, lines, direction, cx)
buffer.expand_excerpts(
selections
.iter()
.map(|selection| selection.head().excerpt_id)
.dedup(),
lines,
direction,
cx,
)
})
}
@@ -9541,7 +9502,7 @@ impl Editor {
url_finder.detach();
}
pub fn open_selected_filename(&mut self, _: &OpenSelectedFilename, cx: &mut ViewContext<Self>) {
pub fn open_file(&mut self, _: &OpenFile, cx: &mut ViewContext<Self>) {
let Some(workspace) = self.workspace() else {
return;
};
@@ -10536,13 +10497,13 @@ impl Editor {
} else {
let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx);
let mut toggled_buffers = HashSet::default();
for (_, buffer_snapshot, _) in multi_buffer_snapshot.excerpts_in_ranges(
for (excerpt, _) in multi_buffer_snapshot.disjoint_ranges_to_buffer_ranges(
self.selections
.disjoint_anchors()
.into_iter()
.map(|selection| selection.range()),
) {
let buffer_id = buffer_snapshot.remote_id();
let buffer_id = excerpt.buffer().remote_id();
if toggled_buffers.insert(buffer_id) {
if self.buffer_folded(buffer_id, cx) {
self.unfold_buffer(buffer_id, cx);
@@ -10622,13 +10583,13 @@ impl Editor {
} else {
let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx);
let mut folded_buffers = HashSet::default();
for (_, buffer_snapshot, _) in multi_buffer_snapshot.excerpts_in_ranges(
for (excerpt, _) in multi_buffer_snapshot.disjoint_ranges_to_buffer_ranges(
self.selections
.disjoint_anchors()
.into_iter()
.map(|selection| selection.range()),
) {
let buffer_id = buffer_snapshot.remote_id();
let buffer_id = excerpt.buffer().remote_id();
if folded_buffers.insert(buffer_id) {
self.fold_buffer(buffer_id, cx);
}
@@ -10788,13 +10749,13 @@ impl Editor {
} else {
let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx);
let mut unfolded_buffers = HashSet::default();
for (_, buffer_snapshot, _) in multi_buffer_snapshot.excerpts_in_ranges(
for (excerpt, _) in multi_buffer_snapshot.disjoint_ranges_to_buffer_ranges(
self.selections
.disjoint_anchors()
.into_iter()
.map(|selection| selection.range()),
) {
let buffer_id = buffer_snapshot.remote_id();
let buffer_id = excerpt.buffer().remote_id();
if unfolded_buffers.insert(buffer_id) {
self.unfold_buffer(buffer_id, cx);
}
@@ -11538,36 +11499,36 @@ impl Editor {
}
fn get_permalink_to_line(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<url::Url>> {
let buffer_and_selection = maybe!({
let selection = self.selections.newest::<Point>(cx);
let buffer_and_selection_rows = maybe!({
let multi_buffer = self.buffer().read(cx);
let multi_buffer_snapshot = multi_buffer.snapshot(cx);
let selection = self.selections.newest_anchor();
let selection_range = selection.range();
let (buffer, selection) = if let Some(buffer) = self.buffer().read(cx).as_singleton() {
(buffer, selection_range.start.row..selection_range.end.row)
let (buffer, selection_rows) = if let Some(buffer) = multi_buffer.as_singleton() {
(
buffer,
selection_range.start.to_point(&multi_buffer_snapshot).row
..selection_range.end.to_point(&multi_buffer_snapshot).row,
)
} else {
let multi_buffer = self.buffer().read(cx);
let multi_buffer_snapshot = multi_buffer.snapshot(cx);
let buffer_ranges = multi_buffer_snapshot.range_to_buffer_ranges(selection_range);
let (excerpt, range) = if selection.reversed {
buffer_ranges.first()
} else {
buffer_ranges.last()
}?;
let selection_head = selection.head();
let excerpt =
multi_buffer_snapshot.excerpt_containing(selection_head..selection_head)?;
let range =
excerpt.map_range_to_buffer(selection_range.to_offset(&multi_buffer_snapshot));
let snapshot = excerpt.buffer();
let selection = text::ToPoint::to_point(&range.start, &snapshot).row
..text::ToPoint::to_point(&range.end, &snapshot).row;
(
multi_buffer.buffer(excerpt.buffer_id()).unwrap().clone(),
selection,
text::ToPoint::to_point(&range.start, &snapshot).row
..text::ToPoint::to_point(&range.end, &snapshot).row,
)
};
Some((buffer, selection))
Some((buffer, selection_rows))
});
let Some((buffer, selection)) = buffer_and_selection else {
let Some((buffer, selection_rows)) = buffer_and_selection_rows else {
return Task::ready(Err(anyhow!("failed to determine buffer and selection")));
};
@@ -11576,7 +11537,7 @@ impl Editor {
};
project.update(cx, |project, cx| {
project.get_permalink_to_line(&buffer, selection, cx)
project.get_permalink_to_line(&buffer, selection_rows, cx)
})
}

View File

@@ -5962,8 +5962,8 @@ async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) {
.with_injection_query(
r#"
(script_element
(raw_text) @injection.content
(#set! injection.language "javascript"))
(raw_text) @content
(#set! "language" "javascript"))
"#,
)
.unwrap(),
@@ -8473,7 +8473,7 @@ async fn test_completion_page_up_down_keys(cx: &mut gpui::TestAppContext) {
cx.update_editor(|editor, _| {
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
{
assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
assert_eq!(completion_menu_entries(&menu.entries), &["first", "last"]);
} else {
panic!("expected completion menu to be open");
}
@@ -8566,7 +8566,7 @@ async fn test_completion_sort(cx: &mut gpui::TestAppContext) {
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
{
assert_eq!(
completion_menu_entries(&menu),
completion_menu_entries(&menu.entries),
&["r", "ret", "Range", "return"]
);
} else {
@@ -9068,8 +9068,8 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
.with_injection_query(
r#"
(script_element
(raw_text) @injection.content
(#set! injection.language "javascript"))
(raw_text) @content
(#set! "language" "javascript"))
"#,
)
.unwrap(),
@@ -11080,7 +11080,6 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo
assert_eq!(
completions_menu
.entries
.borrow()
.iter()
.flat_map(|c| match c {
CompletionEntry::Match(mat) => Some(mat.string.clone()),
@@ -11191,7 +11190,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
{
assert_eq!(
completion_menu_entries(&menu),
completion_menu_entries(&menu.entries),
&["bg-red", "bg-blue", "bg-yellow"]
);
} else {
@@ -11204,7 +11203,10 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
cx.update_editor(|editor, _| {
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
{
assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
assert_eq!(
completion_menu_entries(&menu.entries),
&["bg-blue", "bg-yellow"]
);
} else {
panic!("expected completion menu to be open");
}
@@ -11218,19 +11220,18 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
cx.update_editor(|editor, _| {
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
{
assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
assert_eq!(completion_menu_entries(&menu.entries), &["bg-yellow"]);
} else {
panic!("expected completion menu to be open");
}
});
}
fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
let entries = menu.entries.borrow();
fn completion_menu_entries(entries: &[CompletionEntry]) -> Vec<&str> {
entries
.iter()
.flat_map(|e| match e {
CompletionEntry::Match(mat) => Some(mat.string.clone()),
CompletionEntry::Match(mat) => Some(mat.string.as_str()),
_ => None,
})
.collect()

View File

@@ -342,7 +342,7 @@ impl EditorElement {
.detach_and_log_err(cx);
});
register_action(view, cx, Editor::open_url);
register_action(view, cx, Editor::open_selected_filename);
register_action(view, cx, Editor::open_file);
register_action(view, cx, Editor::fold);
register_action(view, cx, Editor::fold_at_level);
register_action(view, cx, Editor::fold_all);
@@ -3958,13 +3958,7 @@ impl EditorElement {
let Some(()) = line.paint(hitbox.origin, line_height, cx).log_err() else {
continue;
};
// In singleton buffers, we select corresponding lines on the line number click, so use | -like cursor.
// In multi buffers, we open file at the line number clicked, so use a pointing hand cursor.
if is_singleton {
cx.set_cursor_style(CursorStyle::IBeam, hitbox);
} else {
cx.set_cursor_style(CursorStyle::PointingHand, hitbox);
}
cx.set_cursor_style(CursorStyle::PointingHand, hitbox);
}
}
@@ -5584,21 +5578,21 @@ impl LineWithInvisibles {
});
}
} else {
invisibles.extend(line_chunk.char_indices().filter_map(
|(index, c)| {
let is_whitespace = c.is_whitespace();
non_whitespace_added |= !is_whitespace;
if is_whitespace
&& (non_whitespace_added || !is_soft_wrapped)
{
Some(Invisible::Whitespace {
line_offset: line.len() + index,
})
} else {
None
}
},
))
invisibles.extend(
line_chunk
.bytes()
.enumerate()
.filter(|(_, line_byte)| {
let is_whitespace =
(*line_byte as char).is_whitespace();
non_whitespace_added |= !is_whitespace;
is_whitespace
&& (non_whitespace_added || !is_soft_wrapped)
})
.map(|(whitespace_index, _)| Invisible::Whitespace {
line_offset: line.len() + whitespace_index,
}),
)
}
}

View File

@@ -11,11 +11,11 @@ use gpui::{
StyleRefinement, Styled, Task, TextStyleRefinement, View, ViewContext,
};
use itertools::Itertools;
use language::{DiagnosticEntry, Language, LanguageRegistry};
use language::{Diagnostic, DiagnosticEntry, Language, LanguageRegistry};
use lsp::DiagnosticSeverity;
use markdown::{Markdown, MarkdownStyle};
use multi_buffer::ToOffset;
use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart};
use project::{HoverBlock, InlayHintLabelPart};
use settings::Settings;
use std::rc::Rc;
use std::{borrow::Cow, cell::RefCell};
@@ -263,14 +263,50 @@ fn show_hover(
delay.await;
}
let local_diagnostic = snapshot
let local_diagnostic = if let Some(invisible) = snapshot
.buffer_snapshot
.diagnostics_in_range(anchor..anchor, false)
// Find the entry with the most specific range
.min_by_key(|entry| {
let range = entry.range.to_offset(&snapshot.buffer_snapshot);
range.end - range.start
});
.chars_at(anchor)
.next()
.filter(|&c| is_invisible(c))
{
let after = snapshot.buffer_snapshot.anchor_after(
anchor.to_offset(&snapshot.buffer_snapshot) + invisible.len_utf8(),
);
Some(DiagnosticEntry {
diagnostic: Diagnostic {
severity: DiagnosticSeverity::HINT,
message: format!("Unicode character U+{:02X}", invisible as u32),
..Default::default()
},
range: anchor..after,
})
} else if let Some(invisible) = snapshot
.buffer_snapshot
.reversed_chars_at(anchor)
.next()
.filter(|&c| is_invisible(c))
{
let before = snapshot.buffer_snapshot.anchor_before(
anchor.to_offset(&snapshot.buffer_snapshot) - invisible.len_utf8(),
);
Some(DiagnosticEntry {
diagnostic: Diagnostic {
severity: DiagnosticSeverity::HINT,
message: format!("Unicode character U+{:02X}", invisible as u32),
..Default::default()
},
range: before..anchor,
})
} else {
snapshot
.buffer_snapshot
.diagnostics_in_range(anchor..anchor, false)
// Find the entry with the most specific range
.min_by_key(|entry| {
let range = entry.range.to_offset(&snapshot.buffer_snapshot);
range.end - range.start
})
};
let diagnostic_popover = if let Some(local_diagnostic) = local_diagnostic {
let text = match local_diagnostic.diagnostic.source {
@@ -353,31 +389,6 @@ fn show_hover(
this.hover_state.diagnostic_popover = diagnostic_popover;
})?;
let invisible_char = if let Some(invisible) = snapshot
.buffer_snapshot
.chars_at(anchor)
.next()
.filter(|&c| is_invisible(c))
{
let after = snapshot.buffer_snapshot.anchor_after(
anchor.to_offset(&snapshot.buffer_snapshot) + invisible.len_utf8(),
);
Some((invisible, anchor..after))
} else if let Some(invisible) = snapshot
.buffer_snapshot
.reversed_chars_at(anchor)
.next()
.filter(|&c| is_invisible(c))
{
let before = snapshot.buffer_snapshot.anchor_before(
anchor.to_offset(&snapshot.buffer_snapshot) - invisible.len_utf8(),
);
Some((invisible, before..anchor))
} else {
None
};
let hovers_response = if let Some(hover_request) = hover_request {
hover_request.await
} else {
@@ -385,26 +396,8 @@ fn show_hover(
};
let snapshot = this.update(&mut cx, |this, cx| this.snapshot(cx))?;
let mut hover_highlights = Vec::with_capacity(hovers_response.len());
let mut info_popovers = Vec::with_capacity(
hovers_response.len() + if invisible_char.is_some() { 1 } else { 0 },
);
if let Some((invisible, range)) = invisible_char {
let blocks = vec![HoverBlock {
text: format!("Unicode character U+{:02X}", invisible as u32),
kind: HoverBlockKind::PlainText,
}];
let parsed_content = parse_blocks(&blocks, &language_registry, None, &mut cx).await;
let scroll_handle = ScrollHandle::new();
info_popovers.push(InfoPopover {
symbol_range: RangeInEditor::Text(range),
parsed_content,
scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
scroll_handle,
keyboard_grace: Rc::new(RefCell::new(ignore_timeout)),
anchor: Some(anchor),
})
}
let mut info_popovers = Vec::with_capacity(hovers_response.len());
let mut info_popover_tasks = Vec::with_capacity(hovers_response.len());
for hover_result in hovers_response {
// Create symbol range of anchors for highlighting and filtering of future requests.
@@ -417,6 +410,7 @@ fn show_hover(
let end = snapshot
.buffer_snapshot
.anchor_in_excerpt(excerpt_id, range.end)?;
Some(start..end)
})
.or_else(|| {
@@ -434,15 +428,21 @@ fn show_hover(
let parsed_content =
parse_blocks(&blocks, &language_registry, language, &mut cx).await;
let scroll_handle = ScrollHandle::new();
hover_highlights.push(range.clone());
info_popovers.push(InfoPopover {
symbol_range: RangeInEditor::Text(range),
parsed_content,
scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
scroll_handle,
keyboard_grace: Rc::new(RefCell::new(ignore_timeout)),
anchor: Some(anchor),
});
info_popover_tasks.push((
range.clone(),
InfoPopover {
symbol_range: RangeInEditor::Text(range),
parsed_content,
scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
scroll_handle,
keyboard_grace: Rc::new(RefCell::new(ignore_timeout)),
anchor: Some(anchor),
},
));
}
for (highlight_range, info_popover) in info_popover_tasks {
hover_highlights.push(highlight_range);
info_popovers.push(info_popover);
}
this.update(&mut cx, |editor, cx| {
@@ -732,7 +732,6 @@ impl InfoPopover {
cx.notify();
self.scroll_handle.set_offset(current);
}
fn render_vertical_scrollbar(&self, cx: &mut ViewContext<Editor>) -> Stateful<Div> {
div()
.occlude()

View File

@@ -458,10 +458,7 @@ impl Editor {
) -> Option<()> {
let multi_buffer = self.buffer.read(cx);
let multi_buffer_snapshot = multi_buffer.snapshot(cx);
let (excerpt, range) = multi_buffer_snapshot
.range_to_buffer_ranges(range)
.into_iter()
.next()?;
let (excerpt, range) = multi_buffer_snapshot.range_to_buffer_ranges(range).next()?;
multi_buffer
.buffer(excerpt.buffer_id())

View File

@@ -1,9 +1,8 @@
use gpui::{prelude::*, Model};
use indoc::indoc;
use inline_completion::InlineCompletionProvider;
use language::{Language, LanguageConfig};
use multi_buffer::{Anchor, MultiBufferSnapshot, ToPoint};
use std::{num::NonZeroU32, ops::Range, sync::Arc};
use std::ops::Range;
use text::{Point, ToOffset};
use crate::{
@@ -123,54 +122,6 @@ async fn test_inline_completion_jump_button(cx: &mut gpui::TestAppContext) {
"});
}
#[gpui::test]
async fn test_indentation(cx: &mut gpui::TestAppContext) {
init_test(cx, |settings| {
settings.defaults.tab_size = NonZeroU32::new(4)
});
let language = Arc::new(
Language::new(
LanguageConfig::default(),
Some(tree_sitter_rust::LANGUAGE.into()),
)
.with_indents_query(r#"(_ "(" ")" @end) @indent"#)
.unwrap(),
);
let mut cx = EditorTestContext::new(cx).await;
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
let provider = cx.new_model(|_| FakeInlineCompletionProvider::default());
assign_editor_completion_provider(provider.clone(), &mut cx);
cx.set_state(indoc! {"
const a: A = (
ˇ
);
"});
propose_edits(
&provider,
vec![(Point::new(1, 0)..Point::new(1, 0), " const function()")],
&mut cx,
);
cx.update_editor(|editor, cx| editor.update_visible_inline_completion(cx));
assert_editor_active_edit_completion(&mut cx, |_, edits| {
assert_eq!(edits.len(), 1);
assert_eq!(edits[0].1.as_str(), " const function()");
});
// When the cursor is before the suggested indentation level, accepting a
// completion should just indent.
accept_completion(&mut cx);
cx.assert_editor_state(indoc! {"
const a: A = (
ˇ
);
"});
}
#[gpui::test]
async fn test_inline_completion_invalidation_range(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
@@ -374,10 +325,6 @@ impl InlineCompletionProvider for FakeInlineCompletionProvider {
false
}
fn show_completions_in_normal_mode() -> bool {
false
}
fn is_enabled(
&self,
_buffer: &gpui::Model<language::Buffer>,
@@ -387,10 +334,6 @@ impl InlineCompletionProvider for FakeInlineCompletionProvider {
true
}
fn is_refreshing(&self) -> bool {
false
}
fn refresh(
&mut self,
_buffer: gpui::Model<language::Buffer>,

View File

@@ -1468,10 +1468,11 @@ impl SearchableItem for Editor {
search_within_ranges
};
for (excerpt_id, search_buffer, search_range) in
buffer.excerpts_in_ranges(search_within_ranges)
for (excerpt, search_range) in
buffer.disjoint_ranges_to_buffer_ranges(search_within_ranges)
{
if !search_range.is_empty() {
let search_buffer = excerpt.buffer();
ranges.extend(
query
.search(search_buffer, Some(search_range.clone()))
@@ -1482,8 +1483,8 @@ impl SearchableItem for Editor {
.anchor_after(search_range.start + match_range.start);
let end = search_buffer
.anchor_before(search_range.start + match_range.end);
buffer.anchor_in_excerpt(excerpt_id, start).unwrap()
..buffer.anchor_in_excerpt(excerpt_id, end).unwrap()
buffer.anchor_in_excerpt(excerpt.id(), start).unwrap()
..buffer.anchor_in_excerpt(excerpt.id(), end).unwrap()
}),
);
}

View File

@@ -18,19 +18,28 @@ use wasm_encoder::{ComponentSectionId, Encode as _, RawSection, Section as _};
use wasmparser::Parser;
use wit_component::ComponentEncoder;
const RUST_TARGET: &str = "wasm32-wasip2";
/// Currently, we compile with Rust's `wasm32-wasip1` target, which works with WASI `preview1`.
/// But the WASM component model is based on WASI `preview2`. So we need an 'adapter' WASM
/// module, which implements the `preview1` interface in terms of `preview2`.
///
/// Once Rust 1.78 is released, there will be a `wasm32-wasip2` target available, so we will
/// not need the adapter anymore.
const RUST_TARGET: &str = "wasm32-wasip1";
const WASI_ADAPTER_URL: &str =
"https://github.com/bytecodealliance/wasmtime/releases/download/v18.0.2/wasi_snapshot_preview1.reactor.wasm";
/// Compiling Tree-sitter parsers from C to WASM requires Clang 17, and a WASM build of libc
/// and clang's runtime library. The `wasi-sdk` provides these binaries.
///
/// Once Clang 17 and its wasm target are available via system package managers, we won't need
/// to download this.
const WASI_SDK_URL: &str = "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-24/";
const WASI_SDK_URL: &str = "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-21/";
const WASI_SDK_ASSET_NAME: Option<&str> = if cfg!(target_os = "macos") {
Some("wasi-sdk-24.0-macos.tar.gz")
Some("wasi-sdk-21.0-macos.tar.gz")
} else if cfg!(any(target_os = "linux", target_os = "freebsd")) {
Some("wasi-sdk-24.0-linux.tar.gz")
Some("wasi-sdk-21.0-linux.tar.gz")
} else if cfg!(target_os = "windows") {
Some("wasi-sdk-24.0.m-mingw.tar.gz")
Some("wasi-sdk-21.0.m-mingw.tar.gz")
} else {
None
};
@@ -112,6 +121,8 @@ impl ExtensionBuilder {
options: CompileExtensionOptions,
) -> Result<(), anyhow::Error> {
self.install_rust_wasm_target_if_needed()?;
let adapter_bytes = self.install_wasi_preview1_adapter_if_needed().await?;
let cargo_toml_content = fs::read_to_string(extension_dir.join("Cargo.toml"))?;
let cargo_toml: CargoToml = toml::from_str(&cargo_toml_content)?;
@@ -119,7 +130,6 @@ impl ExtensionBuilder {
"compiling Rust crate for extension {}",
extension_dir.display()
);
let output = util::command::new_std_command("cargo")
.args(["build", "--target", RUST_TARGET])
.args(options.release.then_some("--release"))
@@ -158,12 +168,20 @@ impl ExtensionBuilder {
let wasm_bytes = fs::read(&wasm_path)
.with_context(|| format!("failed to read output module `{}`", wasm_path.display()))?;
let encoder = ComponentEncoder::default()
.module(&wasm_bytes)?
.adapter("wasi_snapshot_preview1", &adapter_bytes)
.context("failed to load adapter module")?
.validate(true);
log::info!(
"encoding wasm component for extension {}",
extension_dir.display()
);
let component_bytes = wasm_bytes;
let component_bytes = encoder
.encode()
.context("failed to encode wasm component")?;
let component_bytes = self
.strip_custom_sections(&component_bytes)
@@ -361,6 +379,38 @@ impl ExtensionBuilder {
Ok(())
}
async fn install_wasi_preview1_adapter_if_needed(&self) -> Result<Vec<u8>> {
let cache_path = self.cache_dir.join("wasi_snapshot_preview1.reactor.wasm");
if let Ok(content) = fs::read(&cache_path) {
if Parser::is_core_wasm(&content) {
return Ok(content);
}
}
fs::remove_file(&cache_path).ok();
log::info!(
"downloading wasi adapter module to {}",
cache_path.display()
);
let mut response = self
.http
.get(WASI_ADAPTER_URL, AsyncBody::default(), true)
.await?;
let mut content = Vec::new();
let mut body = BufReader::new(response.body_mut());
body.read_to_end(&mut content).await?;
fs::write(&cache_path, &content)
.with_context(|| format!("failed to save file {}", cache_path.display()))?;
if !Parser::is_core_wasm(&content) {
bail!("downloaded wasi adapter is invalid");
}
Ok(content)
}
async fn install_wasi_sdk_if_needed(&self) -> Result<PathBuf> {
let url = if let Some(asset_name) = WASI_SDK_ASSET_NAME {
format!("{WASI_SDK_URL}/{asset_name}")

View File

@@ -175,7 +175,7 @@ impl ExtensionManifest {
.await
.with_context(|| format!("failed to load {extension_name} extension.toml"))?;
toml::from_str(&manifest_content)
.with_context(|| format!("invalid extension.toml for extension {extension_name}"))
.with_context(|| format!("invalid extension.json for extension {extension_name}"))
}
}
}

View File

@@ -84,7 +84,7 @@ impl HostWorktree for WasmState {
latest::HostWorktree::which(self, delegate, binary_name).await
}
async fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
Ok(())
}
}

View File

@@ -92,7 +92,7 @@ impl HostWorktree for WasmState {
latest::HostWorktree::which(self, delegate, binary_name).await
}
async fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
// We only ever hand out borrows of worktrees.
Ok(())
}

View File

@@ -147,7 +147,7 @@ impl HostWorktree for WasmState {
latest::HostWorktree::which(self, delegate, binary_name).await
}
async fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
// We only ever hand out borrows of worktrees.
Ok(())
}

View File

@@ -240,7 +240,7 @@ impl HostKeyValueStore for WasmState {
kv_store.insert(key, value).await.to_wasmtime_result()
}
async fn drop(&mut self, _worktree: Resource<ExtensionKeyValueStore>) -> Result<()> {
fn drop(&mut self, _worktree: Resource<ExtensionKeyValueStore>) -> Result<()> {
// We only ever hand out borrows of key-value stores.
Ok(())
}
@@ -282,7 +282,7 @@ impl HostWorktree for WasmState {
latest::HostWorktree::which(self, delegate, binary_name).await
}
async fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
// We only ever hand out borrows of worktrees.
Ok(())
}
@@ -350,7 +350,7 @@ impl http_client::HostHttpResponseStream for WasmState {
.to_wasmtime_result()
}
async fn drop(&mut self, _resource: Resource<ExtensionHttpResponseStream>) -> Result<()> {
fn drop(&mut self, _resource: Resource<ExtensionHttpResponseStream>) -> Result<()> {
Ok(())
}
}

View File

@@ -259,7 +259,7 @@ impl HostKeyValueStore for WasmState {
kv_store.insert(key, value).await.to_wasmtime_result()
}
async fn drop(&mut self, _worktree: Resource<ExtensionKeyValueStore>) -> Result<()> {
fn drop(&mut self, _worktree: Resource<ExtensionKeyValueStore>) -> Result<()> {
// We only ever hand out borrows of key-value stores.
Ok(())
}
@@ -275,7 +275,7 @@ impl HostProject for WasmState {
Ok(project.worktree_ids())
}
async fn drop(&mut self, _project: Resource<Project>) -> Result<()> {
fn drop(&mut self, _project: Resource<Project>) -> Result<()> {
// We only ever hand out borrows of projects.
Ok(())
}
@@ -325,7 +325,7 @@ impl HostWorktree for WasmState {
Ok(delegate.which(binary_name).await)
}
async fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
// We only ever hand out borrows of worktrees.
Ok(())
}
@@ -393,7 +393,7 @@ impl http_client::HostHttpResponseStream for WasmState {
.to_wasmtime_result()
}
async fn drop(&mut self, _resource: Resource<ExtensionHttpResponseStream>) -> Result<()> {
fn drop(&mut self, _resource: Resource<ExtensionHttpResponseStream>) -> Result<()> {
Ok(())
}
}

View File

@@ -175,7 +175,7 @@ pub(crate) fn suggest(buffer: Model<Buffer>, cx: &mut ViewContext<Workspace>) {
"Do you want to install the recommended '{}' extension for '{}' files?",
extension_id, file_name_or_extension
))
.with_click_message("Yes, install extension")
.with_click_message("Yes")
.on_click({
let extension_id = extension_id.clone();
move |cx| {
@@ -186,7 +186,7 @@ pub(crate) fn suggest(buffer: Model<Buffer>, cx: &mut ViewContext<Workspace>) {
});
}
})
.with_secondary_click_message("No, don't install it")
.with_secondary_click_message("No")
.on_secondary_click(move |cx| {
let key = language_extension_key(&extension_id);
db::write_and_log(cx, move || {

View File

@@ -1,8 +1,8 @@
#[cfg(target_os = "macos")]
mod mac_watcher;
#[cfg(not(target_os = "macos"))]
pub mod fs_watcher;
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
pub mod linux_watcher;
use anyhow::{anyhow, Result};
use git::GitHostingProviderRegistry;
@@ -11,7 +11,8 @@ use git::GitHostingProviderRegistry;
use ashpd::desktop::trash;
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
use smol::process::Command;
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
use std::fs::File;
#[cfg(unix)]
use std::os::fd::AsFd;
#[cfg(unix)]
@@ -444,13 +445,7 @@ impl Fs for RealFs {
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
async fn trash_file(&self, path: &Path, _options: RemoveOptions) -> Result<()> {
if let Ok(Some(metadata)) = self.metadata(path).await {
if metadata.is_symlink {
// TODO: trash_file does not support trashing symlinks yet - https://github.com/bilelmoussaoui/ashpd/issues/255
return self.remove_file(path, RemoveOptions::default()).await;
}
}
let file = smol::fs::File::open(path).await?;
let file = File::open(path)?;
match trash::trash_file(&file.as_fd()).await {
Ok(_) => Ok(()),
Err(err) => Err(anyhow::Error::new(err)),
@@ -700,7 +695,7 @@ impl Fs for RealFs {
)
}
#[cfg(not(target_os = "macos"))]
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
async fn watch(
&self,
path: &Path,
@@ -710,11 +705,10 @@ impl Fs for RealFs {
Arc<dyn Watcher>,
) {
use parking_lot::Mutex;
use util::paths::SanitizedPath;
let (tx, rx) = smol::channel::unbounded();
let pending_paths: Arc<Mutex<Vec<PathEvent>>> = Default::default();
let watcher = Arc::new(fs_watcher::FsWatcher::new(tx, pending_paths.clone()));
let watcher = Arc::new(linux_watcher::LinuxWatcher::new(tx, pending_paths.clone()));
if watcher.add(path).is_err() {
// If the path doesn't exist yet (e.g. settings.json), watch the parent dir to learn when it's created.
@@ -732,7 +726,7 @@ impl Fs for RealFs {
if let Some(parent) = path.parent() {
target = parent.join(target);
if let Ok(canonical) = self.canonicalize(&target).await {
target = SanitizedPath::from(canonical).as_path().to_path_buf();
target = canonical;
}
}
}
@@ -759,6 +753,56 @@ impl Fs for RealFs {
)
}
#[cfg(target_os = "windows")]
async fn watch(
&self,
path: &Path,
_latency: Duration,
) -> (
Pin<Box<dyn Send + Stream<Item = Vec<PathEvent>>>>,
Arc<dyn Watcher>,
) {
use notify::{EventKind, Watcher};
let (tx, rx) = smol::channel::unbounded();
let mut file_watcher = notify::recommended_watcher({
let tx = tx.clone();
move |event: Result<notify::Event, _>| {
if let Some(event) = event.log_err() {
let kind = match event.kind {
EventKind::Create(_) => Some(PathEventKind::Created),
EventKind::Modify(_) => Some(PathEventKind::Changed),
EventKind::Remove(_) => Some(PathEventKind::Removed),
_ => None,
};
tx.try_send(
event
.paths
.into_iter()
.map(|path| PathEvent { path, kind })
.collect::<Vec<_>>(),
)
.ok();
}
}
})
.expect("Could not start file watcher");
file_watcher
.watch(path, notify::RecursiveMode::Recursive)
.log_err();
(
Box::pin(rx.chain(futures::stream::once(async move {
drop(file_watcher);
vec![]
}))),
Arc::new(RealWatcher {}),
)
}
fn open_repo(&self, dotgit_path: &Path) -> Option<Arc<dyn GitRepository>> {
// with libgit2, we can open git repo from an existing work dir
// https://libgit2.org/docs/reference/main/repository/git_repository_open.html

View File

@@ -5,12 +5,12 @@ use util::ResultExt;
use crate::{PathEvent, PathEventKind, Watcher};
pub struct FsWatcher {
pub struct LinuxWatcher {
tx: smol::channel::Sender<()>,
pending_path_events: Arc<Mutex<Vec<PathEvent>>>,
}
impl FsWatcher {
impl LinuxWatcher {
pub fn new(
tx: smol::channel::Sender<()>,
pending_path_events: Arc<Mutex<Vec<PathEvent>>>,
@@ -22,7 +22,7 @@ impl FsWatcher {
}
}
impl Watcher for FsWatcher {
impl Watcher for LinuxWatcher {
fn add(&self, path: &std::path::Path) -> gpui::Result<()> {
let root_path = path.to_path_buf();
@@ -69,7 +69,7 @@ impl Watcher for FsWatcher {
})?;
global(|g| {
g.watcher
g.inotify
.lock()
.watch(path, notify::RecursiveMode::NonRecursive)
})??;
@@ -79,18 +79,16 @@ impl Watcher for FsWatcher {
fn remove(&self, path: &std::path::Path) -> gpui::Result<()> {
use notify::Watcher;
Ok(global(|w| w.watcher.lock().unwatch(path))??)
Ok(global(|w| w.inotify.lock().unwatch(path))??)
}
}
pub struct GlobalWatcher {
// two mutexes because calling watcher.add triggers an watcher.event, which needs watchers.
// two mutexes because calling inotify.add triggers an inotify.event, which needs watchers.
#[cfg(target_os = "linux")]
pub(super) watcher: Mutex<notify::INotifyWatcher>,
pub(super) inotify: Mutex<notify::INotifyWatcher>,
#[cfg(target_os = "freebsd")]
pub(super) watcher: Mutex<notify::KqueueWatcher>,
#[cfg(target_os = "windows")]
pub(super) watcher: Mutex<notify::ReadDirectoryChangesWatcher>,
pub(super) inotify: Mutex<notify::KqueueWatcher>,
pub(super) watchers: Mutex<Vec<Box<dyn Fn(&notify::Event) + Send + Sync>>>,
}
@@ -100,8 +98,7 @@ impl GlobalWatcher {
}
}
static FS_WATCHER_INSTANCE: OnceLock<anyhow::Result<GlobalWatcher, notify::Error>> =
OnceLock::new();
static INOTIFY_INSTANCE: OnceLock<anyhow::Result<GlobalWatcher, notify::Error>> = OnceLock::new();
fn handle_event(event: Result<notify::Event, notify::Error>) {
let Some(event) = event.log_err() else { return };
@@ -114,9 +111,9 @@ fn handle_event(event: Result<notify::Event, notify::Error>) {
}
pub fn global<T>(f: impl FnOnce(&GlobalWatcher) -> T) -> anyhow::Result<T> {
let result = FS_WATCHER_INSTANCE.get_or_init(|| {
let result = INOTIFY_INSTANCE.get_or_init(|| {
notify::recommended_watcher(handle_event).map(|file_watcher| GlobalWatcher {
watcher: Mutex::new(file_watcher),
inotify: Mutex::new(file_watcher),
watchers: Default::default(),
})
});

View File

@@ -17,6 +17,7 @@ anyhow.workspace = true
collections.workspace = true
db.workspace = true
editor.workspace = true
futures.workspace = true
git.workspace = true
gpui.workspace = true
language.workspace = true
@@ -27,7 +28,6 @@ serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
settings.workspace = true
theme.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
use ::settings::Settings;
use git::repository::GitFileStatus;
use gpui::{actions, AppContext, Context, Global, Hsla, Model};
use gpui::{actions, AppContext, Hsla};
use settings::GitPanelSettings;
use ui::{Color, Icon, IconName, IntoElement, SharedString};
use ui::{Color, Icon, IconName, IntoElement};
pub mod git_panel;
mod settings;
@@ -12,45 +12,14 @@ actions!(
[
StageAll,
UnstageAll,
RevertAll,
DiscardAll,
CommitStagedChanges,
CommitAllChanges,
ClearMessage
CommitAllChanges
]
);
pub fn init(cx: &mut AppContext) {
GitPanelSettings::register(cx);
let git_state = cx.new_model(|_cx| GitState::new());
cx.set_global(GlobalGitState(git_state));
}
struct GlobalGitState(Model<GitState>);
impl Global for GlobalGitState {}
pub struct GitState {
commit_message: Option<SharedString>,
}
impl GitState {
pub fn new() -> Self {
GitState {
commit_message: None,
}
}
pub fn set_message(&mut self, message: Option<SharedString>) {
self.commit_message = message;
}
pub fn clear_message(&mut self) {
self.commit_message = None;
}
pub fn get_global(cx: &mut AppContext) -> Model<GitState> {
cx.global::<GlobalGitState>().0.clone()
}
}
const ADDED_COLOR: Hsla = Hsla {
@@ -82,8 +51,6 @@ pub fn git_status_icon(status: GitFileStatus) -> impl IntoElement {
Icon::new(IconName::SquareDot).color(Color::Custom(MODIFIED_COLOR))
}
GitFileStatus::Conflict => Icon::new(IconName::Warning).color(Color::Custom(REMOVED_COLOR)),
GitFileStatus::Deleted => {
Icon::new(IconName::SquareMinus).color(Color::Custom(REMOVED_COLOR))
}
GitFileStatus::Deleted => Icon::new(IconName::Warning).color(Color::Custom(REMOVED_COLOR)),
}
}

View File

@@ -1,6 +1,6 @@
mod supported_countries;
use anyhow::{anyhow, bail, Result};
use anyhow::{anyhow, Result};
use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt};
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
use serde::{Deserialize, Serialize};
@@ -15,20 +15,6 @@ pub async fn stream_generate_content(
api_key: &str,
mut request: GenerateContentRequest,
) -> Result<BoxStream<'static, Result<GenerateContentResponse>>> {
if request.contents.is_empty() {
bail!("Request must contain at least one content item");
}
if let Some(user_content) = request
.contents
.iter()
.find(|content| content.role == Role::User)
{
if user_content.parts.is_empty() {
bail!("User content must contain at least one part");
}
}
let uri = format!(
"{api_url}/v1beta/models/{model}:streamGenerateContent?alt=sse&key={api_key}",
model = request.model
@@ -154,7 +140,7 @@ pub struct Content {
pub role: Role,
}
#[derive(Debug, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum Role {
User,
@@ -305,8 +291,6 @@ pub enum Model {
Gemini15Pro,
#[serde(rename = "gemini-1.5-flash")]
Gemini15Flash,
#[serde(rename = "gemini-2.0-flash-exp")]
Gemini20Flash,
#[serde(rename = "custom")]
Custom {
name: String,
@@ -321,7 +305,6 @@ impl Model {
match self {
Model::Gemini15Pro => "gemini-1.5-pro",
Model::Gemini15Flash => "gemini-1.5-flash",
Model::Gemini20Flash => "gemini-2.0-flash-exp",
Model::Custom { name, .. } => name,
}
}
@@ -330,7 +313,6 @@ impl Model {
match self {
Model::Gemini15Pro => "Gemini 1.5 Pro",
Model::Gemini15Flash => "Gemini 1.5 Flash",
Model::Gemini20Flash => "Gemini 2.0 Flash",
Self::Custom {
name, display_name, ..
} => display_name.as_ref().unwrap_or(name),
@@ -341,7 +323,6 @@ impl Model {
match self {
Model::Gemini15Pro => 2_000_000,
Model::Gemini15Flash => 1_000_000,
Model::Gemini20Flash => 1_000_000,
Model::Custom { max_tokens, .. } => *max_tokens,
}
}

View File

@@ -148,7 +148,7 @@ pathfinder_geometry = "0.5"
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies]
# Always used
flume = "0.11"
oo7 = { git = "https://github.com/zed-industries/oo7", branch = "avoid-crypto-panic" }
oo7 = "0.3.0"
# Used in both windowing options
ashpd = { workspace = true, optional = true }

View File

@@ -62,14 +62,6 @@ pub trait Action: 'static + Send {
fn build(value: serde_json::Value) -> Result<Box<dyn Action>>
where
Self: Sized;
/// A list of alternate, deprecated names for this action.
fn deprecated_aliases() -> &'static [&'static str]
where
Self: Sized,
{
&[]
}
}
impl std::fmt::Debug for dyn Action {
@@ -93,7 +85,6 @@ pub(crate) struct ActionRegistry {
builders_by_name: HashMap<SharedString, ActionBuilder>,
names_by_type_id: HashMap<TypeId, SharedString>,
all_names: Vec<SharedString>, // So we can return a static slice.
deprecations: Vec<(SharedString, SharedString)>,
}
impl Default for ActionRegistry {
@@ -102,7 +93,6 @@ impl Default for ActionRegistry {
builders_by_name: Default::default(),
names_by_type_id: Default::default(),
all_names: Default::default(),
deprecations: Default::default(),
};
this.load_actions();
@@ -121,7 +111,6 @@ pub type MacroActionBuilder = fn() -> ActionData;
#[doc(hidden)]
pub struct ActionData {
pub name: &'static str,
pub aliases: &'static [&'static str],
pub type_id: TypeId,
pub build: ActionBuilder,
}
@@ -145,7 +134,6 @@ impl ActionRegistry {
pub(crate) fn load_action<A: Action>(&mut self) {
self.insert_action(ActionData {
name: A::debug_name(),
aliases: A::deprecated_aliases(),
type_id: TypeId::of::<A>(),
build: A::build,
});
@@ -154,10 +142,6 @@ impl ActionRegistry {
fn insert_action(&mut self, action: ActionData) {
let name: SharedString = action.name.into();
self.builders_by_name.insert(name.clone(), action.build);
for &alias in action.aliases {
self.builders_by_name.insert(alias.into(), action.build);
self.deprecations.push((alias.into(), name.clone()));
}
self.names_by_type_id.insert(action.type_id, name.clone());
self.all_names.push(name);
}
@@ -190,10 +174,6 @@ impl ActionRegistry {
pub fn all_action_names(&self) -> &[SharedString] {
self.all_names.as_slice()
}
pub fn action_deprecations(&self) -> &[(SharedString, SharedString)] {
self.deprecations.as_slice()
}
}
/// Defines unit structs that can be used as actions.
@@ -226,7 +206,7 @@ macro_rules! actions {
/// `impl_action_as!`
#[macro_export]
macro_rules! action_as {
($namespace:path, $name:ident as $visual_name:ident) => {
($namespace:path, $name:ident as $visual_name:tt) => {
#[doc = "The `"]
#[doc = stringify!($name)]
#[doc = "` action, see [`gpui::actions!`]"]
@@ -248,43 +228,6 @@ macro_rules! action_as {
};
}
/// Defines a unit struct that can be used as an action, with some deprecated aliases.
#[macro_export]
macro_rules! action_aliases {
($namespace:path, $name:ident, [$($alias:ident),* $(,)?]) => {
#[doc = "The `"]
#[doc = stringify!($name)]
#[doc = "` action, see [`gpui::actions!`]"]
#[derive(
::std::cmp::PartialEq,
::std::clone::Clone,
::std::default::Default,
::std::fmt::Debug,
gpui::private::serde_derive::Deserialize,
)]
#[serde(crate = "gpui::private::serde")]
pub struct $name;
gpui::__impl_action!(
$namespace,
$name,
$name,
fn build(
_: gpui::private::serde_json::Value,
) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
Ok(Box::new(Self))
},
fn deprecated_aliases() -> &'static [&'static str] {
&[
$(concat!(stringify!($namespace), "::", stringify!($alias))),*
]
}
);
gpui::register_action!($name);
};
}
/// Implements the Action trait for any struct that implements Clone, Default, PartialEq, and serde_deserialize::Deserialize
#[macro_export]
macro_rules! impl_actions {
@@ -326,7 +269,7 @@ macro_rules! impl_action_as {
#[doc(hidden)]
#[macro_export]
macro_rules! __impl_action {
($namespace:path, $name:ident, $visual_name:tt, $($items:item),*) => {
($namespace:path, $name:ident, $visual_name:tt, $build:item) => {
impl gpui::Action for $name {
fn name(&self) -> &'static str
{
@@ -348,6 +291,8 @@ macro_rules! __impl_action {
)
}
$build
fn partial_eq(&self, action: &dyn gpui::Action) -> bool {
action
.as_any()
@@ -362,8 +307,6 @@ macro_rules! __impl_action {
fn as_any(&self) -> &dyn ::std::any::Any {
self
}
$($items)*
}
};
}

View File

@@ -1226,11 +1226,6 @@ impl AppContext {
self.actions.all_action_names()
}
/// Get a list of all deprecated action aliases and their canonical names.
pub fn action_deprecations(&self) -> &[(SharedString, SharedString)] {
self.actions.action_deprecations()
}
/// Register a callback to be invoked when the application is about to quit.
/// It is not possible to cancel the quit event at this point.
pub fn on_app_quit<Fut>(
@@ -1418,11 +1413,6 @@ impl AppContext {
pub fn get_name(&self) -> &'static str {
self.name.as_ref().unwrap()
}
/// Returns `true` if the platform file picker supports selecting a mix of files and directories.
pub fn can_select_mixed_files_and_dirs(&self) -> bool {
self.platform.can_select_mixed_files_and_dirs()
}
}
impl Context for AppContext {

View File

@@ -19,7 +19,6 @@ pub struct Anchored {
fit_mode: AnchoredFitMode,
anchor_position: Option<Point<Pixels>>,
position_mode: AnchoredPositionMode,
offset: Option<Point<Pixels>>,
}
/// anchored gives you an element that will avoid overflowing the window bounds.
@@ -31,7 +30,6 @@ pub fn anchored() -> Anchored {
fit_mode: AnchoredFitMode::SwitchAnchor,
anchor_position: None,
position_mode: AnchoredPositionMode::Window,
offset: None,
}
}
@@ -49,13 +47,6 @@ impl Anchored {
self
}
/// Offset the final position by this amount.
/// Useful when you want to anchor to an element but offset from it, such as in PopoverMenu.
pub fn offset(mut self, offset: Point<Pixels>) -> Self {
self.offset = Some(offset);
self
}
/// Sets the position mode for this anchored element. Local will have this
/// interpret its [`Anchored::position`] as relative to the parent element.
/// While Window will have it interpret the position as relative to the window.
@@ -138,7 +129,6 @@ impl Element for Anchored {
self.anchor_corner,
size,
bounds,
self.offset,
);
let limits = Bounds {
@@ -255,22 +245,18 @@ impl AnchoredPositionMode {
anchor_corner: Corner,
size: Size<Pixels>,
bounds: Bounds<Pixels>,
offset: Option<Point<Pixels>>,
) -> (Point<Pixels>, Bounds<Pixels>) {
let offset = offset.unwrap_or_default();
match self {
AnchoredPositionMode::Window => {
let anchor_position = anchor_position.unwrap_or(bounds.origin);
let bounds =
Bounds::from_corner_and_size(anchor_corner, anchor_position + offset, size);
let bounds = Bounds::from_corner_and_size(anchor_corner, anchor_position, size);
(anchor_position, bounds)
}
AnchoredPositionMode::Local => {
let anchor_position = anchor_position.unwrap_or_default();
let bounds = Bounds::from_corner_and_size(
anchor_corner,
bounds.origin + anchor_position + offset,
bounds.origin + anchor_position,
size,
);
(anchor_position, bounds)

View File

@@ -134,6 +134,8 @@ impl Keymap {
/// If a user has disabled a binding with `"x": null` it will not be returned. Disabled
/// bindings are evaluated with the same precedence rules so you can disable a rule in
/// a given context only.
///
/// In the case of multi-key bindings, the
pub fn bindings_for_input(
&self,
input: &[Keystroke],

View File

@@ -175,7 +175,6 @@ pub(crate) trait Platform: 'static {
options: PathPromptOptions,
) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>>;
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Result<Option<PathBuf>>>;
fn can_select_mixed_files_and_dirs(&self) -> bool;
fn reveal_path(&self, path: &Path);
fn open_with_system(&self, path: &Path);

View File

@@ -47,10 +47,6 @@ impl LinuxClient for HeadlessClient {
f(&mut self.0.borrow_mut().common)
}
fn keyboard_layout(&self) -> String {
"unknown".to_string()
}
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
vec![]
}

View File

@@ -1,5 +1,6 @@
use std::{
env,
panic::AssertUnwindSafe,
path::{Path, PathBuf},
process::Command,
rc::Rc,
@@ -17,7 +18,7 @@ use std::{
use anyhow::{anyhow, Context as _};
use async_task::Runnable;
use calloop::{channel::Channel, LoopSignal};
use futures::channel::oneshot;
use futures::{channel::oneshot, future::FutureExt};
use util::ResultExt as _;
#[cfg(any(feature = "wayland", feature = "x11"))]
use xkbcommon::xkb::{self, Keycode, Keysym, State};
@@ -45,7 +46,6 @@ const FILE_PICKER_PORTAL_MISSING: &str =
pub trait LinuxClient {
fn compositor_name(&self) -> &'static str;
fn with_common<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R;
fn keyboard_layout(&self) -> String;
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
#[allow(unused)]
fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>>;
@@ -76,7 +76,6 @@ pub(crate) struct PlatformHandlers {
pub(crate) app_menu_action: Option<Box<dyn FnMut(&dyn Action)>>,
pub(crate) will_open_app_menu: Option<Box<dyn FnMut()>>,
pub(crate) validate_app_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
pub(crate) keyboard_layout_change: Option<Box<dyn FnMut()>>,
}
pub(crate) struct LinuxCommon {
@@ -134,11 +133,11 @@ impl<P: LinuxClient + 'static> Platform for P {
}
fn keyboard_layout(&self) -> String {
self.keyboard_layout()
"unknown".into()
}
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
self.with_common(|common| common.callbacks.keyboard_layout_change = Some(callback));
fn on_keyboard_layout_change(&self, _callback: Box<dyn FnMut()>) {
// todo(linux)
}
fn run(&self, on_finish_launching: Box<dyn FnOnce()>) {
@@ -372,11 +371,6 @@ impl<P: LinuxClient + 'static> Platform for P {
done_rx
}
fn can_select_mixed_files_and_dirs(&self) -> bool {
// org.freedesktop.portal.FileChooser only supports "pick files" and "pick directories".
false
}
fn reveal_path(&self, path: &Path) {
self.reveal_path(path.to_owned());
}
@@ -489,7 +483,12 @@ impl<P: LinuxClient + 'static> Platform for P {
let username = attributes
.get("username")
.ok_or_else(|| anyhow!("Cannot find username in stored credentials"))?;
let secret = item.secret().await?;
// oo7 panics if the retrieved secret can't be decrypted due to
// unexpected padding.
let secret = AssertUnwindSafe(item.secret())
.catch_unwind()
.await
.map_err(|_| anyhow!("oo7 panicked while trying to read credentials"))??;
// we lose the zeroizing capabilities at this boundary,
// a current limitation GPUI's credentials api

View File

@@ -588,19 +588,6 @@ impl WaylandClient {
}
impl LinuxClient for WaylandClient {
fn keyboard_layout(&self) -> String {
let state = self.0.borrow();
if let Some(keymap_state) = &state.keymap_state {
let layout_idx = keymap_state.serialize_layout(xkbcommon::xkb::STATE_LAYOUT_EFFECTIVE);
keymap_state
.get_keymap()
.layout_get_name(layout_idx)
.to_string()
} else {
"unknown".to_string()
}
}
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
self.0
.borrow()
@@ -1152,13 +1139,6 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
};
state.keymap_state = Some(xkb::State::new(&keymap));
state.compose_state = get_xkb_compose_state(&xkb_context);
if let Some(mut callback) = state.common.callbacks.keyboard_layout_change.take() {
drop(state);
callback();
state = client.borrow_mut();
state.common.callbacks.keyboard_layout_change = Some(callback);
}
}
wl_keyboard::Event::Enter { surface, .. } => {
state.keyboard_focused_window = get_window(&mut state, &surface.id());
@@ -1196,21 +1176,9 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
let focused_window = state.keyboard_focused_window.clone();
let keymap_state = state.keymap_state.as_mut().unwrap();
let old_layout =
keymap_state.serialize_layout(xkbcommon::xkb::STATE_LAYOUT_EFFECTIVE);
keymap_state.update_mask(mods_depressed, mods_latched, mods_locked, 0, 0, group);
state.modifiers = Modifiers::from_xkb(keymap_state);
if group != old_layout {
if let Some(mut callback) = state.common.callbacks.keyboard_layout_change.take()
{
drop(state);
callback();
state = client.borrow_mut();
state.common.callbacks.keyboard_layout_change = Some(callback);
}
}
let Some(focused_window) = focused_window else {
return;
};

View File

@@ -37,7 +37,7 @@ use x11rb::{
};
use xim::{x11rb::X11rbClient, AttributeName, Client, InputStyle};
use xkbc::x11::ffi::{XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION};
use xkbcommon::xkb::{self as xkbc, LayoutIndex, ModMask, STATE_LAYOUT_EFFECTIVE};
use xkbcommon::xkb::{self as xkbc, LayoutIndex, ModMask};
use super::{
button_or_scroll_from_event_detail, get_valuator_axis_index, modifiers_from_state,
@@ -840,8 +840,6 @@ impl X11Client {
}
Event::XkbStateNotify(event) => {
let mut state = self.0.borrow_mut();
let old_layout = state.xkb.serialize_layout(STATE_LAYOUT_EFFECTIVE);
let new_layout = u32::from(event.group);
state.xkb.update_mask(
event.base_mods.into(),
event.latched_mods.into(),
@@ -855,17 +853,6 @@ impl X11Client {
latched_layout: event.latched_group as u32,
locked_layout: event.locked_group.into(),
};
if new_layout != old_layout {
if let Some(mut callback) = state.common.callbacks.keyboard_layout_change.take()
{
drop(state);
callback();
state = self.0.borrow_mut();
state.common.callbacks.keyboard_layout_change = Some(callback);
}
}
let modifiers = Modifiers::from_xkb(&state.xkb);
if state.modifiers == modifiers {
drop(state);
@@ -1278,16 +1265,6 @@ impl LinuxClient for X11Client {
f(&mut self.0.borrow_mut().common)
}
fn keyboard_layout(&self) -> String {
let state = self.0.borrow();
let layout_idx = state.xkb.serialize_layout(STATE_LAYOUT_EFFECTIVE);
state
.xkb
.get_keymap()
.layout_get_name(layout_idx)
.to_string()
}
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
let state = self.0.borrow();
let setup = state.xcb_connection.setup();

View File

@@ -759,10 +759,6 @@ impl Platform for MacPlatform {
done_rx
}
fn can_select_mixed_files_and_dirs(&self) -> bool {
true
}
fn reveal_path(&self, path: &Path) {
unsafe {
let path = path.to_path_buf();

View File

@@ -227,27 +227,21 @@ fragment float4 shadow_fragment(ShadowFragmentInput input [[stage_in]],
}
}
float alpha;
if (shadow.blur_radius == 0.) {
float distance = quad_sdf(input.position.xy, shadow.bounds, shadow.corner_radii);
alpha = saturate(0.5 - distance);
} else {
// The signal is only non-zero in a limited range, so don't waste samples
float low = point.y - half_size.y;
float high = point.y + half_size.y;
float start = clamp(-3. * shadow.blur_radius, low, high);
float end = clamp(3. * shadow.blur_radius, low, high);
// The signal is only non-zero in a limited range, so don't waste samples
float low = point.y - half_size.y;
float high = point.y + half_size.y;
float start = clamp(-3. * shadow.blur_radius, low, high);
float end = clamp(3. * shadow.blur_radius, low, high);
// Accumulate samples (we can get away with surprisingly few samples)
float step = (end - start) / 4.;
float y = start + step * 0.5;
alpha = 0.;
for (int i = 0; i < 4; i++) {
alpha += blur_along_x(point.x, point.y - y, shadow.blur_radius,
corner_radius, half_size) *
gaussian(y, shadow.blur_radius) * step;
y += step;
}
// Accumulate samples (we can get away with surprisingly few samples)
float step = (end - start) / 4.;
float y = start + step * 0.5;
float alpha = 0.;
for (int i = 0; i < 4; i++) {
alpha += blur_along_x(point.x, point.y - y, shadow.blur_radius,
corner_radius, half_size) *
gaussian(y, shadow.blur_radius) * step;
y += step;
}
return input.color * float4(1., 1., 1., alpha);

View File

@@ -299,10 +299,6 @@ impl Platform for TestPlatform {
rx
}
fn can_select_mixed_files_and_dirs(&self) -> bool {
true
}
fn reveal_path(&self, _path: &std::path::Path) {
unimplemented!()
}

View File

@@ -407,11 +407,6 @@ impl Platform for WindowsPlatform {
rx
}
fn can_select_mixed_files_and_dirs(&self) -> bool {
// The FOS_PICKFOLDERS flag toggles between "only files" and "only folders".
false
}
fn reveal_path(&self, path: &Path) {
let Ok(file_full_path) = path.canonicalize() else {
log::error!("unable to parse file path");

View File

@@ -32,7 +32,6 @@ pub(crate) fn register_action(type_name: &Ident) -> proc_macro2::TokenStream {
fn #action_builder_fn_name() -> gpui::ActionData {
gpui::ActionData {
name: <#type_name as gpui::Action>::debug_name(),
aliases: <#type_name as gpui::Action>::deprecated_aliases(),
type_id: ::std::any::TypeId::of::<#type_name>(),
build: <#type_name as gpui::Action>::build,
}

View File

@@ -24,6 +24,3 @@ theme.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
[features]
test-support = ["gpui/test-support"]

View File

@@ -21,14 +21,12 @@ pub trait InlineCompletionProvider: 'static + Sized {
fn name() -> &'static str;
fn display_name() -> &'static str;
fn show_completions_in_menu() -> bool;
fn show_completions_in_normal_mode() -> bool;
fn is_enabled(
&self,
buffer: &Model<Buffer>,
cursor_position: language::Anchor,
cx: &AppContext,
) -> bool;
fn is_refreshing(&self) -> bool;
fn refresh(
&mut self,
buffer: Model<Buffer>,
@@ -63,8 +61,6 @@ pub trait InlineCompletionProviderHandle {
cx: &AppContext,
) -> bool;
fn show_completions_in_menu(&self) -> bool;
fn show_completions_in_normal_mode(&self) -> bool;
fn is_refreshing(&self, cx: &AppContext) -> bool;
fn refresh(
&self,
buffer: Model<Buffer>,
@@ -105,10 +101,6 @@ where
T::show_completions_in_menu()
}
fn show_completions_in_normal_mode(&self) -> bool {
T::show_completions_in_normal_mode()
}
fn is_enabled(
&self,
buffer: &Model<Buffer>,
@@ -118,10 +110,6 @@ where
self.read(cx).is_enabled(buffer, cursor_position, cx)
}
fn is_refreshing(&self, cx: &AppContext) -> bool {
self.read(cx).is_refreshing()
}
fn refresh(
&self,
buffer: Model<Buffer>,

View File

@@ -19,7 +19,6 @@ editor.workspace = true
feature_flags.workspace = true
fs.workspace = true
gpui.workspace = true
inline_completion.workspace = true
language.workspace = true
paths.workspace = true
settings.workspace = true

View File

@@ -4,9 +4,8 @@ use editor::{scroll::Autoscroll, Editor};
use feature_flags::{FeatureFlagAppExt, ZetaFeatureFlag};
use fs::Fs;
use gpui::{
actions, div, pulsating_between, Action, Animation, AnimationExt, AppContext,
AsyncWindowContext, Corner, Entity, IntoElement, ParentElement, Render, Subscription, View,
ViewContext, WeakView, WindowContext,
actions, div, Action, AppContext, AsyncWindowContext, Corner, Entity, IntoElement,
ParentElement, Render, Subscription, View, ViewContext, WeakView, WindowContext,
};
use language::{
language_settings::{
@@ -15,7 +14,7 @@ use language::{
File, Language,
};
use settings::{update_settings_file, Settings, SettingsStore};
use std::{path::Path, sync::Arc, time::Duration};
use std::{path::Path, sync::Arc};
use supermaven::{AccountStatus, Supermaven};
use workspace::{
create_and_open_local_file,
@@ -40,7 +39,6 @@ pub struct InlineCompletionButton {
editor_enabled: Option<bool>,
language: Option<Arc<Language>>,
file: Option<Arc<dyn File>>,
inline_completion_provider: Option<Arc<dyn inline_completion::InlineCompletionProviderHandle>>,
fs: Arc<dyn Fs>,
workspace: WeakView<Workspace>,
}
@@ -206,35 +204,24 @@ impl Render for InlineCompletionButton {
return div();
}
let this = cx.view().clone();
let button = IconButton::new("zeta", IconName::ZedPredict)
.tooltip(|cx| Tooltip::text("Zed Predict", cx));
let is_refreshing = self
.inline_completion_provider
.as_ref()
.map_or(false, |provider| provider.is_refreshing(cx));
let mut popover_menu = PopoverMenu::new("zeta")
.menu(move |cx| {
Some(this.update(cx, |this, cx| this.build_zeta_context_menu(cx)))
})
.anchor(Corner::BottomRight);
if is_refreshing {
popover_menu = popover_menu.trigger(
button.with_animation(
"pulsating-label",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.2, 1.0)),
|icon_button, delta| icon_button.alpha(delta),
),
);
} else {
popover_menu = popover_menu.trigger(button);
}
div().child(popover_menu.into_any_element())
div().child(
IconButton::new("zeta", IconName::ZedPredict)
.tooltip(|cx| {
Tooltip::with_meta(
"Zed Predict",
Some(&RateCompletions),
"Click to rate completions",
cx,
)
})
.on_click(cx.listener(|this, _, cx| {
if let Some(workspace) = this.workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
RateCompletionModal::toggle(workspace, cx)
});
}
})),
)
}
}
}
@@ -258,7 +245,6 @@ impl InlineCompletionButton {
editor_enabled: None,
language: None,
file: None,
inline_completion_provider: None,
workspace,
fs,
}
@@ -374,25 +360,6 @@ impl InlineCompletionButton {
})
}
fn build_zeta_context_menu(&self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
let workspace = self.workspace.clone();
ContextMenu::build(cx, |menu, cx| {
self.build_language_settings_menu(menu, cx)
.separator()
.entry(
"Rate Completions",
Some(RateCompletions.boxed_clone()),
move |cx| {
workspace
.update(cx, |workspace, cx| {
RateCompletionModal::toggle(workspace, cx)
})
.ok();
},
)
})
}
pub fn update_enabled(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
let editor = editor.read(cx);
let snapshot = editor.buffer().read(cx).snapshot(cx);
@@ -410,7 +377,6 @@ impl InlineCompletionButton {
),
)
};
self.inline_completion_provider = editor.inline_completion_provider();
self.language = language.cloned();
self.file = file;

View File

@@ -208,10 +208,10 @@ pub struct Diagnostic {
/// The human-readable message associated with this diagnostic.
pub message: String,
/// An id that identifies the group to which this diagnostic belongs.
/// 0 is used for diagnostics that do not come from a language server.
///
/// When a language server produces a diagnostic with
/// one or more associated diagnostics, those diagnostics are all
/// assigned a single group ID.
/// When a language server produces a diagnostic with one or more associated diagnostics, those
/// diagnostics are all assigned a single group ID.
pub group_id: usize,
/// Whether this diagnostic is the primary diagnostic for its group.
///

View File

@@ -3115,8 +3115,8 @@ fn html_lang() -> Language {
.with_injection_query(
r#"
(script_element
(raw_text) @injection.content
(#set! injection.language "javascript"))
(raw_text) @content
(#set! "language" "javascript"))
"#,
)
.unwrap()
@@ -3138,15 +3138,15 @@ fn erb_lang() -> Language {
.with_injection_query(
r#"
(
(code) @injection.content
(#set! injection.language "ruby")
(#set! injection.combined)
(code) @content
(#set! "language" "ruby")
(#set! "combined")
)
(
(content) @injection.content
(#set! injection.language "html")
(#set! injection.combined)
(content) @content
(#set! "language" "html")
(#set! "combined")
)
"#,
)
@@ -3278,11 +3278,11 @@ pub fn markdown_lang() -> Language {
r#"
(fenced_code_block
(info_string
(language) @injection.language)
(code_fence_content) @injection.content)
(language) @language)
(code_fence_content) @content)
((inline) @injection.content
(#set! injection.language "markdown-inline"))
((inline) @content
(#set! "language" "markdown-inline"))
"#,
)
.unwrap()

View File

@@ -1,5 +1,4 @@
use crate::{range_to_lsp, Diagnostic};
use anyhow::Result;
use collections::HashMap;
use lsp::LanguageServerId;
use std::{
@@ -55,16 +54,16 @@ pub struct Summary {
impl DiagnosticEntry<PointUtf16> {
/// Returns a raw LSP diagnostic used to provide diagnostic context to LSP
/// codeAction request
pub fn to_lsp_diagnostic_stub(&self) -> Result<lsp::Diagnostic> {
pub fn to_lsp_diagnostic_stub(&self) -> lsp::Diagnostic {
let code = self
.diagnostic
.code
.clone()
.map(lsp::NumberOrString::String);
let range = range_to_lsp(self.range.clone())?;
let range = range_to_lsp(self.range.clone());
Ok(lsp::Diagnostic {
lsp::Diagnostic {
code,
range,
severity: Some(self.diagnostic.severity),
@@ -72,7 +71,7 @@ impl DiagnosticEntry<PointUtf16> {
message: self.diagnostic.message.clone(),
data: self.diagnostic.data.clone(),
..Default::default()
})
}
}
}

View File

@@ -1273,45 +1273,23 @@ impl Language {
.ok_or_else(|| anyhow!("cannot mutate grammar"))?;
let query = Query::new(&grammar.ts_language, source)?;
let mut language_capture_ix = None;
let mut injection_language_capture_ix = None;
let mut content_capture_ix = None;
let mut injection_content_capture_ix = None;
get_capture_indices(
&query,
&mut [
("language", &mut language_capture_ix),
("injection.language", &mut injection_language_capture_ix),
("content", &mut content_capture_ix),
("injection.content", &mut injection_content_capture_ix),
],
);
language_capture_ix = match (language_capture_ix, injection_language_capture_ix) {
(None, Some(ix)) => Some(ix),
(Some(_), Some(_)) => {
return Err(anyhow!(
"both language and injection.language captures are present"
));
}
_ => language_capture_ix,
};
content_capture_ix = match (content_capture_ix, injection_content_capture_ix) {
(None, Some(ix)) => Some(ix),
(Some(_), Some(_)) => {
return Err(anyhow!(
"both content and injection.content captures are present"
));
}
_ => content_capture_ix,
};
let patterns = (0..query.pattern_count())
.map(|ix| {
let mut config = InjectionPatternConfig::default();
for setting in query.property_settings(ix) {
match setting.key.as_ref() {
"language" | "injection.language" => {
"language" => {
config.language.clone_from(&setting.value);
}
"combined" | "injection.combined" => {
"combined" => {
config.combined = true;
}
_ => {}
@@ -1871,19 +1849,14 @@ pub fn point_from_lsp(point: lsp::Position) -> Unclipped<PointUtf16> {
Unclipped(PointUtf16::new(point.line, point.character))
}
pub fn range_to_lsp(range: Range<PointUtf16>) -> Result<lsp::Range> {
if range.start > range.end {
Err(anyhow!(
"Inverted range provided to an LSP request: {:?}-{:?}",
range.start,
range.end
))
} else {
Ok(lsp::Range {
start: point_to_lsp(range.start),
end: point_to_lsp(range.end),
})
pub fn range_to_lsp(range: Range<PointUtf16>) -> lsp::Range {
let mut start = point_to_lsp(range.start);
let mut end = point_to_lsp(range.end);
if start > end {
log::error!("range_to_lsp called with inverted range {start:?}-{end:?}");
mem::swap(&mut start, &mut end);
}
lsp::Range { start, end }
}
pub fn range_from_lsp(range: lsp::Range) -> Range<Unclipped<PointUtf16>> {

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