Merge branch 'main' into telemetry-event

This commit is contained in:
Conrad Irwin 2024-11-22 10:24:13 -07:00
commit fce2b15348
201 changed files with 5764 additions and 1885 deletions

View file

@ -245,6 +245,7 @@ jobs:
# 25 was chosen arbitrarily.
fetch-depth: 25
clean: false
ref: ${{ github.ref }}
- name: Limit target directory size
run: script/clear-target-dir-if-larger-than 100
@ -261,6 +262,9 @@ jobs:
mkdir -p target/
# Ignore any errors that occur while drafting release notes to not fail the build.
script/draft-release-notes "$RELEASE_VERSION" "$RELEASE_CHANNEL" > target/release-notes.md || true
script/create-draft-release target/release-notes.md
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Generate license file
run: script/generate-licenses
@ -268,18 +272,12 @@ jobs:
- name: Create macOS app bundle
run: script/bundle-mac
- name: Rename single-architecture binaries
- name: Rename binaries
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
run: |
mv target/aarch64-apple-darwin/release/Zed.dmg target/aarch64-apple-darwin/release/Zed-aarch64.dmg
mv target/x86_64-apple-darwin/release/Zed.dmg target/x86_64-apple-darwin/release/Zed-x86_64.dmg
- name: Upload app bundle (universal) to workflow run if main branch or specific label
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
with:
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg
path: target/release/Zed.dmg
- name: Upload app bundle (aarch64) to workflow run if main branch or specific label
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
@ -305,8 +303,6 @@ jobs:
target/zed-remote-server-macos-aarch64.gz
target/aarch64-apple-darwin/release/Zed-aarch64.dmg
target/x86_64-apple-darwin/release/Zed-x86_64.dmg
target/release/Zed.dmg
body_path: target/release-notes.md
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@ -402,18 +398,15 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
auto-publish-release:
timeout-minutes: 60
name: Create a Linux bundle
auto-release-preview:
name: Auto release preview
if: ${{ startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre') }}
needs: [bundle-mac, bundle-linux, bundle-linux-aarch64]
runs-on:
- self-hosted
if: ${{ startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre') }}
needs: [bundle-mac, bundle-linux-aarch64, bundle-linux]
- bundle
steps:
- name: Upload app bundle to release
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
with:
draft: false
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
- name: gh release
run: gh release edit $GITHUB_REF_NAME --draft=false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

165
Cargo.lock generated
View file

@ -402,6 +402,7 @@ dependencies = [
"indoc",
"language",
"language_model",
"language_models",
"languages",
"log",
"lsp",
@ -1013,26 +1014,41 @@ dependencies = [
"anyhow",
"client",
"db",
"editor",
"gpui",
"http_client",
"log",
"markdown_preview",
"menu",
"paths",
"release_channel",
"schemars",
"serde",
"serde_derive",
"serde_json",
"settings",
"smol",
"tempfile",
"util",
"which 6.0.3",
"workspace",
]
[[package]]
name = "auto_update_ui"
version = "0.1.0"
dependencies = [
"anyhow",
"auto_update",
"client",
"editor",
"gpui",
"http_client",
"markdown_preview",
"menu",
"release_channel",
"serde",
"serde_json",
"smol",
"util",
"workspace",
]
[[package]]
name = "autocfg"
version = "1.4.0"
@ -2107,9 +2123,9 @@ dependencies = [
[[package]]
name = "cargo_metadata"
version = "0.18.1"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037"
checksum = "afc309ed89476c8957c50fb818f56fe894db857866c3e163335faa91dc34eb85"
dependencies = [
"camino",
"cargo-platform",
@ -2696,7 +2712,6 @@ dependencies = [
"tree-sitter-md",
"ui",
"util",
"vcs_menu",
"workspace",
]
@ -2876,6 +2891,7 @@ dependencies = [
"gpui",
"http_client",
"indoc",
"inline_completion",
"language",
"lsp",
"menu",
@ -3715,12 +3731,14 @@ dependencies = [
"emojis",
"env_logger 0.11.5",
"file_icons",
"fs",
"futures 0.3.31",
"fuzzy",
"git",
"gpui",
"http_client",
"indoc",
"inline_completion",
"itertools 0.13.0",
"language",
"linkify",
@ -4027,6 +4045,7 @@ dependencies = [
"serde_json",
"settings",
"smol",
"util",
]
[[package]]
@ -4112,6 +4131,7 @@ dependencies = [
"serde",
"serde_json",
"toml 0.8.19",
"util",
"wasm-encoder 0.215.0",
"wasmparser 0.215.0",
"wit-component",
@ -4165,6 +4185,7 @@ dependencies = [
"paths",
"project",
"release_channel",
"remote",
"reqwest_client",
"schemars",
"semantic_version",
@ -4173,6 +4194,7 @@ dependencies = [
"serde_json_lenient",
"settings",
"task",
"tempfile",
"theme",
"toml 0.8.19",
"url",
@ -4213,12 +4235,12 @@ dependencies = [
"snippet_provider",
"telemetry",
"theme",
"theme_selector",
"ui",
"util",
"vim",
"vim_mode_setting",
"wasmtime-wasi",
"workspace",
"zed_actions",
]
[[package]]
@ -4325,6 +4347,7 @@ dependencies = [
"urlencoding",
"util",
"workspace",
"zed_actions",
]
[[package]]
@ -4600,6 +4623,7 @@ dependencies = [
"objc",
"parking_lot",
"paths",
"proto",
"rope",
"serde",
"serde_json",
@ -4933,7 +4957,6 @@ dependencies = [
"unindent",
"url",
"util",
"windows 0.58.0",
]
[[package]]
@ -6056,6 +6079,16 @@ dependencies = [
"syn 2.0.87",
]
[[package]]
name = "inline_completion"
version = "0.1.0"
dependencies = [
"gpui",
"language",
"project",
"text",
]
[[package]]
name = "inline_completion_button"
version = "0.1.0"
@ -6076,7 +6109,6 @@ dependencies = [
"supermaven",
"theme",
"ui",
"util",
"workspace",
"zed_actions",
]
@ -6458,6 +6490,7 @@ dependencies = [
"ctor",
"ec4rs",
"env_logger 0.11.5",
"fs",
"futures 0.3.31",
"fuzzy",
"git",
@ -6509,28 +6542,49 @@ dependencies = [
"anthropic",
"anyhow",
"base64 0.22.1",
"client",
"collections",
"copilot",
"ctor",
"editor",
"env_logger 0.11.5",
"feature_flags",
"futures 0.3.31",
"google_ai",
"gpui",
"http_client",
"image",
"inline_completion_button",
"language",
"log",
"menu",
"ollama",
"open_ai",
"parking_lot",
"proto",
"schemars",
"serde",
"serde_json",
"smol",
"strum 0.25.0",
"telemetry",
"ui",
"util",
]
[[package]]
name = "language_models"
version = "0.1.0"
dependencies = [
"anthropic",
"anyhow",
"client",
"collections",
"copilot",
"editor",
"feature_flags",
"fs",
"futures 0.3.31",
"google_ai",
"gpui",
"http_client",
"language_model",
"menu",
"ollama",
"open_ai",
"project",
"proto",
"rand 0.8.5",
"schemars",
"serde",
"serde_json",
@ -6538,12 +6592,10 @@ dependencies = [
"smol",
"strum 0.25.0",
"telemetry",
"text",
"theme",
"thiserror 1.0.69",
"tiktoken-rs",
"ui",
"unindent",
"util",
]
@ -6921,7 +6973,6 @@ dependencies = [
"serde_json",
"smol",
"util",
"windows 0.58.0",
]
[[package]]
@ -7460,7 +7511,6 @@ dependencies = [
"util",
"walkdir",
"which 6.0.3",
"windows 0.58.0",
]
[[package]]
@ -9149,7 +9199,6 @@ dependencies = [
"url",
"util",
"which 6.0.3",
"windows 0.58.0",
"worktree",
]
@ -9382,24 +9431,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "quick_action_bar"
version = "0.1.0"
dependencies = [
"assistant",
"editor",
"gpui",
"markdown_preview",
"picker",
"repl",
"search",
"settings",
"ui",
"util",
"workspace",
"zed_actions",
]
[[package]]
name = "quinn"
version = "0.11.6"
@ -9649,6 +9680,7 @@ dependencies = [
"anyhow",
"auto_update",
"editor",
"extension_host",
"file_finder",
"futures 0.3.31",
"fuzzy",
@ -9674,6 +9706,7 @@ dependencies = [
"ui",
"util",
"workspace",
"zed_actions",
]
[[package]]
@ -9823,6 +9856,7 @@ dependencies = [
"client",
"clock",
"env_logger 0.11.5",
"extension_host",
"fork",
"fs",
"futures 0.3.31",
@ -9879,6 +9913,7 @@ dependencies = [
"editor",
"env_logger 0.11.5",
"feature_flags",
"file_icons",
"futures 0.3.31",
"gpui",
"http_client",
@ -9910,7 +9945,6 @@ dependencies = [
"ui",
"util",
"uuid",
"windows 0.58.0",
"workspace",
]
@ -11783,6 +11817,7 @@ dependencies = [
"futures 0.3.31",
"gpui",
"http_client",
"inline_completion",
"language",
"log",
"postage",
@ -11797,7 +11832,6 @@ dependencies = [
"ui",
"unicode-segmentation",
"util",
"windows 0.58.0",
]
[[package]]
@ -12164,6 +12198,7 @@ dependencies = [
"ui",
"util",
"workspace",
"zed_actions",
]
[[package]]
@ -12251,6 +12286,7 @@ name = "terminal_view"
version = "0.1.0"
dependencies = [
"anyhow",
"breadcrumbs",
"client",
"collections",
"db",
@ -12269,7 +12305,6 @@ dependencies = [
"shellexpand 2.1.2",
"smol",
"task",
"tasks_ui",
"terminal",
"theme",
"ui",
@ -12364,6 +12399,7 @@ dependencies = [
"ui",
"util",
"workspace",
"zed_actions",
]
[[package]]
@ -12576,17 +12612,12 @@ dependencies = [
"call",
"client",
"collections",
"command_palette",
"editor",
"extensions_ui",
"feature_flags",
"feedback",
"gpui",
"http_client",
"notifications",
"pretty_assertions",
"project",
"recent_projects",
"remote",
"rpc",
"serde",
@ -12594,11 +12625,9 @@ dependencies = [
"smallvec",
"story",
"theme",
"theme_selector",
"tree-sitter-md",
"ui",
"util",
"vcs_menu",
"windows 0.58.0",
"workspace",
"zed_actions",
@ -13520,6 +13549,7 @@ dependencies = [
"rust-embed",
"serde",
"serde_json",
"smol",
"take-until",
"tempfile",
"tendril",
@ -13609,6 +13639,7 @@ dependencies = [
"ui",
"util",
"workspace",
"zed_actions",
]
[[package]]
@ -13655,10 +13686,20 @@ dependencies = [
"tokio",
"ui",
"util",
"vim_mode_setting",
"workspace",
"zed_actions",
]
[[package]]
name = "vim_mode_setting"
version = "0.1.0"
dependencies = [
"anyhow",
"gpui",
"settings",
]
[[package]]
name = "vscode_theme"
version = "0.2.0"
@ -14359,22 +14400,20 @@ version = "0.1.0"
dependencies = [
"anyhow",
"client",
"copilot",
"db",
"editor",
"extensions_ui",
"fuzzy",
"gpui",
"inline_completion_button",
"install_cli",
"picker",
"project",
"schemars",
"serde",
"settings",
"theme_selector",
"ui",
"util",
"vim",
"vim_mode_setting",
"workspace",
"zed_actions",
]
@ -15425,7 +15464,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.163.0"
version = "0.164.0"
dependencies = [
"activity_indicator",
"anyhow",
@ -15436,6 +15475,7 @@ dependencies = [
"async-watch",
"audio",
"auto_update",
"auto_update_ui",
"backtrace",
"breadcrumbs",
"call",
@ -15474,6 +15514,7 @@ dependencies = [
"journal",
"language",
"language_model",
"language_models",
"language_selector",
"language_tools",
"languages",
@ -15489,12 +15530,12 @@ dependencies = [
"outline_panel",
"parking_lot",
"paths",
"picker",
"profiling",
"project",
"project_panel",
"project_symbols",
"proto",
"quick_action_bar",
"recent_projects",
"release_channel",
"remote",
@ -15530,7 +15571,9 @@ dependencies = [
"urlencoding",
"util",
"uuid",
"vcs_menu",
"vim",
"vim_mode_setting",
"welcome",
"windows 0.58.0",
"winresource",

View file

@ -9,6 +9,7 @@ members = [
"crates/assistant_tool",
"crates/audio",
"crates/auto_update",
"crates/auto_update_ui",
"crates/breadcrumbs",
"crates/call",
"crates/channel",
@ -49,11 +50,13 @@ members = [
"crates/http_client",
"crates/image_viewer",
"crates/indexed_docs",
"crates/inline_completion",
"crates/inline_completion_button",
"crates/install_cli",
"crates/journal",
"crates/language",
"crates/language_model",
"crates/language_models",
"crates/language_selector",
"crates/language_tools",
"crates/languages",
@ -78,7 +81,6 @@ members = [
"crates/project_panel",
"crates/project_symbols",
"crates/proto",
"crates/quick_action_bar",
"crates/recent_projects",
"crates/refineable",
"crates/refineable/derive_refineable",
@ -126,6 +128,7 @@ members = [
"crates/util",
"crates/vcs_menu",
"crates/vim",
"crates/vim_mode_setting",
"crates/welcome",
"crates/workspace",
"crates/worktree",
@ -185,6 +188,7 @@ assistant_slash_command = { path = "crates/assistant_slash_command" }
assistant_tool = { path = "crates/assistant_tool" }
audio = { path = "crates/audio" }
auto_update = { path = "crates/auto_update" }
auto_update_ui = { path = "crates/auto_update_ui" }
breadcrumbs = { path = "crates/breadcrumbs" }
call = { path = "crates/call" }
channel = { path = "crates/channel" }
@ -221,11 +225,13 @@ html_to_markdown = { path = "crates/html_to_markdown" }
http_client = { path = "crates/http_client" }
image_viewer = { path = "crates/image_viewer" }
indexed_docs = { path = "crates/indexed_docs" }
inline_completion = { path = "crates/inline_completion" }
inline_completion_button = { path = "crates/inline_completion_button" }
install_cli = { path = "crates/install_cli" }
journal = { path = "crates/journal" }
language = { path = "crates/language" }
language_model = { path = "crates/language_model" }
language_models = { path = "crates/language_models" }
language_selector = { path = "crates/language_selector" }
language_tools = { path = "crates/language_tools" }
languages = { path = "crates/languages" }
@ -252,7 +258,6 @@ project = { path = "crates/project" }
project_panel = { path = "crates/project_panel" }
project_symbols = { path = "crates/project_symbols" }
proto = { path = "crates/proto" }
quick_action_bar = { path = "crates/quick_action_bar" }
recent_projects = { path = "crates/recent_projects" }
refineable = { path = "crates/refineable" }
release_channel = { path = "crates/release_channel" }
@ -298,6 +303,7 @@ ui_macros = { path = "crates/ui_macros" }
util = { path = "crates/util" }
vcs_menu = { path = "crates/vcs_menu" }
vim = { path = "crates/vim" }
vim_mode_setting = { path = "crates/vim_mode_setting" }
welcome = { path = "crates/welcome" }
workspace = { path = "crates/workspace" }
worktree = { path = "crates/worktree" }
@ -332,7 +338,7 @@ blade-macros = { git = "https://github.com/kvark/blade", rev = "e142a3a5e678eb6a
blade-util = { git = "https://github.com/kvark/blade", rev = "e142a3a5e678eb6a13e642ad8401b1f3aa38e969" }
blake3 = "1.5.3"
bytes = "1.0"
cargo_metadata = "0.18"
cargo_metadata = "0.19"
cargo_toml = "0.20"
chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4.4", features = ["derive"] }

View file

@ -49,8 +49,9 @@
"ctrl-d": "editor::Delete",
"tab": "editor::Tab",
"shift-tab": "editor::TabPrev",
"ctrl-k": "editor::CutToEndOfLine",
"ctrl-t": "editor::Transpose",
"ctrl-k": "editor::KillRingCut",
"ctrl-y": "editor::KillRingYank",
"cmd-k q": "editor::Rewrap",
"cmd-k cmd-q": "editor::Rewrap",
"cmd-backspace": "editor::DeleteToBeginningOfLine",
@ -92,6 +93,8 @@
"ctrl-e": "editor::MoveToEndOfLine",
"cmd-up": "editor::MoveToBeginning",
"cmd-down": "editor::MoveToEnd",
"ctrl-home": "editor::MoveToBeginning",
"ctrl-end": "editor::MoveToEnd",
"shift-up": "editor::SelectUp",
"ctrl-shift-p": "editor::SelectUp",
"shift-down": "editor::SelectDown",

View file

@ -381,8 +381,7 @@
"shift-b": "vim::CurlyBrackets",
"<": "vim::AngleBrackets",
">": "vim::AngleBrackets",
"a": "vim::AngleBrackets",
"g": "vim::Argument"
"a": "vim::Argument"
}
},
{
@ -578,7 +577,7 @@
}
},
{
"context": "EmptyPane || SharedScreen || MarkdownPreview || KeyContextView",
"context": "EmptyPane || SharedScreen || MarkdownPreview || KeyContextView || Welcome",
"use_layout_keys": true,
"bindings": {
":": "command_palette::Toggle",

View file

@ -668,7 +668,7 @@
},
// Add files or globs of files that will be excluded by Zed entirely:
// they will be skipped during FS scan(s), file tree and file search
// will lack the corresponding file entries.
// will lack the corresponding file entries. Overrides `file_scan_inclusions`.
"file_scan_exclusions": [
"**/.git",
"**/.svn",
@ -679,6 +679,14 @@
"**/.classpath",
"**/.settings"
],
// Add files or globs of files that will be included by Zed, even when
// ignored by git. This is useful for files that are not tracked by git,
// but are still important to your project. Note that globs that are
// overly broad can slow down Zed's file scanning. Overridden by `file_scan_exclusions`.
"file_scan_inclusions": [
".env*",
"docker-compose.*.yml"
],
// Git gutter behavior configuration.
"git": {
// Control whether the git gutter is shown. May take 2 values:
@ -839,8 +847,12 @@
}
},
"toolbar": {
// Whether to display the terminal title in its toolbar.
"title": true
// Whether to display the terminal title in its toolbar's breadcrumbs.
// Only shown if the terminal title is not empty.
//
// The shell running in the terminal needs to be configured to emit the title.
// Example: `echo -e "\e]2;New Title\007";`
"breadcrumbs": true
}
// Set the terminal's font size. If this option is not included,
// the terminal will default to matching the buffer's font size.

View file

@ -50,6 +50,7 @@ indexed_docs.workspace = true
indoc.workspace = true
language.workspace = true
language_model.workspace = true
language_models.workspace = true
log.workspace = true
lsp.workspace = true
markdown.workspace = true

View file

@ -50,11 +50,11 @@ use indexed_docs::IndexedDocsStore;
use language::{
language_settings::SoftWrap, BufferSnapshot, LanguageRegistry, LspAdapterDelegate, ToOffset,
};
use language_model::{
provider::cloud::PROVIDER_ID, LanguageModelProvider, LanguageModelProviderId,
LanguageModelRegistry, Role,
};
use language_model::{LanguageModelImage, LanguageModelToolUse};
use language_model::{
LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, Role,
ZED_CLOUD_PROVIDER_ID,
};
use multi_buffer::MultiBufferRow;
use picker::{Picker, PickerDelegate};
use project::lsp_store::LocalLspAdapterDelegate;
@ -664,7 +664,7 @@ impl AssistantPanel {
// If we're signed out and don't have a provider configured, or we're signed-out AND Zed.dev is
// the provider, we want to show a nudge to sign in.
let show_zed_ai_notice = client_status.is_signed_out()
&& active_provider.map_or(true, |provider| provider.id().0 == PROVIDER_ID);
&& active_provider.map_or(true, |provider| provider.id().0 == ZED_CLOUD_PROVIDER_ID);
self.show_zed_ai_notice = show_zed_ai_notice;
cx.notify();

View file

@ -5,13 +5,12 @@ use anthropic::Model as AnthropicModel;
use feature_flags::FeatureFlagAppExt;
use fs::Fs;
use gpui::{AppContext, Pixels};
use language_model::provider::open_ai;
use language_model::settings::{
AnthropicSettingsContent, AnthropicSettingsContentV1, OllamaSettingsContent,
OpenAiSettingsContent, OpenAiSettingsContentV1, VersionedAnthropicSettingsContent,
VersionedOpenAiSettingsContent,
use language_model::{CloudModel, LanguageModel};
use language_models::{
provider::open_ai, AllLanguageModelSettings, AnthropicSettingsContent,
AnthropicSettingsContentV1, OllamaSettingsContent, OpenAiSettingsContent,
OpenAiSettingsContentV1, VersionedAnthropicSettingsContent, VersionedOpenAiSettingsContent,
};
use language_model::{settings::AllLanguageModelSettings, CloudModel, LanguageModel};
use ollama::Model as OllamaModel;
use schemars::{schema::Schema, JsonSchema};
use serde::{Deserialize, Serialize};

View file

@ -25,13 +25,15 @@ use gpui::{
use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, Point, ToOffset};
use language_model::{
logging::report_assistant_event,
provider::cloud::{MaxMonthlySpendReachedError, PaymentRequiredError},
LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionEvent,
LanguageModelImage, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolUse, MessageContent, Role,
StopReason,
};
use language_models::{
provider::cloud::{MaxMonthlySpendReachedError, PaymentRequiredError},
report_assistant_event,
};
use open_ai::Model as OpenAiModel;
use paths::contexts_dir;
use project::Project;

View file

@ -770,7 +770,7 @@ impl ContextStore {
contexts.push(SavedContextMetadata {
title: title.to_string(),
path,
mtime: metadata.mtime.into(),
mtime: metadata.mtime.timestamp_for_user().into(),
});
}
}

View file

@ -30,9 +30,10 @@ use gpui::{
};
use language::{Buffer, IndentKind, Point, Selection, TransactionId};
use language_model::{
logging::report_assistant_event, LanguageModel, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequestMessage, LanguageModelTextStream, Role,
LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
LanguageModelTextStream, Role,
};
use language_models::report_assistant_event;
use multi_buffer::MultiBufferRow;
use parking_lot::Mutex;
use project::{CodeAction, ProjectTransaction};

View file

@ -17,9 +17,9 @@ use gpui::{
};
use language::Buffer;
use language_model::{
logging::report_assistant_event, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequestMessage, Role,
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
};
use language_models::report_assistant_event;
use settings::Settings;
use std::{
cmp,

View file

@ -16,21 +16,16 @@ doctest = false
anyhow.workspace = true
client.workspace = true
db.workspace = true
editor.workspace = true
gpui.workspace = true
http_client.workspace = true
log.workspace = true
markdown_preview.workspace = true
menu.workspace = true
paths.workspace = true
release_channel.workspace = true
schemars.workspace = true
serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
settings.workspace = true
smol.workspace = true
tempfile.workspace = true
util.workspace = true
which.workspace = true
workspace.workspace = true

View file

@ -1,27 +1,19 @@
mod update_notification;
use anyhow::{anyhow, Context, Result};
use client::{Client, TelemetrySettings};
use db::kvp::KEY_VALUE_STORE;
use db::RELEASE_CHANNEL;
use editor::{Editor, MultiBuffer};
use gpui::{
actions, AppContext, AsyncAppContext, Context as _, Global, Model, ModelContext,
SemanticVersion, SharedString, Task, View, ViewContext, VisualContext, WindowContext,
SemanticVersion, Task, WindowContext,
};
use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView};
use paths::remote_servers_dir;
use schemars::JsonSchema;
use serde::Deserialize;
use serde_derive::Serialize;
use smol::{fs, io::AsyncReadExt};
use settings::{Settings, SettingsSources, SettingsStore};
use smol::{fs::File, process::Command};
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
use paths::remote_servers_dir;
use release_channel::{AppCommitSha, ReleaseChannel};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources, SettingsStore};
use smol::{fs, io::AsyncReadExt};
use smol::{fs::File, process::Command};
use std::{
env::{
self,
@ -32,24 +24,13 @@ use std::{
sync::Arc,
time::Duration,
};
use update_notification::UpdateNotification;
use util::ResultExt;
use which::which;
use workspace::notifications::NotificationId;
use workspace::Workspace;
const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";
const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60);
actions!(
auto_update,
[
Check,
DismissErrorMessage,
ViewReleaseNotes,
ViewReleaseNotesLocally
]
);
actions!(auto_update, [Check, DismissErrorMessage, ViewReleaseNotes,]);
#[derive(Serialize)]
struct UpdateRequestBody {
@ -146,12 +127,6 @@ struct GlobalAutoUpdate(Option<Model<AutoUpdater>>);
impl Global for GlobalAutoUpdate {}
#[derive(Deserialize)]
struct ReleaseNotesBody {
title: String,
release_notes: String,
}
pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut AppContext) {
AutoUpdateSetting::register(cx);
@ -161,10 +136,6 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut AppContext) {
workspace.register_action(|_, action, cx| {
view_release_notes(action, cx);
});
workspace.register_action(|workspace, _: &ViewReleaseNotesLocally, cx| {
view_release_notes_locally(workspace, cx);
});
})
.detach();
@ -264,121 +235,6 @@ pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) -> Option<(
None
}
fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
let release_channel = ReleaseChannel::global(cx);
let url = match release_channel {
ReleaseChannel::Nightly => Some("https://github.com/zed-industries/zed/commits/nightly/"),
ReleaseChannel::Dev => Some("https://github.com/zed-industries/zed/commits/main/"),
_ => None,
};
if let Some(url) = url {
cx.open_url(url);
return;
}
let version = AppVersion::global(cx).to_string();
let client = client::Client::global(cx).http_client();
let url = client.build_url(&format!(
"/api/release_notes/v2/{}/{}",
release_channel.dev_name(),
version
));
let markdown = workspace
.app_state()
.languages
.language_for_name("Markdown");
workspace
.with_local_workspace(cx, move |_, cx| {
cx.spawn(|workspace, mut cx| async move {
let markdown = markdown.await.log_err();
let response = client.get(&url, Default::default(), true).await;
let Some(mut response) = response.log_err() else {
return;
};
let mut body = Vec::new();
response.body_mut().read_to_end(&mut body).await.ok();
let body: serde_json::Result<ReleaseNotesBody> =
serde_json::from_slice(body.as_slice());
if let Ok(body) = body {
workspace
.update(&mut cx, |workspace, cx| {
let project = workspace.project().clone();
let buffer = project.update(cx, |project, cx| {
project.create_local_buffer("", markdown, cx)
});
buffer.update(cx, |buffer, cx| {
buffer.edit([(0..0, body.release_notes)], None, cx)
});
let language_registry = project.read(cx).languages().clone();
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
let tab_description = SharedString::from(body.title.to_string());
let editor = cx.new_view(|cx| {
Editor::for_multibuffer(buffer, Some(project), true, cx)
});
let workspace_handle = workspace.weak_handle();
let view: View<MarkdownPreviewView> = MarkdownPreviewView::new(
MarkdownPreviewMode::Default,
editor,
workspace_handle,
language_registry,
Some(tab_description),
cx,
);
workspace.add_item_to_active_pane(
Box::new(view.clone()),
None,
true,
cx,
);
cx.notify();
})
.log_err();
}
})
.detach();
})
.detach();
}
pub fn notify_of_any_new_update(cx: &mut ViewContext<Workspace>) -> Option<()> {
let updater = AutoUpdater::get(cx)?;
let version = updater.read(cx).current_version;
let should_show_notification = updater.read(cx).should_show_update_notification(cx);
cx.spawn(|workspace, mut cx| async move {
let should_show_notification = should_show_notification.await?;
if should_show_notification {
workspace.update(&mut cx, |workspace, cx| {
let workspace_handle = workspace.weak_handle();
workspace.show_notification(
NotificationId::unique::<UpdateNotification>(),
cx,
|cx| cx.new_view(|_| UpdateNotification::new(version, workspace_handle)),
);
updater.update(cx, |updater, cx| {
updater
.set_should_show_update_notification(false, cx)
.detach_and_log_err(cx);
});
})?;
}
anyhow::Ok(())
})
.detach();
None
}
impl AutoUpdater {
pub fn get(cx: &mut AppContext) -> Option<Model<Self>> {
cx.default_global::<GlobalAutoUpdate>().0.clone()
@ -423,6 +279,10 @@ impl AutoUpdater {
}));
}
pub fn current_version(&self) -> SemanticVersion {
self.current_version
}
pub fn status(&self) -> AutoUpdateStatus {
self.status.clone()
}
@ -646,7 +506,7 @@ impl AutoUpdater {
Ok(())
}
fn set_should_show_update_notification(
pub fn set_should_show_update_notification(
&self,
should_show: bool,
cx: &AppContext,
@ -668,7 +528,7 @@ impl AutoUpdater {
})
}
fn should_show_update_notification(&self, cx: &AppContext) -> Task<Result<bool>> {
pub fn should_show_update_notification(&self, cx: &AppContext) -> Task<Result<bool>> {
cx.background_executor().spawn(async move {
Ok(KEY_VALUE_STORE
.read_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)?

View file

@ -0,0 +1,28 @@
[package]
name = "auto_update_ui"
version = "0.1.0"
edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/auto_update_ui.rs"
[dependencies]
anyhow.workspace = true
auto_update.workspace = true
client.workspace = true
editor.workspace = true
gpui.workspace = true
http_client.workspace = true
markdown_preview.workspace = true
menu.workspace = true
release_channel.workspace = true
serde.workspace = true
serde_json.workspace = true
smol.workspace = true
util.workspace = true
workspace.workspace = true

View file

@ -0,0 +1,147 @@
mod update_notification;
use auto_update::AutoUpdater;
use editor::{Editor, MultiBuffer};
use gpui::{actions, prelude::*, AppContext, SharedString, View, ViewContext};
use http_client::HttpClient;
use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView};
use release_channel::{AppVersion, ReleaseChannel};
use serde::Deserialize;
use smol::io::AsyncReadExt;
use util::ResultExt as _;
use workspace::notifications::NotificationId;
use workspace::Workspace;
use crate::update_notification::UpdateNotification;
actions!(auto_update, [ViewReleaseNotesLocally]);
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(|workspace: &mut Workspace, _cx| {
workspace.register_action(|workspace, _: &ViewReleaseNotesLocally, cx| {
view_release_notes_locally(workspace, cx);
});
})
.detach();
}
#[derive(Deserialize)]
struct ReleaseNotesBody {
title: String,
release_notes: String,
}
fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
let release_channel = ReleaseChannel::global(cx);
let url = match release_channel {
ReleaseChannel::Nightly => Some("https://github.com/zed-industries/zed/commits/nightly/"),
ReleaseChannel::Dev => Some("https://github.com/zed-industries/zed/commits/main/"),
_ => None,
};
if let Some(url) = url {
cx.open_url(url);
return;
}
let version = AppVersion::global(cx).to_string();
let client = client::Client::global(cx).http_client();
let url = client.build_url(&format!(
"/api/release_notes/v2/{}/{}",
release_channel.dev_name(),
version
));
let markdown = workspace
.app_state()
.languages
.language_for_name("Markdown");
workspace
.with_local_workspace(cx, move |_, cx| {
cx.spawn(|workspace, mut cx| async move {
let markdown = markdown.await.log_err();
let response = client.get(&url, Default::default(), true).await;
let Some(mut response) = response.log_err() else {
return;
};
let mut body = Vec::new();
response.body_mut().read_to_end(&mut body).await.ok();
let body: serde_json::Result<ReleaseNotesBody> =
serde_json::from_slice(body.as_slice());
if let Ok(body) = body {
workspace
.update(&mut cx, |workspace, cx| {
let project = workspace.project().clone();
let buffer = project.update(cx, |project, cx| {
project.create_local_buffer("", markdown, cx)
});
buffer.update(cx, |buffer, cx| {
buffer.edit([(0..0, body.release_notes)], None, cx)
});
let language_registry = project.read(cx).languages().clone();
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
let tab_description = SharedString::from(body.title.to_string());
let editor = cx.new_view(|cx| {
Editor::for_multibuffer(buffer, Some(project), true, cx)
});
let workspace_handle = workspace.weak_handle();
let view: View<MarkdownPreviewView> = MarkdownPreviewView::new(
MarkdownPreviewMode::Default,
editor,
workspace_handle,
language_registry,
Some(tab_description),
cx,
);
workspace.add_item_to_active_pane(
Box::new(view.clone()),
None,
true,
cx,
);
cx.notify();
})
.log_err();
}
})
.detach();
})
.detach();
}
pub fn notify_of_any_new_update(cx: &mut ViewContext<Workspace>) -> Option<()> {
let updater = AutoUpdater::get(cx)?;
let version = updater.read(cx).current_version();
let should_show_notification = updater.read(cx).should_show_update_notification(cx);
cx.spawn(|workspace, mut cx| async move {
let should_show_notification = should_show_notification.await?;
if should_show_notification {
workspace.update(&mut cx, |workspace, cx| {
let workspace_handle = workspace.weak_handle();
workspace.show_notification(
NotificationId::unique::<UpdateNotification>(),
cx,
|cx| cx.new_view(|_| UpdateNotification::new(version, workspace_handle)),
);
updater.update(cx, |updater, cx| {
updater
.set_should_show_update_notification(false, cx)
.detach_and_log_err(cx);
});
})?;
}
anyhow::Ok(())
})
.detach();
None
}

View file

@ -225,6 +225,8 @@ impl Telemetry {
cx.background_executor()
.spawn({
let state = state.clone();
let os_version = os_version();
state.lock().os_version = Some(os_version.clone());
async move {
if let Some(tempfile) = File::create(Self::log_file_path()).log_err() {
state.lock().log_file = Some(tempfile);

View file

@ -1559,10 +1559,13 @@ fn for_snowflake(
);
map.insert("signed_in".to_string(), event.signed_in.into());
if let Some(country_code) = country_code.as_ref() {
map.insert("country_code".to_string(), country_code.clone().into());
map.insert("country".to_string(), country_code.clone().into());
}
}
// NOTE: most amplitude user properties are read out of our event_properties
// dictionary. See https://app.amplitude.com/data/zed/Zed/sources/detail/production/falcon%3A159998
// for how that is configured.
let user_properties = Some(serde_json::json!({
"is_staff": body.is_staff,
}));
@ -1589,48 +1592,3 @@ struct SnowflakeRow {
pub user_properties: Option<serde_json::Value>,
pub insert_id: Option<String>,
}
#[derive(Serialize, Deserialize)]
struct SnowflakeData {
/// Identifier unique to each Zed installation (differs for stable, preview, dev)
pub installation_id: Option<String>,
/// Identifier unique to each logged in Zed user (randomly generated on first sign in)
/// Identifier unique to each Zed session (differs for each time you open Zed)
pub session_id: Option<String>,
pub metrics_id: Option<String>,
/// True for Zed staff, otherwise false
pub is_staff: Option<bool>,
/// Zed version number
pub app_version: String,
pub os_name: String,
pub os_version: Option<String>,
pub architecture: String,
/// Zed release channel (stable, preview, dev)
pub release_channel: Option<String>,
pub signed_in: bool,
#[serde(flatten)]
pub editor_event: Option<EditorEvent>,
#[serde(flatten)]
pub inline_completion_event: Option<InlineCompletionEvent>,
#[serde(flatten)]
pub call_event: Option<CallEvent>,
#[serde(flatten)]
pub assistant_event: Option<AssistantEvent>,
#[serde(flatten)]
pub cpu_event: Option<CpuEvent>,
#[serde(flatten)]
pub memory_event: Option<MemoryEvent>,
#[serde(flatten)]
pub app_event: Option<AppEvent>,
#[serde(flatten)]
pub setting_event: Option<SettingEvent>,
#[serde(flatten)]
pub extension_event: Option<ExtensionEvent>,
#[serde(flatten)]
pub edit_event: Option<EditEvent>,
#[serde(flatten)]
pub repl_event: Option<ReplEvent>,
#[serde(flatten)]
pub action_event: Option<ActionEvent>,
}

View file

@ -835,7 +835,7 @@ impl RandomizedTest for ProjectCollaborationTest {
.map_ok(|_| ())
.boxed(),
LspRequestKind::CodeAction => project
.code_actions(&buffer, offset..offset, cx)
.code_actions(&buffer, offset..offset, None, cx)
.map(|_| Ok(()))
.boxed(),
LspRequestKind::Definition => project

View file

@ -58,12 +58,11 @@ settings.workspace = true
smallvec.workspace = true
story = { workspace = true, optional = true }
theme.workspace = true
time_format.workspace = true
time.workspace = true
time_format.workspace = true
title_bar.workspace = true
ui.workspace = true
util.workspace = true
vcs_menu.workspace = true
workspace.workspace = true
[dev-dependencies]

View file

@ -33,7 +33,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
notification_panel::init(cx);
notifications::init(app_state, cx);
title_bar::init(cx);
vcs_menu::init(cx);
}
fn notification_window_options(

View file

@ -11,7 +11,7 @@ use command_palette_hooks::{
};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
actions, Action, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Global,
Action, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Global,
ParentElement, Render, Styled, Task, UpdateGlobal, View, ViewContext, VisualContext, WeakView,
};
use picker::{Picker, PickerDelegate};
@ -21,9 +21,7 @@ use settings::Settings;
use ui::{h_flex, prelude::*, v_flex, HighlightedLabel, KeyBinding, ListItem, ListItemSpacing};
use util::ResultExt;
use workspace::{ModalView, Workspace, WorkspaceSettings};
use zed_actions::OpenZedUrl;
actions!(command_palette, [Toggle]);
use zed_actions::{command_palette::Toggle, OpenZedUrl};
pub fn init(cx: &mut AppContext) {
client::init_settings(cx);

View file

@ -9,7 +9,7 @@ use serde_json::{value::RawValue, Value};
use smol::{
channel,
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
process::{self, Child},
process::Child,
};
use std::{
fmt,
@ -152,7 +152,7 @@ impl Client {
&binary.args
);
let mut command = process::Command::new(&binary.executable);
let mut command = util::command::new_smol_command(&binary.executable);
command
.args(&binary.args)
.envs(binary.env.unwrap_or_default())

View file

@ -24,6 +24,8 @@ use gpui::{AsyncAppContext, EventEmitter, Model, ModelContext, Subscription, Tas
use log;
use parking_lot::RwLock;
use project::Project;
use schemars::gen::SchemaGenerator;
use schemars::schema::{InstanceType, Schema, SchemaObject};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources, SettingsStore};
@ -36,16 +38,32 @@ use crate::{
#[derive(Deserialize, Serialize, Default, Clone, PartialEq, Eq, JsonSchema, Debug)]
pub struct ContextServerSettings {
/// Settings for context servers used in the Assistant.
#[serde(default)]
pub context_servers: HashMap<Arc<str>, ServerConfig>,
}
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, Debug, Default)]
pub struct ServerConfig {
/// The command to run this context server.
///
/// This will override the command set by an extension.
pub command: Option<ServerCommand>,
/// The settings for this context server.
///
/// Consult the documentation for the context server to see what settings
/// are supported.
#[schemars(schema_with = "server_config_settings_json_schema")]
pub settings: Option<serde_json::Value>,
}
fn server_config_settings_json_schema(_generator: &mut SchemaGenerator) -> Schema {
Schema::Object(SchemaObject {
instance_type: Some(InstanceType::Object.into()),
..Default::default()
})
}
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
pub struct ServerCommand {
pub path: String,

View file

@ -29,14 +29,14 @@ anyhow.workspace = true
async-compression.workspace = true
async-tar.workspace = true
chrono.workspace = true
collections.workspace = true
client.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
editor.workspace = true
fs.workspace = true
futures.workspace = true
gpui.workspace = true
http_client.workspace = true
inline_completion.workspace = true
language.workspace = true
lsp.workspace = true
menu.workspace = true
@ -44,12 +44,12 @@ node_runtime.workspace = true
parking_lot.workspace = true
paths.workspace = true
project.workspace = true
schemars = { workspace = true, optional = true }
serde.workspace = true
serde_json.workspace = true
schemars = { workspace = true, optional = true }
strum.workspace = true
settings.workspace = true
smol.workspace = true
strum.workspace = true
task.workspace = true
ui.workspace = true
util.workspace = true

View file

@ -38,8 +38,8 @@ use std::{
};
use util::{fs::remove_matching, maybe, ResultExt};
pub use copilot_completion_provider::CopilotCompletionProvider;
pub use sign_in::CopilotCodeVerification;
pub use crate::copilot_completion_provider::CopilotCompletionProvider;
pub use crate::sign_in::{initiate_sign_in, CopilotCodeVerification};
actions!(
copilot,
@ -1231,7 +1231,7 @@ mod tests {
fn disk_state(&self) -> language::DiskState {
language::DiskState::Present {
mtime: std::time::UNIX_EPOCH,
mtime: ::fs::MTime::from_seconds_and_nanos(100, 42),
}
}

View file

@ -1,8 +1,8 @@
use crate::{Completion, Copilot};
use anyhow::Result;
use client::telemetry::Telemetry;
use editor::{CompletionProposal, Direction, InlayProposal, InlineCompletionProvider};
use gpui::{AppContext, EntityId, Model, ModelContext, Task};
use inline_completion::{CompletionProposal, Direction, InlayProposal, InlineCompletionProvider};
use language::{
language_settings::{all_language_settings, AllLanguageSettings},
Buffer, OffsetRangeExt, ToOffset,

View file

@ -5,10 +5,79 @@ use gpui::{
Styled, Subscription, ViewContext,
};
use ui::{prelude::*, Button, Label, Vector, VectorName};
use workspace::ModalView;
use util::ResultExt as _;
use workspace::notifications::NotificationId;
use workspace::{ModalView, Toast, Workspace};
const COPILOT_SIGN_UP_URL: &str = "https://github.com/features/copilot";
struct CopilotStartingToast;
pub fn initiate_sign_in(cx: &mut WindowContext) {
let Some(copilot) = Copilot::global(cx) else {
return;
};
let status = copilot.read(cx).status();
let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
return;
};
match status {
Status::Starting { task } => {
let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
return;
};
let Ok(workspace) = workspace.update(cx, |workspace, cx| {
workspace.show_toast(
Toast::new(
NotificationId::unique::<CopilotStartingToast>(),
"Copilot is starting...",
),
cx,
);
workspace.weak_handle()
}) else {
return;
};
cx.spawn(|mut cx| async move {
task.await;
if let Some(copilot) = cx.update(|cx| Copilot::global(cx)).ok().flatten() {
workspace
.update(&mut cx, |workspace, cx| match copilot.read(cx).status() {
Status::Authorized => workspace.show_toast(
Toast::new(
NotificationId::unique::<CopilotStartingToast>(),
"Copilot has started!",
),
cx,
),
_ => {
workspace.dismiss_toast(
&NotificationId::unique::<CopilotStartingToast>(),
cx,
);
copilot
.update(cx, |copilot, cx| copilot.sign_in(cx))
.detach_and_log_err(cx);
}
})
.log_err();
}
})
.detach();
}
_ => {
copilot.update(cx, |this, cx| this.sign_in(cx)).detach();
workspace
.update(cx, |this, cx| {
this.toggle_modal(cx, |cx| CopilotCodeVerification::new(&copilot, cx));
})
.ok();
}
}
}
pub struct CopilotCodeVerification {
status: Status,
connect_clicked: bool,

View file

@ -776,7 +776,7 @@ impl Item for ProjectDiagnosticsEditor {
}
}
fn breadcrumb_location(&self) -> ToolbarItemLocation {
fn breadcrumb_location(&self, _: &AppContext) -> ToolbarItemLocation {
ToolbarItemLocation::PrimaryLeft
}

View file

@ -42,10 +42,12 @@ emojis.workspace = true
file_icons.workspace = true
futures.workspace = true
fuzzy.workspace = true
fs.workspace = true
git.workspace = true
gpui.workspace = true
http_client.workspace = true
indoc.workspace = true
inline_completion.workspace = true
itertools.workspace = true
language.workspace = true
linkify.workspace = true

View file

@ -271,6 +271,8 @@ gpui::actions!(
Hover,
Indent,
JoinLines,
KillRingCut,
KillRingYank,
LineDown,
LineUp,
MoveDown,

View file

@ -28,7 +28,6 @@ mod hover_popover;
mod hunk_diff;
mod indent_guides;
mod inlay_hint_cache;
mod inline_completion_provider;
pub mod items;
mod linked_editing_ranges;
mod lsp_ext;
@ -75,7 +74,7 @@ use gpui::{
div, impl_actions, point, prelude::*, px, relative, size, uniform_list, Action, AnyElement,
AppContext, AsyncWindowContext, AvailableSpace, BackgroundExecutor, Bounds, ClipboardEntry,
ClipboardItem, Context, DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusOutEvent,
FocusableView, FontId, FontWeight, HighlightStyle, Hsla, InteractiveText, KeyContext,
FocusableView, FontId, FontWeight, Global, HighlightStyle, Hsla, InteractiveText, KeyContext,
ListSizingBehavior, Model, ModelContext, MouseButton, PaintQuad, ParentElement, Pixels, Render,
ScrollStrategy, SharedString, Size, StrikethroughStyle, Styled, StyledText, Subscription, Task,
TextStyle, TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle, View,
@ -87,7 +86,8 @@ pub(crate) use hunk_diff::HoveredHunk;
use hunk_diff::{diff_hunk_to_display, ExpandedHunks};
use indent_guides::ActiveIndentGuidesState;
use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
pub use inline_completion_provider::*;
pub use inline_completion::Direction;
use inline_completion::{InlayProposal, InlineCompletionProvider, InlineCompletionProviderHandle};
pub use items::MAX_TAB_TITLE_LEN;
use itertools::Itertools;
use language::{
@ -273,12 +273,6 @@ enum DocumentHighlightRead {}
enum DocumentHighlightWrite {}
enum InputComposition {}
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum Direction {
Prev,
Next,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Navigated {
Yes,
@ -7370,7 +7364,7 @@ impl Editor {
.update(cx, |buffer, cx| buffer.edit(edits, None, cx));
}
pub fn cut(&mut self, _: &Cut, cx: &mut ViewContext<Self>) {
pub fn cut_common(&mut self, cx: &mut ViewContext<Self>) -> ClipboardItem {
let mut text = String::new();
let buffer = self.buffer.read(cx).snapshot(cx);
let mut selections = self.selections.all::<Point>(cx);
@ -7414,11 +7408,38 @@ impl Editor {
s.select(selections);
});
this.insert("", cx);
cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata(
text,
clipboard_selections,
));
});
ClipboardItem::new_string_with_json_metadata(text, clipboard_selections)
}
pub fn cut(&mut self, _: &Cut, cx: &mut ViewContext<Self>) {
let item = self.cut_common(cx);
cx.write_to_clipboard(item);
}
pub fn kill_ring_cut(&mut self, _: &KillRingCut, cx: &mut ViewContext<Self>) {
self.change_selections(None, cx, |s| {
s.move_with(|snapshot, sel| {
if sel.is_empty() {
sel.end = DisplayPoint::new(sel.end.row(), snapshot.line_len(sel.end.row()))
}
});
});
let item = self.cut_common(cx);
cx.set_global(KillRing(item))
}
pub fn kill_ring_yank(&mut self, _: &KillRingYank, cx: &mut ViewContext<Self>) {
let (text, metadata) = if let Some(KillRing(item)) = cx.try_global() {
if let Some(ClipboardEntry::String(kill_ring)) = item.entries().first() {
(kill_ring.text().to_string(), kill_ring.metadata_json())
} else {
return;
}
} else {
return;
};
self.do_paste(&text, metadata, false, cx);
}
pub fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
@ -11875,7 +11896,15 @@ impl Editor {
style: &EditorStyle,
cx: &mut WindowContext,
) -> Option<AnyElement> {
if !self.newest_selection_head_on_empty_line(cx) || self.has_active_inline_completion(cx) {
let selection = self.selections.newest::<Point>(cx);
if !selection.is_empty() {
return None;
};
let snapshot = self.buffer.read(cx).snapshot(cx);
let buffer_row = MultiBufferRow(selection.head().row);
if snapshot.line_len(buffer_row) != 0 || self.has_active_inline_completion(cx) {
return None;
}
@ -13782,7 +13811,9 @@ impl CodeActionProvider for Model<Project> {
range: Range<text::Anchor>,
cx: &mut WindowContext,
) -> Task<Result<Vec<CodeAction>>> {
self.update(cx, |project, cx| project.code_actions(buffer, range, cx))
self.update(cx, |project, cx| {
project.code_actions(buffer, range, None, cx)
})
}
fn apply_code_action(
@ -14426,15 +14457,16 @@ impl ViewInputHandler for Editor {
fn text_for_range(
&mut self,
range_utf16: Range<usize>,
adjusted_range: &mut Option<Range<usize>>,
cx: &mut ViewContext<Self>,
) -> Option<String> {
Some(
self.buffer
.read(cx)
.read(cx)
.text_for_range(OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end))
.collect(),
)
let snapshot = self.buffer.read(cx).read(cx);
let start = snapshot.clip_offset_utf16(OffsetUtf16(range_utf16.start), Bias::Left);
let end = snapshot.clip_offset_utf16(OffsetUtf16(range_utf16.end), Bias::Right);
if (start.0..end.0) != range_utf16 {
adjusted_range.replace(start.0..end.0);
}
Some(snapshot.text_for_range(start..end).collect())
}
fn selected_text_range(
@ -15142,4 +15174,7 @@ fn check_multiline_range(buffer: &Buffer, range: Range<usize>) -> Range<usize> {
}
}
pub struct KillRing(ClipboardItem);
impl Global for KillRing {}
const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);

View file

@ -217,6 +217,8 @@ impl EditorElement {
register_action(view, cx, Editor::transpose);
register_action(view, cx, Editor::rewrap);
register_action(view, cx, Editor::cut);
register_action(view, cx, Editor::kill_ring_cut);
register_action(view, cx, Editor::kill_ring_yank);
register_action(view, cx, Editor::copy);
register_action(view, cx, Editor::paste);
register_action(view, cx, Editor::undo);

View file

@ -841,7 +841,7 @@ impl Item for Editor {
self.pixel_position_of_newest_cursor
}
fn breadcrumb_location(&self) -> ToolbarItemLocation {
fn breadcrumb_location(&self, _: &AppContext) -> ToolbarItemLocation {
if self.show_breadcrumbs {
ToolbarItemLocation::PrimaryLeft
} else {
@ -1618,15 +1618,14 @@ fn path_for_file<'a>(
#[cfg(test)]
mod tests {
use crate::editor_tests::init_test;
use fs::Fs;
use super::*;
use fs::MTime;
use gpui::{AppContext, VisualTestContext};
use language::{LanguageMatcher, TestFile};
use project::FakeFs;
use std::{
path::{Path, PathBuf},
time::SystemTime,
};
use std::path::{Path, PathBuf};
#[gpui::test]
fn test_path_for_file(cx: &mut AppContext) {
@ -1679,9 +1678,7 @@ mod tests {
async fn test_deserialize(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let now = SystemTime::now();
let fs = FakeFs::new(cx.executor());
fs.set_next_mtime(now);
fs.insert_file("/file.rs", Default::default()).await;
// Test case 1: Deserialize with path and contents
@ -1690,12 +1687,18 @@ mod tests {
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
let item_id = 1234 as ItemId;
let mtime = fs
.metadata(Path::new("/file.rs"))
.await
.unwrap()
.unwrap()
.mtime;
let serialized_editor = SerializedEditor {
abs_path: Some(PathBuf::from("/file.rs")),
contents: Some("fn main() {}".to_string()),
language: Some("Rust".to_string()),
mtime: Some(now),
mtime: Some(mtime),
};
DB.save_serialized_editor(item_id, workspace_id, serialized_editor.clone())
@ -1792,9 +1795,7 @@ mod tests {
let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
let item_id = 9345 as ItemId;
let old_mtime = now
.checked_sub(std::time::Duration::from_secs(60 * 60 * 24))
.unwrap();
let old_mtime = MTime::from_seconds_and_nanos(0, 50);
let serialized_editor = SerializedEditor {
abs_path: Some(PathBuf::from("/file.rs")),
contents: Some("fn main() {}".to_string()),

View file

@ -1,8 +1,8 @@
use anyhow::Result;
use db::sqlez::bindable::{Bind, Column, StaticColumnCount};
use db::sqlez::statement::Statement;
use fs::MTime;
use std::path::PathBuf;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use db::sqlez_macros::sql;
use db::{define_connection, query};
@ -14,7 +14,7 @@ pub(crate) struct SerializedEditor {
pub(crate) abs_path: Option<PathBuf>,
pub(crate) contents: Option<String>,
pub(crate) language: Option<String>,
pub(crate) mtime: Option<SystemTime>,
pub(crate) mtime: Option<MTime>,
}
impl StaticColumnCount for SerializedEditor {
@ -29,16 +29,13 @@ impl Bind for SerializedEditor {
let start_index = statement.bind(&self.contents, start_index)?;
let start_index = statement.bind(&self.language, start_index)?;
let mtime = self.mtime.and_then(|mtime| {
mtime
.duration_since(UNIX_EPOCH)
.ok()
.map(|duration| (duration.as_secs() as i64, duration.subsec_nanos() as i32))
});
let start_index = match mtime {
let start_index = match self
.mtime
.and_then(|mtime| mtime.to_seconds_and_nanos_for_persistence())
{
Some((seconds, nanos)) => {
let start_index = statement.bind(&seconds, start_index)?;
statement.bind(&nanos, start_index)?
let start_index = statement.bind(&(seconds as i64), start_index)?;
statement.bind(&(nanos as i32), start_index)?
}
None => {
let start_index = statement.bind::<Option<i64>>(&None, start_index)?;
@ -64,7 +61,7 @@ impl Column for SerializedEditor {
let mtime = mtime_seconds
.zip(mtime_nanos)
.map(|(seconds, nanos)| UNIX_EPOCH + Duration::new(seconds as u64, nanos as u32));
.map(|(seconds, nanos)| MTime::from_seconds_and_nanos(seconds as u64, nanos as u32));
let editor = Self {
abs_path,
@ -280,12 +277,11 @@ mod tests {
assert_eq!(have, serialized_editor);
// Storing and retrieving mtime
let now = SystemTime::now();
let serialized_editor = SerializedEditor {
abs_path: None,
contents: None,
language: None,
mtime: Some(now),
mtime: Some(MTime::from_seconds_and_nanos(100, 42)),
};
DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())

View file

@ -30,9 +30,10 @@ languages.workspace = true
node_runtime.workspace = true
open_ai.workspace = true
project.workspace = true
reqwest_client.workspace = true
semantic_index.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
smol.workspace = true
reqwest_client.workspace = true
util.workspace = true

View file

@ -27,7 +27,7 @@ use std::time::Duration;
use std::{
fs,
path::Path,
process::{exit, Command, Stdio},
process::{exit, Stdio},
sync::{
atomic::{AtomicUsize, Ordering::SeqCst},
Arc,
@ -667,7 +667,7 @@ async fn fetch_eval_repo(
return;
}
if !repo_dir.join(".git").exists() {
let init_output = Command::new("git")
let init_output = util::command::new_std_command("git")
.current_dir(&repo_dir)
.args(&["init"])
.output()
@ -682,13 +682,13 @@ async fn fetch_eval_repo(
}
}
let url = format!("https://github.com/{}.git", repo);
Command::new("git")
util::command::new_std_command("git")
.current_dir(&repo_dir)
.args(&["remote", "add", "-f", "origin", &url])
.stdin(Stdio::null())
.output()
.unwrap();
let fetch_output = Command::new("git")
let fetch_output = util::command::new_std_command("git")
.current_dir(&repo_dir)
.args(&["fetch", "--depth", "1", "origin", &sha])
.stdin(Stdio::null())
@ -703,7 +703,7 @@ async fn fetch_eval_repo(
);
return;
}
let checkout_output = Command::new("git")
let checkout_output = util::command::new_std_command("git")
.current_dir(&repo_dir)
.args(&["checkout", &sha])
.output()

View file

@ -28,6 +28,7 @@ semantic_version.workspace = true
serde.workspace = true
serde_json.workspace = true
toml.workspace = true
util.workspace = true
wasm-encoder.workspace = true
wasmparser.workspace = true
wit-component.workspace = true

View file

@ -11,7 +11,7 @@ use serde::Deserialize;
use std::{
env, fs, mem,
path::{Path, PathBuf},
process::{Command, Stdio},
process::Stdio,
sync::Arc,
};
use wasm_encoder::{ComponentSectionId, Encode as _, RawSection, Section as _};
@ -130,7 +130,7 @@ impl ExtensionBuilder {
"compiling Rust crate for extension {}",
extension_dir.display()
);
let output = Command::new("cargo")
let output = util::command::new_std_command("cargo")
.args(["build", "--target", RUST_TARGET])
.args(options.release.then_some("--release"))
.arg("--target-dir")
@ -237,7 +237,7 @@ impl ExtensionBuilder {
let scanner_path = src_path.join("scanner.c");
log::info!("compiling {grammar_name} parser");
let clang_output = Command::new(&clang_path)
let clang_output = util::command::new_std_command(&clang_path)
.args(["-fPIC", "-shared", "-Os"])
.arg(format!("-Wl,--export=tree_sitter_{grammar_name}"))
.arg("-o")
@ -264,7 +264,7 @@ impl ExtensionBuilder {
let git_dir = directory.join(".git");
if directory.exists() {
let remotes_output = Command::new("git")
let remotes_output = util::command::new_std_command("git")
.arg("--git-dir")
.arg(&git_dir)
.args(["remote", "-v"])
@ -287,7 +287,7 @@ impl ExtensionBuilder {
fs::create_dir_all(directory).with_context(|| {
format!("failed to create grammar directory {}", directory.display(),)
})?;
let init_output = Command::new("git")
let init_output = util::command::new_std_command("git")
.arg("init")
.current_dir(directory)
.output()?;
@ -298,7 +298,7 @@ impl ExtensionBuilder {
);
}
let remote_add_output = Command::new("git")
let remote_add_output = util::command::new_std_command("git")
.arg("--git-dir")
.arg(&git_dir)
.args(["remote", "add", "origin", url])
@ -312,14 +312,14 @@ impl ExtensionBuilder {
}
}
let fetch_output = Command::new("git")
let fetch_output = util::command::new_std_command("git")
.arg("--git-dir")
.arg(&git_dir)
.args(["fetch", "--depth", "1", "origin", rev])
.output()
.context("failed to execute `git fetch`")?;
let checkout_output = Command::new("git")
let checkout_output = util::command::new_std_command("git")
.arg("--git-dir")
.arg(&git_dir)
.args(["checkout", rev])
@ -346,7 +346,7 @@ impl ExtensionBuilder {
}
fn install_rust_wasm_target_if_needed(&self) -> Result<()> {
let rustc_output = Command::new("rustc")
let rustc_output = util::command::new_std_command("rustc")
.arg("--print")
.arg("sysroot")
.output()
@ -363,7 +363,7 @@ impl ExtensionBuilder {
return Ok(());
}
let output = Command::new("rustup")
let output = util::command::new_std_command("rustup")
.args(["target", "add", RUST_TARGET])
.stderr(Stdio::piped())
.stdout(Stdio::inherit())

View file

@ -34,6 +34,7 @@ lsp.workspace = true
node_runtime.workspace = true
paths.workspace = true
project.workspace = true
remote.workspace = true
release_channel.workspace = true
schemars.workspace = true
semantic_version.workspace = true
@ -42,6 +43,7 @@ serde_json.workspace = true
serde_json_lenient.workspace = true
settings.workspace = true
task.workspace = true
tempfile.workspace = true
toml.workspace = true
url.workspace = true
util.workspace = true

View file

@ -1,16 +1,16 @@
pub mod extension_lsp_adapter;
pub mod extension_settings;
pub mod headless_host;
pub mod wasm_host;
#[cfg(test)]
mod extension_store_test;
use crate::extension_lsp_adapter::ExtensionLspAdapter;
use anyhow::{anyhow, bail, Context as _, Result};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use client::{telemetry::Telemetry, Client, ExtensionMetadata, GetExtensionsResponse};
use collections::{btree_map, BTreeMap, HashSet};
use client::{proto, telemetry::Telemetry, Client, ExtensionMetadata, GetExtensionsResponse};
use collections::{btree_map, BTreeMap, HashMap, HashSet};
use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder};
use extension::Extension;
pub use extension::ExtensionManifest;
@ -36,6 +36,7 @@ use lsp::LanguageServerName;
use node_runtime::NodeRuntime;
use project::ContextProviderWithTasks;
use release_channel::ReleaseChannel;
use remote::SshRemoteClient;
use semantic_version::SemanticVersion;
use serde::{Deserialize, Serialize};
use settings::Settings;
@ -120,7 +121,13 @@ pub trait ExtensionRegistrationHooks: Send + Sync + 'static {
) {
}
fn register_lsp_adapter(&self, _language: LanguageName, _adapter: ExtensionLspAdapter) {}
fn register_lsp_adapter(
&self,
_extension: Arc<dyn Extension>,
_language_server_id: LanguageServerName,
_language: LanguageName,
) {
}
fn remove_lsp_adapter(&self, _language: &LanguageName, _server_name: &LanguageServerName) {}
@ -178,6 +185,8 @@ pub struct ExtensionStore {
pub wasm_host: Arc<WasmHost>,
pub wasm_extensions: Vec<(Arc<ExtensionManifest>, WasmExtension)>,
pub tasks: Vec<Task<()>>,
pub ssh_clients: HashMap<String, WeakModel<SshRemoteClient>>,
pub ssh_registered_tx: UnboundedSender<()>,
}
#[derive(Clone, Copy)]
@ -289,6 +298,7 @@ impl ExtensionStore {
let index_path = extensions_dir.join("index.json");
let (reload_tx, mut reload_rx) = unbounded();
let (connection_registered_tx, mut connection_registered_rx) = unbounded();
let mut this = Self {
registration_hooks: extension_api.clone(),
extension_index: Default::default(),
@ -312,6 +322,9 @@ impl ExtensionStore {
telemetry,
reload_tx,
tasks: Vec::new(),
ssh_clients: HashMap::default(),
ssh_registered_tx: connection_registered_tx,
};
// The extensions store maintains an index file, which contains a complete
@ -337,7 +350,10 @@ impl ExtensionStore {
if let (Ok(Some(index_metadata)), Ok(Some(extensions_metadata))) =
(index_metadata, extensions_metadata)
{
if index_metadata.mtime > extensions_metadata.mtime {
if index_metadata
.mtime
.bad_is_greater_than(extensions_metadata.mtime)
{
extension_index_needs_rebuild = false;
}
}
@ -386,6 +402,14 @@ impl ExtensionStore {
.await;
index_changed = false;
}
Self::update_ssh_clients(&this, &mut cx).await?;
}
_ = connection_registered_rx.next() => {
debounce_timer = cx
.background_executor()
.timer(RELOAD_DEBOUNCE_DURATION)
.fuse();
}
extension_id = reload_rx.next() => {
let Some(extension_id) = extension_id else { break; };
@ -1236,12 +1260,9 @@ impl ExtensionStore {
for (language_server_id, language_server_config) in &manifest.language_servers {
for language in language_server_config.languages() {
this.registration_hooks.register_lsp_adapter(
extension.clone(),
language_server_id.clone(),
language.clone(),
ExtensionLspAdapter {
extension: extension.clone(),
language_server_id: language_server_id.clone(),
language_name: language.clone(),
},
);
}
}
@ -1431,6 +1452,144 @@ impl ExtensionStore {
Ok(())
}
fn prepare_remote_extension(
&mut self,
extension_id: Arc<str>,
tmp_dir: PathBuf,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let src_dir = self.extensions_dir().join(extension_id.as_ref());
let Some(loaded_extension) = self.extension_index.extensions.get(&extension_id).cloned()
else {
return Task::ready(Err(anyhow!("extension no longer installed")));
};
let fs = self.fs.clone();
cx.background_executor().spawn(async move {
for well_known_path in ["extension.toml", "extension.json", "extension.wasm"] {
if fs.is_file(&src_dir.join(well_known_path)).await {
fs.copy_file(
&src_dir.join(well_known_path),
&tmp_dir.join(well_known_path),
fs::CopyOptions::default(),
)
.await?
}
}
for language_path in loaded_extension.manifest.languages.iter() {
if fs
.is_file(&src_dir.join(language_path).join("config.toml"))
.await
{
fs.create_dir(&tmp_dir.join(language_path)).await?;
fs.copy_file(
&src_dir.join(language_path).join("config.toml"),
&tmp_dir.join(language_path).join("config.toml"),
fs::CopyOptions::default(),
)
.await?
}
}
Ok(())
})
}
async fn sync_extensions_over_ssh(
this: &WeakModel<Self>,
client: WeakModel<SshRemoteClient>,
cx: &mut AsyncAppContext,
) -> Result<()> {
let extensions = this.update(cx, |this, _cx| {
this.extension_index
.extensions
.iter()
.filter_map(|(id, entry)| {
if entry.manifest.language_servers.is_empty() {
return None;
}
Some(proto::Extension {
id: id.to_string(),
version: entry.manifest.version.to_string(),
dev: entry.dev,
})
})
.collect()
})?;
let response = client
.update(cx, |client, _cx| {
client
.proto_client()
.request(proto::SyncExtensions { extensions })
})?
.await?;
for missing_extension in response.missing_extensions.into_iter() {
let tmp_dir = tempfile::tempdir()?;
this.update(cx, |this, cx| {
this.prepare_remote_extension(
missing_extension.id.clone().into(),
tmp_dir.path().to_owned(),
cx,
)
})?
.await?;
let dest_dir = PathBuf::from(&response.tmp_dir).join(missing_extension.clone().id);
log::info!("Uploading extension {}", missing_extension.clone().id);
client
.update(cx, |client, cx| {
client.upload_directory(tmp_dir.path().to_owned(), dest_dir.clone(), cx)
})?
.await?;
client
.update(cx, |client, _cx| {
client.proto_client().request(proto::InstallExtension {
tmp_dir: dest_dir.to_string_lossy().to_string(),
extension: Some(missing_extension),
})
})?
.await?;
}
anyhow::Ok(())
}
pub async fn update_ssh_clients(
this: &WeakModel<Self>,
cx: &mut AsyncAppContext,
) -> Result<()> {
let clients = this.update(cx, |this, _cx| {
this.ssh_clients.retain(|_k, v| v.upgrade().is_some());
this.ssh_clients.values().cloned().collect::<Vec<_>>()
})?;
for client in clients {
Self::sync_extensions_over_ssh(&this, client, cx)
.await
.log_err();
}
anyhow::Ok(())
}
pub fn register_ssh_client(
&mut self,
client: Model<SshRemoteClient>,
cx: &mut ModelContext<Self>,
) {
let connection_options = client.read(cx).connection_options();
if self.ssh_clients.contains_key(&connection_options.ssh_url()) {
return;
}
self.ssh_clients
.insert(connection_options.ssh_url(), client.downgrade());
self.ssh_registered_tx.unbounded_send(()).ok();
}
}
fn load_plugin_queries(root_path: &Path) -> LanguageQueries {

View file

@ -45,9 +45,23 @@ impl WorktreeDelegate for WorktreeDelegateAdapter {
}
pub struct ExtensionLspAdapter {
pub(crate) extension: Arc<dyn Extension>,
pub(crate) language_server_id: LanguageServerName,
pub(crate) language_name: LanguageName,
extension: Arc<dyn Extension>,
language_server_id: LanguageServerName,
language_name: LanguageName,
}
impl ExtensionLspAdapter {
pub fn new(
extension: Arc<dyn Extension>,
language_server_id: LanguageServerName,
language_name: LanguageName,
) -> Self {
Self {
extension,
language_server_id,
language_name,
}
}
}
#[async_trait(?Send)]

View file

@ -7,11 +7,14 @@ use crate::{
use anyhow::Result;
use async_compression::futures::bufread::GzipEncoder;
use collections::BTreeMap;
use extension::Extension;
use fs::{FakeFs, Fs, RealFs};
use futures::{io::BufReader, AsyncReadExt, StreamExt};
use gpui::{BackgroundExecutor, Context, SemanticVersion, SharedString, Task, TestAppContext};
use http_client::{FakeHttpClient, Response};
use language::{LanguageMatcher, LanguageRegistry, LanguageServerBinaryStatus, LoadedLanguage};
use language::{
LanguageMatcher, LanguageName, LanguageRegistry, LanguageServerBinaryStatus, LoadedLanguage,
};
use lsp::LanguageServerName;
use node_runtime::NodeRuntime;
use parking_lot::Mutex;
@ -80,11 +83,18 @@ impl ExtensionRegistrationHooks for TestExtensionRegistrationHooks {
fn register_lsp_adapter(
&self,
language_name: language::LanguageName,
adapter: ExtensionLspAdapter,
extension: Arc<dyn Extension>,
language_server_id: LanguageServerName,
language: LanguageName,
) {
self.language_registry
.register_lsp_adapter(language_name, Arc::new(adapter));
self.language_registry.register_lsp_adapter(
language.clone(),
Arc::new(ExtensionLspAdapter::new(
extension,
language_server_id,
language,
)),
);
}
fn update_lsp_status(

View file

@ -0,0 +1,388 @@
use std::{path::PathBuf, sync::Arc};
use anyhow::{anyhow, Context as _, Result};
use client::{proto, TypedEnvelope};
use collections::{HashMap, HashSet};
use extension::{Extension, ExtensionManifest};
use fs::{Fs, RemoveOptions, RenameOptions};
use gpui::{AppContext, AsyncAppContext, Context, Model, ModelContext, Task, WeakModel};
use http_client::HttpClient;
use language::{LanguageConfig, LanguageName, LanguageQueries, LanguageRegistry, LoadedLanguage};
use lsp::LanguageServerName;
use node_runtime::NodeRuntime;
use crate::{
extension_lsp_adapter::ExtensionLspAdapter,
wasm_host::{WasmExtension, WasmHost},
ExtensionRegistrationHooks,
};
pub struct HeadlessExtensionStore {
pub registration_hooks: Arc<dyn ExtensionRegistrationHooks>,
pub fs: Arc<dyn Fs>,
pub extension_dir: PathBuf,
pub wasm_host: Arc<WasmHost>,
pub loaded_extensions: HashMap<Arc<str>, Arc<str>>,
pub loaded_languages: HashMap<Arc<str>, Vec<LanguageName>>,
pub loaded_language_servers: HashMap<Arc<str>, Vec<(LanguageServerName, LanguageName)>>,
}
#[derive(Clone, Debug)]
pub struct ExtensionVersion {
pub id: String,
pub version: String,
pub dev: bool,
}
impl HeadlessExtensionStore {
pub fn new(
fs: Arc<dyn Fs>,
http_client: Arc<dyn HttpClient>,
languages: Arc<LanguageRegistry>,
extension_dir: PathBuf,
node_runtime: NodeRuntime,
cx: &mut AppContext,
) -> Model<Self> {
let registration_hooks = Arc::new(HeadlessRegistrationHooks::new(languages.clone()));
cx.new_model(|cx| Self {
registration_hooks: registration_hooks.clone(),
fs: fs.clone(),
wasm_host: WasmHost::new(
fs.clone(),
http_client.clone(),
node_runtime,
registration_hooks,
extension_dir.join("work"),
cx,
),
extension_dir,
loaded_extensions: Default::default(),
loaded_languages: Default::default(),
loaded_language_servers: Default::default(),
})
}
pub fn sync_extensions(
&mut self,
extensions: Vec<ExtensionVersion>,
cx: &ModelContext<Self>,
) -> Task<Result<Vec<ExtensionVersion>>> {
let on_client = HashSet::from_iter(extensions.iter().map(|e| e.id.as_str()));
let to_remove: Vec<Arc<str>> = self
.loaded_extensions
.keys()
.filter(|id| !on_client.contains(id.as_ref()))
.cloned()
.collect();
let to_load: Vec<ExtensionVersion> = extensions
.into_iter()
.filter(|e| {
if e.dev {
return true;
}
!self
.loaded_extensions
.get(e.id.as_str())
.is_some_and(|loaded| loaded.as_ref() == e.version.as_str())
})
.collect();
cx.spawn(|this, mut cx| async move {
let mut missing = Vec::new();
for extension_id in to_remove {
log::info!("removing extension: {}", extension_id);
this.update(&mut cx, |this, cx| {
this.uninstall_extension(&extension_id, cx)
})?
.await?;
}
for extension in to_load {
if let Err(e) = Self::load_extension(this.clone(), extension.clone(), &mut cx).await
{
log::info!("failed to load extension: {}, {:?}", extension.id, e);
missing.push(extension)
} else if extension.dev {
missing.push(extension)
}
}
Ok(missing)
})
}
pub async fn load_extension(
this: WeakModel<Self>,
extension: ExtensionVersion,
cx: &mut AsyncAppContext,
) -> Result<()> {
let (fs, wasm_host, extension_dir) = this.update(cx, |this, _cx| {
this.loaded_extensions.insert(
extension.id.clone().into(),
extension.version.clone().into(),
);
(
this.fs.clone(),
this.wasm_host.clone(),
this.extension_dir.join(&extension.id),
)
})?;
let manifest = Arc::new(ExtensionManifest::load(fs.clone(), &extension_dir).await?);
debug_assert!(!manifest.languages.is_empty() || !manifest.language_servers.is_empty());
if manifest.version.as_ref() != extension.version.as_str() {
anyhow::bail!(
"mismatched versions: ({}) != ({})",
manifest.version,
extension.version
)
}
for language_path in &manifest.languages {
let language_path = extension_dir.join(language_path);
let config = fs.load(&language_path.join("config.toml")).await?;
let mut config = ::toml::from_str::<LanguageConfig>(&config)?;
this.update(cx, |this, _cx| {
this.loaded_languages
.entry(manifest.id.clone())
.or_default()
.push(config.name.clone());
config.grammar = None;
this.registration_hooks.register_language(
config.name.clone(),
None,
config.matcher.clone(),
Arc::new(move || {
Ok(LoadedLanguage {
config: config.clone(),
queries: LanguageQueries::default(),
context_provider: None,
toolchain_provider: None,
})
}),
);
})?;
}
if manifest.language_servers.is_empty() {
return Ok(());
}
let wasm_extension: Arc<dyn Extension> =
Arc::new(WasmExtension::load(extension_dir, &manifest, wasm_host.clone(), &cx).await?);
for (language_server_id, language_server_config) in &manifest.language_servers {
for language in language_server_config.languages() {
this.update(cx, |this, _cx| {
this.loaded_language_servers
.entry(manifest.id.clone())
.or_default()
.push((language_server_id.clone(), language.clone()));
this.registration_hooks.register_lsp_adapter(
wasm_extension.clone(),
language_server_id.clone(),
language.clone(),
);
})?;
}
}
Ok(())
}
fn uninstall_extension(
&mut self,
extension_id: &Arc<str>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
self.loaded_extensions.remove(extension_id);
let languages_to_remove = self
.loaded_languages
.remove(extension_id)
.unwrap_or_default();
self.registration_hooks
.remove_languages(&languages_to_remove, &[]);
for (language_server_name, language) in self
.loaded_language_servers
.remove(extension_id)
.unwrap_or_default()
{
self.registration_hooks
.remove_lsp_adapter(&language, &language_server_name);
}
let path = self.extension_dir.join(&extension_id.to_string());
let fs = self.fs.clone();
cx.spawn(|_, _| async move {
fs.remove_dir(
&path,
RemoveOptions {
recursive: true,
ignore_if_not_exists: true,
},
)
.await
})
}
pub fn install_extension(
&mut self,
extension: ExtensionVersion,
tmp_path: PathBuf,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let path = self.extension_dir.join(&extension.id);
let fs = self.fs.clone();
cx.spawn(|this, mut cx| async move {
if fs.is_dir(&path).await {
this.update(&mut cx, |this, cx| {
this.uninstall_extension(&extension.id.clone().into(), cx)
})?
.await?;
}
fs.rename(&tmp_path, &path, RenameOptions::default())
.await?;
Self::load_extension(this, extension, &mut cx).await
})
}
pub async fn handle_sync_extensions(
extension_store: Model<HeadlessExtensionStore>,
envelope: TypedEnvelope<proto::SyncExtensions>,
mut cx: AsyncAppContext,
) -> Result<proto::SyncExtensionsResponse> {
let requested_extensions =
envelope
.payload
.extensions
.into_iter()
.map(|p| ExtensionVersion {
id: p.id,
version: p.version,
dev: p.dev,
});
let missing_extensions = extension_store
.update(&mut cx, |extension_store, cx| {
extension_store.sync_extensions(requested_extensions.collect(), cx)
})?
.await?;
Ok(proto::SyncExtensionsResponse {
missing_extensions: missing_extensions
.into_iter()
.map(|e| proto::Extension {
id: e.id,
version: e.version,
dev: e.dev,
})
.collect(),
tmp_dir: paths::remote_extensions_uploads_dir()
.to_string_lossy()
.to_string(),
})
}
pub async fn handle_install_extension(
extensions: Model<HeadlessExtensionStore>,
envelope: TypedEnvelope<proto::InstallExtension>,
mut cx: AsyncAppContext,
) -> Result<proto::Ack> {
let extension = envelope
.payload
.extension
.with_context(|| anyhow!("Invalid InstallExtension request"))?;
extensions
.update(&mut cx, |extensions, cx| {
extensions.install_extension(
ExtensionVersion {
id: extension.id,
version: extension.version,
dev: extension.dev,
},
PathBuf::from(envelope.payload.tmp_dir),
cx,
)
})?
.await?;
Ok(proto::Ack {})
}
}
struct HeadlessRegistrationHooks {
language_registry: Arc<LanguageRegistry>,
}
impl HeadlessRegistrationHooks {
fn new(language_registry: Arc<LanguageRegistry>) -> Self {
Self { language_registry }
}
}
impl ExtensionRegistrationHooks for HeadlessRegistrationHooks {
fn register_language(
&self,
language: LanguageName,
_grammar: Option<Arc<str>>,
matcher: language::LanguageMatcher,
load: Arc<dyn Fn() -> Result<LoadedLanguage> + 'static + Send + Sync>,
) {
log::info!("registering language: {:?}", language);
self.language_registry
.register_language(language, None, matcher, load)
}
fn register_lsp_adapter(
&self,
extension: Arc<dyn Extension>,
language_server_id: LanguageServerName,
language: LanguageName,
) {
log::info!("registering lsp adapter {:?}", language);
self.language_registry.register_lsp_adapter(
language.clone(),
Arc::new(ExtensionLspAdapter::new(
extension,
language_server_id,
language,
)),
);
}
fn register_wasm_grammars(&self, grammars: Vec<(Arc<str>, PathBuf)>) {
self.language_registry.register_wasm_grammars(grammars)
}
fn remove_lsp_adapter(&self, language: &LanguageName, server_name: &LanguageServerName) {
self.language_registry
.remove_lsp_adapter(language, server_name)
}
fn remove_languages(
&self,
languages_to_remove: &[LanguageName],
_grammars_to_remove: &[Arc<str>],
) {
self.language_registry
.remove_languages(languages_to_remove, &[])
}
fn update_lsp_status(
&self,
server_name: LanguageServerName,
status: language::LanguageServerBinaryStatus,
) {
self.language_registry
.update_lsp_status(server_name, status)
}
}

View file

@ -39,12 +39,12 @@ smallvec.workspace = true
snippet_provider.workspace = true
telemetry.workspace = true
theme.workspace = true
theme_selector.workspace = true
ui.workspace = true
util.workspace = true
vim.workspace = true
vim_mode_setting.workspace = true
wasmtime-wasi.workspace = true
workspace.workspace = true
zed_actions.workspace = true
[dev-dependencies]
editor = { workspace = true, features = ["test-support"] }

View file

@ -11,7 +11,8 @@ use extension_host::{extension_lsp_adapter::ExtensionLspAdapter, wasm_host};
use fs::Fs;
use gpui::{AppContext, BackgroundExecutor, Model, Task};
use indexed_docs::{ExtensionIndexedDocsProvider, IndexedDocsRegistry, ProviderId};
use language::{LanguageRegistry, LanguageServerBinaryStatus, LoadedLanguage};
use language::{LanguageName, LanguageRegistry, LanguageServerBinaryStatus, LoadedLanguage};
use lsp::LanguageServerName;
use snippet_provider::SnippetRegistry;
use theme::{ThemeRegistry, ThemeSettings};
use ui::SharedString;
@ -159,11 +160,18 @@ impl extension_host::ExtensionRegistrationHooks for ConcreteExtensionRegistratio
fn register_lsp_adapter(
&self,
language_name: language::LanguageName,
adapter: ExtensionLspAdapter,
extension: Arc<dyn Extension>,
language_server_id: LanguageServerName,
language: LanguageName,
) {
self.language_registry
.register_lsp_adapter(language_name, Arc::new(adapter));
self.language_registry.register_lsp_adapter(
language.clone(),
Arc::new(ExtensionLspAdapter::new(
extension,
language_server_id,
language,
)),
);
}
fn remove_lsp_adapter(

View file

@ -17,9 +17,9 @@ use editor::{Editor, EditorElement, EditorStyle};
use extension_host::{ExtensionManifest, ExtensionOperation, ExtensionStore};
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
actions, uniform_list, AppContext, EventEmitter, Flatten, FocusableView, InteractiveElement,
KeyContext, ParentElement, Render, Styled, Task, TextStyle, UniformListScrollHandle, View,
ViewContext, VisualContext, WeakView, WindowContext,
actions, uniform_list, Action, AppContext, EventEmitter, Flatten, FocusableView,
InteractiveElement, KeyContext, ParentElement, Render, Styled, Task, TextStyle,
UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WindowContext,
};
use num_format::{Locale, ToFormattedString};
use project::DirectoryLister;
@ -27,7 +27,7 @@ use release_channel::ReleaseChannel;
use settings::Settings;
use theme::ThemeSettings;
use ui::{prelude::*, CheckboxWithLabel, ContextMenu, PopoverMenu, ToggleButton, Tooltip};
use vim::VimModeSetting;
use vim_mode_setting::VimModeSetting;
use workspace::{
item::{Item, ItemEvent},
Workspace, WorkspaceId,
@ -38,12 +38,12 @@ use crate::extension_version_selector::{
ExtensionVersionSelector, ExtensionVersionSelectorDelegate,
};
actions!(zed, [Extensions, InstallDevExtension]);
actions!(zed, [InstallDevExtension]);
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(move |workspace: &mut Workspace, cx| {
workspace
.register_action(move |workspace, _: &Extensions, cx| {
.register_action(move |workspace, _: &zed_actions::Extensions, cx| {
let existing = workspace
.active_pane()
.read(cx)
@ -254,14 +254,13 @@ impl ExtensionsPage {
.collect::<Vec<_>>();
if !themes.is_empty() {
workspace
.update(cx, |workspace, cx| {
theme_selector::toggle(
workspace,
&theme_selector::Toggle {
.update(cx, |_workspace, cx| {
cx.dispatch_action(
zed_actions::theme_selector::Toggle {
themes_filter: Some(themes),
},
cx,
)
}
.boxed_clone(),
);
})
.ok();
}

View file

@ -22,8 +22,8 @@ db.workspace = true
editor.workspace = true
futures.workspace = true
gpui.workspace = true
human_bytes = "0.4.1"
http_client.workspace = true
human_bytes = "0.4.1"
language.workspace = true
log.workspace = true
menu.workspace = true
@ -39,6 +39,7 @@ ui.workspace = true
urlencoding = "2.1.2"
util.workspace = true
workspace.workspace = true
zed_actions.workspace = true
[dev-dependencies]
editor = { workspace = true, features = ["test-support"] }

View file

@ -5,8 +5,6 @@ use workspace::Workspace;
pub mod feedback_modal;
actions!(feedback, [GiveFeedback, SubmitFeedback]);
mod system_specs;
actions!(

View file

@ -18,8 +18,9 @@ use serde_derive::Serialize;
use ui::{prelude::*, Button, ButtonStyle, IconPosition, Tooltip};
use util::ResultExt;
use workspace::{DismissDecision, ModalView, Workspace};
use zed_actions::feedback::GiveFeedback;
use crate::{system_specs::SystemSpecs, GiveFeedback, OpenZedRepo};
use crate::{system_specs::SystemSpecs, OpenZedRepo};
// For UI testing purposes
const SEND_SUCCESS_IN_DEV_MODE: bool = true;

View file

@ -24,6 +24,7 @@ libc.workspace = true
parking_lot.workspace = true
paths.workspace = true
rope.workspace = true
proto.workspace = true
serde.workspace = true
serde_json.workspace = true
smol.workspace = true

View file

@ -27,13 +27,14 @@ use futures::{future::BoxFuture, AsyncRead, Stream, StreamExt};
use git::repository::{GitRepository, RealGitRepository};
use gpui::{AppContext, Global, ReadGlobal};
use rope::Rope;
use serde::{Deserialize, Serialize};
use smol::io::AsyncWriteExt;
use std::{
io::{self, Write},
path::{Component, Path, PathBuf},
pin::Pin,
sync::Arc,
time::{Duration, SystemTime},
time::{Duration, SystemTime, UNIX_EPOCH},
};
use tempfile::{NamedTempFile, TempDir};
use text::LineEnding;
@ -179,13 +180,62 @@ pub struct RemoveOptions {
#[derive(Copy, Clone, Debug)]
pub struct Metadata {
pub inode: u64,
pub mtime: SystemTime,
pub mtime: MTime,
pub is_symlink: bool,
pub is_dir: bool,
pub len: u64,
pub is_fifo: bool,
}
/// Filesystem modification time. The purpose of this newtype is to discourage use of operations
/// that do not make sense for mtimes. In particular, it is not always valid to compare mtimes using
/// `<` or `>`, as there are many things that can cause the mtime of a file to be earlier than it
/// was. See ["mtime comparison considered harmful" - apenwarr](https://apenwarr.ca/log/20181113).
///
/// Do not derive Ord, PartialOrd, or arithmetic operation traits.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
#[serde(transparent)]
pub struct MTime(SystemTime);
impl MTime {
/// Conversion intended for persistence and testing.
pub fn from_seconds_and_nanos(secs: u64, nanos: u32) -> Self {
MTime(UNIX_EPOCH + Duration::new(secs, nanos))
}
/// Conversion intended for persistence.
pub fn to_seconds_and_nanos_for_persistence(self) -> Option<(u64, u32)> {
self.0
.duration_since(UNIX_EPOCH)
.ok()
.map(|duration| (duration.as_secs(), duration.subsec_nanos()))
}
/// Returns the value wrapped by this `MTime`, for presentation to the user. The name including
/// "_for_user" is to discourage misuse - this method should not be used when making decisions
/// about file dirtiness.
pub fn timestamp_for_user(self) -> SystemTime {
self.0
}
/// Temporary method to split out the behavior changes from introduction of this newtype.
pub fn bad_is_greater_than(self, other: MTime) -> bool {
self.0 > other.0
}
}
impl From<proto::Timestamp> for MTime {
fn from(timestamp: proto::Timestamp) -> Self {
MTime(timestamp.into())
}
}
impl From<MTime> for proto::Timestamp {
fn from(mtime: MTime) -> Self {
mtime.0.into()
}
}
#[derive(Default)]
pub struct RealFs {
git_hosting_provider_registry: Arc<GitHostingProviderRegistry>,
@ -558,7 +608,7 @@ impl Fs for RealFs {
Ok(Some(Metadata {
inode,
mtime: metadata.modified().unwrap(),
mtime: MTime(metadata.modified().unwrap()),
len: metadata.len(),
is_symlink,
is_dir: metadata.file_type().is_dir(),
@ -818,13 +868,13 @@ struct FakeFsState {
enum FakeFsEntry {
File {
inode: u64,
mtime: SystemTime,
mtime: MTime,
len: u64,
content: Vec<u8>,
},
Dir {
inode: u64,
mtime: SystemTime,
mtime: MTime,
len: u64,
entries: BTreeMap<String, Arc<Mutex<FakeFsEntry>>>,
git_repo_state: Option<Arc<Mutex<git::repository::FakeGitRepositoryState>>>,
@ -836,6 +886,18 @@ enum FakeFsEntry {
#[cfg(any(test, feature = "test-support"))]
impl FakeFsState {
fn get_and_increment_mtime(&mut self) -> MTime {
let mtime = self.next_mtime;
self.next_mtime += FakeFs::SYSTEMTIME_INTERVAL;
MTime(mtime)
}
fn get_and_increment_inode(&mut self) -> u64 {
let inode = self.next_inode;
self.next_inode += 1;
inode
}
fn read_path(&self, target: &Path) -> Result<Arc<Mutex<FakeFsEntry>>> {
Ok(self
.try_read_path(target, true)
@ -959,7 +1021,7 @@ pub static FS_DOT_GIT: std::sync::LazyLock<&'static OsStr> =
impl FakeFs {
/// We need to use something large enough for Windows and Unix to consider this a new file.
/// https://doc.rust-lang.org/nightly/std/time/struct.SystemTime.html#platform-specific-behavior
const SYSTEMTIME_INTERVAL: u64 = 100;
const SYSTEMTIME_INTERVAL: Duration = Duration::from_nanos(100);
pub fn new(executor: gpui::BackgroundExecutor) -> Arc<Self> {
let (tx, mut rx) = smol::channel::bounded::<PathBuf>(10);
@ -969,13 +1031,13 @@ impl FakeFs {
state: Mutex::new(FakeFsState {
root: Arc::new(Mutex::new(FakeFsEntry::Dir {
inode: 0,
mtime: SystemTime::UNIX_EPOCH,
mtime: MTime(UNIX_EPOCH),
len: 0,
entries: Default::default(),
git_repo_state: None,
})),
git_event_tx: tx,
next_mtime: SystemTime::UNIX_EPOCH,
next_mtime: UNIX_EPOCH + Self::SYSTEMTIME_INTERVAL,
next_inode: 1,
event_txs: Default::default(),
buffered_events: Vec::new(),
@ -1007,13 +1069,16 @@ impl FakeFs {
state.next_mtime = next_mtime;
}
pub fn get_and_increment_mtime(&self) -> MTime {
let mut state = self.state.lock();
state.get_and_increment_mtime()
}
pub async fn touch_path(&self, path: impl AsRef<Path>) {
let mut state = self.state.lock();
let path = path.as_ref();
let new_mtime = state.next_mtime;
let new_inode = state.next_inode;
state.next_inode += 1;
state.next_mtime += Duration::from_nanos(Self::SYSTEMTIME_INTERVAL);
let new_mtime = state.get_and_increment_mtime();
let new_inode = state.get_and_increment_inode();
state
.write_path(path, move |entry| {
match entry {
@ -1062,19 +1127,14 @@ impl FakeFs {
fn write_file_internal(&self, path: impl AsRef<Path>, content: Vec<u8>) -> Result<()> {
let mut state = self.state.lock();
let path = path.as_ref();
let inode = state.next_inode;
let mtime = state.next_mtime;
state.next_inode += 1;
state.next_mtime += Duration::from_nanos(Self::SYSTEMTIME_INTERVAL);
let file = Arc::new(Mutex::new(FakeFsEntry::File {
inode,
mtime,
inode: state.get_and_increment_inode(),
mtime: state.get_and_increment_mtime(),
len: content.len() as u64,
content,
}));
let mut kind = None;
state.write_path(path, {
state.write_path(path.as_ref(), {
let kind = &mut kind;
move |entry| {
match entry {
@ -1090,7 +1150,7 @@ impl FakeFs {
Ok(())
}
})?;
state.emit_event([(path, kind)]);
state.emit_event([(path.as_ref(), kind)]);
Ok(())
}
@ -1383,16 +1443,6 @@ impl FakeFsEntry {
}
}
fn set_file_content(&mut self, path: &Path, new_content: Vec<u8>) -> Result<()> {
if let Self::File { content, mtime, .. } = self {
*mtime = SystemTime::now();
*content = new_content;
Ok(())
} else {
Err(anyhow!("not a file: {}", path.display()))
}
}
fn dir_entries(
&mut self,
path: &Path,
@ -1456,10 +1506,8 @@ impl Fs for FakeFs {
}
let mut state = self.state.lock();
let inode = state.next_inode;
let mtime = state.next_mtime;
state.next_mtime += Duration::from_nanos(Self::SYSTEMTIME_INTERVAL);
state.next_inode += 1;
let inode = state.get_and_increment_inode();
let mtime = state.get_and_increment_mtime();
state.write_path(&cur_path, |entry| {
entry.or_insert_with(|| {
created_dirs.push((cur_path.clone(), Some(PathEventKind::Created)));
@ -1482,10 +1530,8 @@ impl Fs for FakeFs {
async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()> {
self.simulate_random_delay().await;
let mut state = self.state.lock();
let inode = state.next_inode;
let mtime = state.next_mtime;
state.next_mtime += Duration::from_nanos(Self::SYSTEMTIME_INTERVAL);
state.next_inode += 1;
let inode = state.get_and_increment_inode();
let mtime = state.get_and_increment_mtime();
let file = Arc::new(Mutex::new(FakeFsEntry::File {
inode,
mtime,
@ -1625,13 +1671,12 @@ impl Fs for FakeFs {
let source = normalize_path(source);
let target = normalize_path(target);
let mut state = self.state.lock();
let mtime = state.next_mtime;
let inode = util::post_inc(&mut state.next_inode);
state.next_mtime += Duration::from_nanos(Self::SYSTEMTIME_INTERVAL);
let mtime = state.get_and_increment_mtime();
let inode = state.get_and_increment_inode();
let source_entry = state.read_path(&source)?;
let content = source_entry.lock().file_content(&source)?.clone();
let mut kind = Some(PathEventKind::Created);
let entry = state.write_path(&target, |e| match e {
state.write_path(&target, |e| match e {
btree_map::Entry::Occupied(e) => {
if options.overwrite {
kind = Some(PathEventKind::Changed);
@ -1647,14 +1692,11 @@ impl Fs for FakeFs {
inode,
mtime,
len: content.len() as u64,
content: Vec::new(),
content,
})))
.clone(),
)),
})?;
if let Some(entry) = entry {
entry.lock().set_file_content(&target, content)?;
}
state.emit_event([(target, kind)]);
Ok(())
}

View file

@ -31,10 +31,6 @@ time.workspace = true
url.workspace = true
util.workspace = true
[target.'cfg(target_os = "windows")'.dependencies]
windows.workspace = true
[dev-dependencies]
unindent.workspace = true
serde_json.workspace = true

View file

@ -4,7 +4,7 @@ use anyhow::{anyhow, Context, Result};
use collections::{HashMap, HashSet};
use serde::{Deserialize, Serialize};
use std::io::Write;
use std::process::{Command, Stdio};
use std::process::Stdio;
use std::sync::Arc;
use std::{ops::Range, path::Path};
use text::Rope;
@ -80,9 +80,7 @@ fn run_git_blame(
path: &Path,
contents: &Rope,
) -> Result<String> {
let mut child = Command::new(git_binary);
child
let child = util::command::new_std_command(git_binary)
.current_dir(working_directory)
.arg("blame")
.arg("--incremental")
@ -91,15 +89,7 @@ fn run_git_blame(
.arg(path.as_os_str())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
child.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0);
}
let child = child
.stderr(Stdio::piped())
.spawn()
.map_err(|e| anyhow!("Failed to start git blame process: {}", e))?;

View file

@ -2,10 +2,6 @@ use crate::Oid;
use anyhow::{anyhow, Result};
use collections::HashMap;
use std::path::Path;
use std::process::Command;
#[cfg(windows)]
use std::os::windows::process::CommandExt;
pub fn get_messages(working_directory: &Path, shas: &[Oid]) -> Result<HashMap<Oid, String>> {
if shas.is_empty() {
@ -14,19 +10,12 @@ pub fn get_messages(working_directory: &Path, shas: &[Oid]) -> Result<HashMap<Oi
const MARKER: &str = "<MARKER>";
let mut command = Command::new("git");
command
let output = util::command::new_std_command("git")
.current_dir(working_directory)
.arg("show")
.arg("-s")
.arg(format!("--format=%B{}", MARKER))
.args(shas.iter().map(ToString::to_string));
#[cfg(windows)]
command.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0);
let output = command
.args(shas.iter().map(ToString::to_string))
.output()
.map_err(|e| anyhow!("Failed to start git blame process: {}", e))?;

View file

@ -2,7 +2,7 @@ use crate::repository::{GitFileStatus, RepoPath};
use anyhow::{anyhow, Result};
use std::{
path::{Path, PathBuf},
process::{Command, Stdio},
process::Stdio,
sync::Arc,
};
@ -17,9 +17,7 @@ impl GitStatus {
working_directory: &Path,
path_prefixes: &[PathBuf],
) -> Result<Self> {
let mut child = Command::new(git_binary);
child
let child = util::command::new_std_command(git_binary)
.current_dir(working_directory)
.args([
"--no-optional-locks",
@ -37,15 +35,7 @@ impl GitStatus {
}))
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
child.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0);
}
let child = child
.stderr(Stdio::piped())
.spawn()
.map_err(|e| anyhow!("Failed to start git status process: {}", e))?;

View file

@ -15,7 +15,10 @@ actions!(
SelectAll,
Home,
End,
ShowCharacterPalette
ShowCharacterPalette,
Paste,
Cut,
Copy,
]
);
@ -107,6 +110,28 @@ impl TextInput {
cx.show_character_palette();
}
fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
if let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) {
self.replace_text_in_range(None, &text.replace("\n", " "), cx);
}
}
fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
if !self.selected_range.is_empty() {
cx.write_to_clipboard(ClipboardItem::new_string(
(&self.content[self.selected_range.clone()]).to_string(),
));
}
}
fn cut(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
if !self.selected_range.is_empty() {
cx.write_to_clipboard(ClipboardItem::new_string(
(&self.content[self.selected_range.clone()]).to_string(),
));
self.replace_text_in_range(None, "", cx)
}
}
fn move_to(&mut self, offset: usize, cx: &mut ViewContext<Self>) {
self.selected_range = offset..offset;
cx.notify()
@ -219,9 +244,11 @@ impl ViewInputHandler for TextInput {
fn text_for_range(
&mut self,
range_utf16: Range<usize>,
actual_range: &mut Option<Range<usize>>,
_cx: &mut ViewContext<Self>,
) -> Option<String> {
let range = self.range_from_utf16(&range_utf16);
actual_range.replace(self.range_to_utf16(&range));
Some(self.content[range].to_string())
}
@ -497,6 +524,9 @@ impl Render for TextInput {
.on_action(cx.listener(Self::home))
.on_action(cx.listener(Self::end))
.on_action(cx.listener(Self::show_character_palette))
.on_action(cx.listener(Self::paste))
.on_action(cx.listener(Self::cut))
.on_action(cx.listener(Self::copy))
.on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down))
.on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up))
.on_mouse_up_out(MouseButton::Left, cx.listener(Self::on_mouse_up))
@ -581,8 +611,8 @@ impl Render for InputExample {
format!(
"{:} {}",
ks.unparse(),
if let Some(ime_key) = ks.ime_key.as_ref() {
format!("-> {:?}", ime_key)
if let Some(key_char) = ks.key_char.as_ref() {
format!("-> {:?}", key_char)
} else {
"".to_owned()
}
@ -602,6 +632,9 @@ fn main() {
KeyBinding::new("shift-left", SelectLeft, None),
KeyBinding::new("shift-right", SelectRight, None),
KeyBinding::new("cmd-a", SelectAll, None),
KeyBinding::new("cmd-v", Paste, None),
KeyBinding::new("cmd-c", Copy, None),
KeyBinding::new("cmd-x", Cut, None),
KeyBinding::new("home", Home, None),
KeyBinding::new("end", End, None),
KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, None),

View file

@ -263,7 +263,7 @@ impl TextLayout {
.line_height
.to_pixels(font_size.into(), cx.rem_size());
let runs = if let Some(runs) = runs {
let mut runs = if let Some(runs) = runs {
runs
} else {
vec![text_style.to_run(text.len())]
@ -306,7 +306,7 @@ impl TextLayout {
let mut line_wrapper = cx.text_system().line_wrapper(text_style.font(), font_size);
let text = if let Some(truncate_width) = truncate_width {
line_wrapper.truncate_line(text.clone(), truncate_width, ellipsis)
line_wrapper.truncate_line(text.clone(), truncate_width, ellipsis, &mut runs)
} else {
text.clone()
};

View file

@ -9,8 +9,12 @@ use std::ops::Range;
/// See [`InputHandler`] for details on how to implement each method.
pub trait ViewInputHandler: 'static + Sized {
/// See [`InputHandler::text_for_range`] for details
fn text_for_range(&mut self, range: Range<usize>, cx: &mut ViewContext<Self>)
-> Option<String>;
fn text_for_range(
&mut self,
range: Range<usize>,
adjusted_range: &mut Option<Range<usize>>,
cx: &mut ViewContext<Self>,
) -> Option<String>;
/// See [`InputHandler::selected_text_range`] for details
fn selected_text_range(
@ -89,10 +93,12 @@ impl<V: ViewInputHandler> InputHandler for ElementInputHandler<V> {
fn text_for_range(
&mut self,
range_utf16: Range<usize>,
adjusted_range: &mut Option<Range<usize>>,
cx: &mut WindowContext,
) -> Option<String> {
self.view
.update(cx, |view, cx| view.text_for_range(range_utf16, cx))
self.view.update(cx, |view, cx| {
view.text_for_range(range_utf16, adjusted_range, cx)
})
}
fn replace_text_in_range(

View file

@ -643,9 +643,13 @@ impl PlatformInputHandler {
}
#[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
fn text_for_range(&mut self, range_utf16: Range<usize>) -> Option<String> {
fn text_for_range(
&mut self,
range_utf16: Range<usize>,
adjusted: &mut Option<Range<usize>>,
) -> Option<String> {
self.cx
.update(|cx| self.handler.text_for_range(range_utf16, cx))
.update(|cx| self.handler.text_for_range(range_utf16, adjusted, cx))
.ok()
.flatten()
}
@ -712,6 +716,7 @@ impl PlatformInputHandler {
/// A struct representing a selection in a text buffer, in UTF16 characters.
/// This is different from a range because the head may be before the tail.
#[derive(Debug)]
pub struct UTF16Selection {
/// The range of text in the document this selection corresponds to
/// in UTF16 characters.
@ -749,6 +754,7 @@ pub trait InputHandler: 'static {
fn text_for_range(
&mut self,
range_utf16: Range<usize>,
adjusted_range: &mut Option<Range<usize>>,
cx: &mut WindowContext,
) -> Option<String>;

View file

@ -12,14 +12,15 @@ pub struct Keystroke {
/// e.g. for option-s, key is "s"
pub key: String,
/// ime_key is the character inserted by the IME engine when that key was pressed.
/// e.g. for option-s, ime_key is "ß"
pub ime_key: Option<String>,
/// key_char is the character that could have been typed when
/// this binding was pressed.
/// e.g. for s this is "s", for option-s "ß", and cmd-s None
pub key_char: Option<String>,
}
impl Keystroke {
/// When matching a key we cannot know whether the user intended to type
/// the ime_key or the key itself. On some non-US keyboards keys we use in our
/// the key_char or the key itself. On some non-US keyboards keys we use in our
/// bindings are behind option (for example `$` is typed `alt-ç` on a Czech keyboard),
/// and on some keyboards the IME handler converts a sequence of keys into a
/// specific character (for example `"` is typed as `" space` on a brazilian keyboard).
@ -27,10 +28,10 @@ impl Keystroke {
/// This method assumes that `self` was typed and `target' is in the keymap, and checks
/// both possibilities for self against the target.
pub(crate) fn should_match(&self, target: &Keystroke) -> bool {
if let Some(ime_key) = self
.ime_key
if let Some(key_char) = self
.key_char
.as_ref()
.filter(|ime_key| ime_key != &&self.key)
.filter(|key_char| key_char != &&self.key)
{
let ime_modifiers = Modifiers {
control: self.modifiers.control,
@ -38,7 +39,7 @@ impl Keystroke {
..Default::default()
};
if &target.key == ime_key && target.modifiers == ime_modifiers {
if &target.key == key_char && target.modifiers == ime_modifiers {
return true;
}
}
@ -47,9 +48,9 @@ impl Keystroke {
}
/// key syntax is:
/// [ctrl-][alt-][shift-][cmd-][fn-]key[->ime_key]
/// ime_key syntax is only used for generating test events,
/// when matching a key with an ime_key set will be matched without it.
/// [ctrl-][alt-][shift-][cmd-][fn-]key[->key_char]
/// key_char syntax is only used for generating test events,
/// when matching a key with an key_char set will be matched without it.
pub fn parse(source: &str) -> anyhow::Result<Self> {
let mut control = false;
let mut alt = false;
@ -57,7 +58,7 @@ impl Keystroke {
let mut platform = false;
let mut function = false;
let mut key = None;
let mut ime_key = None;
let mut key_char = None;
let mut components = source.split('-').peekable();
while let Some(component) = components.next() {
@ -74,7 +75,7 @@ impl Keystroke {
break;
} else if next.len() > 1 && next.starts_with('>') {
key = Some(String::from(component));
ime_key = Some(String::from(&next[1..]));
key_char = Some(String::from(&next[1..]));
components.next();
} else {
return Err(anyhow!("Invalid keystroke `{}`", source));
@ -118,7 +119,7 @@ impl Keystroke {
function,
},
key,
ime_key,
key_char: key_char,
})
}
@ -154,7 +155,7 @@ impl Keystroke {
/// Returns true if this keystroke left
/// the ime system in an incomplete state.
pub fn is_ime_in_progress(&self) -> bool {
self.ime_key.is_none()
self.key_char.is_none()
&& (is_printable_key(&self.key) || self.key.is_empty())
&& !(self.modifiers.platform
|| self.modifiers.control
@ -162,17 +163,17 @@ impl Keystroke {
|| self.modifiers.alt)
}
/// Returns a new keystroke with the ime_key filled.
/// Returns a new keystroke with the key_char filled.
/// This is used for dispatch_keystroke where we want users to
/// be able to simulate typing "space", etc.
pub fn with_simulated_ime(mut self) -> Self {
if self.ime_key.is_none()
if self.key_char.is_none()
&& !self.modifiers.platform
&& !self.modifiers.control
&& !self.modifiers.function
&& !self.modifiers.alt
{
self.ime_key = match self.key.as_str() {
self.key_char = match self.key.as_str() {
"space" => Some(" ".into()),
"tab" => Some("\t".into()),
"enter" => Some("\n".into()),

View file

@ -742,14 +742,14 @@ impl Keystroke {
}
}
// Ignore control characters (and DEL) for the purposes of ime_key
let ime_key =
// Ignore control characters (and DEL) for the purposes of key_char
let key_char =
(key_utf32 >= 32 && key_utf32 != 127 && !key_utf8.is_empty()).then_some(key_utf8);
Keystroke {
modifiers,
key,
ime_key,
key_char,
}
}

View file

@ -1208,7 +1208,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
compose.feed(keysym);
match compose.status() {
xkb::Status::Composing => {
keystroke.ime_key = None;
keystroke.key_char = None;
state.pre_edit_text =
compose.utf8().or(Keystroke::underlying_dead_key(keysym));
let pre_edit =
@ -1220,7 +1220,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
xkb::Status::Composed => {
state.pre_edit_text.take();
keystroke.ime_key = compose.utf8();
keystroke.key_char = compose.utf8();
if let Some(keysym) = compose.keysym() {
keystroke.key = xkb::keysym_get_name(keysym);
}
@ -1340,7 +1340,7 @@ impl Dispatch<zwp_text_input_v3::ZwpTextInputV3, ()> for WaylandClientStatePtr {
keystroke: Keystroke {
modifiers: Modifiers::default(),
key: commit_text.clone(),
ime_key: Some(commit_text),
key_char: Some(commit_text),
},
is_held: false,
}));

View file

@ -687,11 +687,11 @@ impl WaylandWindowStatePtr {
}
}
if let PlatformInput::KeyDown(event) = input {
if let Some(ime_key) = &event.keystroke.ime_key {
if let Some(key_char) = &event.keystroke.key_char {
let mut state = self.state.borrow_mut();
if let Some(mut input_handler) = state.input_handler.take() {
drop(state);
input_handler.replace_text_in_range(None, ime_key);
input_handler.replace_text_in_range(None, key_char);
self.state.borrow_mut().input_handler = Some(input_handler);
}
}

View file

@ -178,7 +178,7 @@ pub struct X11ClientState {
pub(crate) compose_state: Option<xkbc::compose::State>,
pub(crate) pre_edit_text: Option<String>,
pub(crate) composing: bool,
pub(crate) pre_ime_key_down: Option<Keystroke>,
pub(crate) pre_key_char_down: Option<Keystroke>,
pub(crate) cursor_handle: cursor::Handle,
pub(crate) cursor_styles: HashMap<xproto::Window, CursorStyle>,
pub(crate) cursor_cache: HashMap<CursorStyle, xproto::Cursor>,
@ -446,7 +446,7 @@ impl X11Client {
compose_state,
pre_edit_text: None,
pre_ime_key_down: None,
pre_key_char_down: None,
composing: false,
cursor_handle,
@ -858,7 +858,7 @@ impl X11Client {
let modifiers = modifiers_from_state(event.state);
state.modifiers = modifiers;
state.pre_ime_key_down.take();
state.pre_key_char_down.take();
let keystroke = {
let code = event.detail.into();
let xkb_state = state.previous_xkb_state.clone();
@ -880,13 +880,13 @@ impl X11Client {
match compose_state.status() {
xkbc::Status::Composed => {
state.pre_edit_text.take();
keystroke.ime_key = compose_state.utf8();
keystroke.key_char = compose_state.utf8();
if let Some(keysym) = compose_state.keysym() {
keystroke.key = xkbc::keysym_get_name(keysym);
}
}
xkbc::Status::Composing => {
keystroke.ime_key = None;
keystroke.key_char = None;
state.pre_edit_text = compose_state
.utf8()
.or(crate::Keystroke::underlying_dead_key(keysym));
@ -1156,7 +1156,7 @@ impl X11Client {
match event {
Event::KeyPress(event) | Event::KeyRelease(event) => {
let mut state = self.0.borrow_mut();
state.pre_ime_key_down = Some(Keystroke::from_xkb(
state.pre_key_char_down = Some(Keystroke::from_xkb(
&state.xkb,
state.modifiers,
event.detail.into(),
@ -1187,11 +1187,11 @@ impl X11Client {
fn xim_handle_commit(&self, window: xproto::Window, text: String) -> Option<()> {
let window = self.get_window(window).unwrap();
let mut state = self.0.borrow_mut();
let keystroke = state.pre_ime_key_down.take();
let keystroke = state.pre_key_char_down.take();
state.composing = false;
drop(state);
if let Some(mut keystroke) = keystroke {
keystroke.ime_key = Some(text.clone());
keystroke.key_char = Some(text.clone());
window.handle_input(PlatformInput::KeyDown(crate::KeyDownEvent {
keystroke,
is_held: false,

View file

@ -846,9 +846,9 @@ impl X11WindowStatePtr {
if let PlatformInput::KeyDown(event) = input {
let mut state = self.state.borrow_mut();
if let Some(mut input_handler) = state.input_handler.take() {
if let Some(ime_key) = &event.keystroke.ime_key {
if let Some(key_char) = &event.keystroke.key_char {
drop(state);
input_handler.replace_text_in_range(None, ime_key);
input_handler.replace_text_in_range(None, key_char);
state = self.state.borrow_mut();
}
state.input_handler = Some(input_handler);

View file

@ -245,7 +245,7 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
.charactersIgnoringModifiers()
.to_str()
.to_string();
let mut ime_key = None;
let mut key_char = None;
let first_char = characters.chars().next().map(|ch| ch as u16);
let modifiers = native_event.modifierFlags();
@ -260,11 +260,20 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
#[allow(non_upper_case_globals)]
let key = match first_char {
Some(SPACE_KEY) => "space".to_string(),
Some(SPACE_KEY) => {
key_char = Some(" ".to_string());
"space".to_string()
}
Some(TAB_KEY) => {
key_char = Some("\t".to_string());
"tab".to_string()
}
Some(ENTER_KEY) | Some(NUMPAD_ENTER_KEY) => {
key_char = Some("\n".to_string());
"enter".to_string()
}
Some(BACKSPACE_KEY) => "backspace".to_string(),
Some(ENTER_KEY) | Some(NUMPAD_ENTER_KEY) => "enter".to_string(),
Some(ESCAPE_KEY) => "escape".to_string(),
Some(TAB_KEY) => "tab".to_string(),
Some(SHIFT_TAB_KEY) => "tab".to_string(),
Some(NSUpArrowFunctionKey) => "up".to_string(),
Some(NSDownArrowFunctionKey) => "down".to_string(),
@ -332,6 +341,18 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
chars_ignoring_modifiers = chars_with_cmd;
}
if !control && !command && !function {
let mut mods = NO_MOD;
if shift {
mods |= SHIFT_MOD;
}
if alt {
mods |= OPTION_MOD;
}
key_char = Some(chars_for_modified_key(native_event.keyCode(), mods));
}
let mut key = if shift
&& chars_ignoring_modifiers
.chars()
@ -345,20 +366,6 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
chars_ignoring_modifiers
};
if always_use_cmd_layout || alt {
let mut mods = NO_MOD;
if shift {
mods |= SHIFT_MOD;
}
if alt {
mods |= OPTION_MOD;
}
let alt_key = chars_for_modified_key(native_event.keyCode(), mods);
if alt_key != key {
ime_key = Some(alt_key);
}
};
key
}
};
@ -372,7 +379,7 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
function,
},
key,
ime_key,
key_char,
}
}

View file

@ -844,7 +844,9 @@ impl Platform for MacPlatform {
let app: id = msg_send![APP_CLASS, sharedApplication];
let mut state = self.0.lock();
let actions = &mut state.menu_actions;
app.setMainMenu_(self.create_menu_bar(menus, NSWindow::delegate(app), actions, keymap));
let menu = self.create_menu_bar(menus, NSWindow::delegate(app), actions, keymap);
drop(state);
app.setMainMenu_(menu);
}
}

View file

@ -38,6 +38,7 @@ use std::{
cell::Cell,
ffi::{c_void, CStr},
mem,
ops::Range,
path::PathBuf,
ptr::{self, NonNull},
rc::Rc,
@ -1283,18 +1284,17 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
}
if event.is_held {
let handled = with_input_handler(&this, |input_handler| {
if !input_handler.apple_press_and_hold_enabled() {
input_handler.replace_text_in_range(
None,
&event.keystroke.ime_key.unwrap_or(event.keystroke.key),
);
if let Some(key_char) = event.keystroke.key_char.as_ref() {
let handled = with_input_handler(&this, |input_handler| {
if !input_handler.apple_press_and_hold_enabled() {
input_handler.replace_text_in_range(None, &key_char);
return YES;
}
NO
});
if handled == Some(YES) {
return YES;
}
NO
});
if handled == Some(YES) {
return YES;
}
}
@ -1437,7 +1437,7 @@ extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) {
let keystroke = Keystroke {
modifiers: Default::default(),
key: ".".into(),
ime_key: None,
key_char: None,
};
let event = PlatformInput::KeyDown(KeyDownEvent {
keystroke: keystroke.clone(),
@ -1755,15 +1755,21 @@ extern "C" fn attributed_substring_for_proposed_range(
this: &Object,
_: Sel,
range: NSRange,
_actual_range: *mut c_void,
actual_range: *mut c_void,
) -> id {
with_input_handler(this, |input_handler| {
let range = range.to_range()?;
if range.is_empty() {
return None;
}
let mut adjusted: Option<Range<usize>> = None;
let selected_text = input_handler.text_for_range(range.clone())?;
let selected_text = input_handler.text_for_range(range.clone(), &mut adjusted)?;
if let Some(adjusted) = adjusted {
if adjusted != range {
unsafe { (actual_range as *mut NSRange).write(NSRange::from(adjusted)) };
}
}
unsafe {
let string: id = msg_send![class!(NSAttributedString), alloc];
let string: id = msg_send![string, initWithString: ns_string(&selected_text)];

View file

@ -386,7 +386,7 @@ fn handle_char_msg(
return Some(1);
};
drop(lock);
let ime_key = keystroke.ime_key.clone();
let key_char = keystroke.key_char.clone();
let event = KeyDownEvent {
keystroke,
is_held: lparam.0 & (0x1 << 30) > 0,
@ -397,7 +397,7 @@ fn handle_char_msg(
if dispatch_event_result.default_prevented || !dispatch_event_result.propagate {
return Some(0);
}
let Some(ime_char) = ime_key else {
let Some(ime_char) = key_char else {
return Some(1);
};
with_input_handler(&state_ptr, |input_handler| {
@ -1172,7 +1172,7 @@ fn parse_syskeydown_msg_keystroke(wparam: WPARAM) -> Option<Keystroke> {
Some(Keystroke {
modifiers,
key,
ime_key: None,
key_char: None,
})
}
@ -1220,7 +1220,7 @@ fn parse_keydown_msg_keystroke(wparam: WPARAM) -> Option<KeystrokeOrModifier> {
return Some(KeystrokeOrModifier::Keystroke(Keystroke {
modifiers,
key: format!("f{}", offset + 1),
ime_key: None,
key_char: None,
}));
};
return None;
@ -1231,7 +1231,7 @@ fn parse_keydown_msg_keystroke(wparam: WPARAM) -> Option<KeystrokeOrModifier> {
Some(KeystrokeOrModifier::Keystroke(Keystroke {
modifiers,
key,
ime_key: None,
key_char: None,
}))
}
@ -1253,7 +1253,7 @@ fn parse_char_msg_keystroke(wparam: WPARAM) -> Option<Keystroke> {
Some(Keystroke {
modifiers,
key,
ime_key: Some(first_char.to_string()),
key_char: Some(first_char.to_string()),
})
}
}
@ -1327,7 +1327,7 @@ fn basic_vkcode_to_string(code: u16, modifiers: Modifiers) -> Option<Keystroke>
Some(Keystroke {
modifiers,
key,
ime_key: None,
key_char: None,
})
}

View file

@ -292,7 +292,7 @@ impl Platform for WindowsPlatform {
pid,
app_path.display(),
);
let restart_process = std::process::Command::new("powershell.exe")
let restart_process = util::command::new_std_command("powershell.exe")
.arg("-command")
.arg(script)
.spawn();

View file

@ -1,4 +1,4 @@
use crate::{px, FontId, FontRun, Pixels, PlatformTextSystem, SharedString};
use crate::{px, FontId, FontRun, Pixels, PlatformTextSystem, SharedString, TextRun};
use collections::HashMap;
use std::{iter, sync::Arc};
@ -104,6 +104,7 @@ impl LineWrapper {
line: SharedString,
truncate_width: Pixels,
ellipsis: Option<&str>,
runs: &mut Vec<TextRun>,
) -> SharedString {
let mut width = px(0.);
let mut ellipsis_width = px(0.);
@ -124,15 +125,15 @@ impl LineWrapper {
width += char_width;
if width.floor() > truncate_width {
return SharedString::from(format!(
"{}{}",
&line[..truncate_ix],
ellipsis.unwrap_or("")
));
let ellipsis = ellipsis.unwrap_or("");
let result = SharedString::from(format!("{}{}", &line[..truncate_ix], ellipsis));
update_runs_after_truncation(&result, ellipsis, runs);
return result;
}
}
line.clone()
line
}
pub(crate) fn is_word_char(c: char) -> bool {
@ -195,6 +196,23 @@ impl LineWrapper {
}
}
fn update_runs_after_truncation(result: &str, ellipsis: &str, runs: &mut Vec<TextRun>) {
let mut truncate_at = result.len() - ellipsis.len();
let mut run_end = None;
for (run_index, run) in runs.iter_mut().enumerate() {
if run.len <= truncate_at {
truncate_at -= run.len;
} else {
run.len = truncate_at + ellipsis.len();
run_end = Some(run_index + 1);
break;
}
}
if let Some(run_end) = run_end {
runs.truncate(run_end);
}
}
/// A boundary between two lines of text.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct Boundary {
@ -213,7 +231,9 @@ impl Boundary {
#[cfg(test)]
mod tests {
use super::*;
use crate::{font, TestAppContext, TestDispatcher};
use crate::{
font, Font, FontFeatures, FontStyle, FontWeight, Hsla, TestAppContext, TestDispatcher,
};
#[cfg(target_os = "macos")]
use crate::{TextRun, WindowTextSystem, WrapBoundary};
use rand::prelude::*;
@ -232,6 +252,26 @@ mod tests {
LineWrapper::new(id, px(16.), cx.text_system().platform_text_system.clone())
}
fn generate_test_runs(input_run_len: &[usize]) -> Vec<TextRun> {
input_run_len
.iter()
.map(|run_len| TextRun {
len: *run_len,
font: Font {
family: "Dummy".into(),
features: FontFeatures::default(),
fallbacks: None,
weight: FontWeight::default(),
style: FontStyle::Normal,
},
color: Hsla::default(),
background_color: None,
underline: None,
strikethrough: None,
})
.collect()
}
#[test]
fn test_wrap_line() {
let mut wrapper = build_wrapper();
@ -293,28 +333,135 @@ mod tests {
fn test_truncate_line() {
let mut wrapper = build_wrapper();
assert_eq!(
wrapper.truncate_line("aa bbb cccc ddddd eeee ffff gggg".into(), px(220.), None),
"aa bbb cccc ddddd eeee"
fn perform_test(
wrapper: &mut LineWrapper,
text: &'static str,
result: &'static str,
ellipsis: Option<&str>,
) {
let dummy_run_lens = vec![text.len()];
let mut dummy_runs = generate_test_runs(&dummy_run_lens);
assert_eq!(
wrapper.truncate_line(text.into(), px(220.), ellipsis, &mut dummy_runs),
result
);
assert_eq!(dummy_runs.first().unwrap().len, result.len());
}
perform_test(
&mut wrapper,
"aa bbb cccc ddddd eeee ffff gggg",
"aa bbb cccc ddddd eeee",
None,
);
assert_eq!(
wrapper.truncate_line(
"aa bbb cccc ddddd eeee ffff gggg".into(),
px(220.),
Some("")
),
"aa bbb cccc ddddd eee…"
perform_test(
&mut wrapper,
"aa bbb cccc ddddd eeee ffff gggg",
"aa bbb cccc ddddd eee…",
Some(""),
);
assert_eq!(
wrapper.truncate_line(
"aa bbb cccc ddddd eeee ffff gggg".into(),
px(220.),
Some("......")
),
"aa bbb cccc dddd......"
perform_test(
&mut wrapper,
"aa bbb cccc ddddd eeee ffff gggg",
"aa bbb cccc dddd......",
Some("......"),
);
}
#[test]
fn test_truncate_multiple_runs() {
let mut wrapper = build_wrapper();
fn perform_test(
wrapper: &mut LineWrapper,
text: &'static str,
result: &str,
run_lens: &[usize],
result_run_len: &[usize],
line_width: Pixels,
) {
let mut dummy_runs = generate_test_runs(run_lens);
assert_eq!(
wrapper.truncate_line(text.into(), line_width, Some(""), &mut dummy_runs),
result
);
for (run, result_len) in dummy_runs.iter().zip(result_run_len) {
assert_eq!(run.len, *result_len);
}
}
// Case 0: Normal
// Text: abcdefghijkl
// Runs: Run0 { len: 12, ... }
//
// Truncate res: abcd… (truncate_at = 4)
// Run res: Run0 { string: abcd…, len: 7, ... }
perform_test(&mut wrapper, "abcdefghijkl", "abcd…", &[12], &[7], px(50.));
// Case 1: Drop some runs
// Text: abcdefghijkl
// Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
//
// Truncate res: abcdef… (truncate_at = 6)
// Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: ef…, len:
// 5, ... }
perform_test(
&mut wrapper,
"abcdefghijkl",
"abcdef…",
&[4, 4, 4],
&[4, 5],
px(70.),
);
// Case 2: Truncate at start of some run
// Text: abcdefghijkl
// Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
//
// Truncate res: abcdefgh… (truncate_at = 8)
// Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: efgh, len:
// 4, ... }, Run2 { string: …, len: 3, ... }
perform_test(
&mut wrapper,
"abcdefghijkl",
"abcdefgh…",
&[4, 4, 4],
&[4, 4, 3],
px(90.),
);
}
#[test]
fn test_update_run_after_truncation() {
fn perform_test(result: &str, run_lens: &[usize], result_run_lens: &[usize]) {
let mut dummy_runs = generate_test_runs(run_lens);
update_runs_after_truncation(result, "", &mut dummy_runs);
for (run, result_len) in dummy_runs.iter().zip(result_run_lens) {
assert_eq!(run.len, *result_len);
}
}
// Case 0: Normal
// Text: abcdefghijkl
// Runs: Run0 { len: 12, ... }
//
// Truncate res: abcd… (truncate_at = 4)
// Run res: Run0 { string: abcd…, len: 7, ... }
perform_test("abcd…", &[12], &[7]);
// Case 1: Drop some runs
// Text: abcdefghijkl
// Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
//
// Truncate res: abcdef… (truncate_at = 6)
// Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: ef…, len:
// 5, ... }
perform_test("abcdef…", &[4, 4, 4], &[4, 5]);
// Case 2: Truncate at start of some run
// Text: abcdefghijkl
// Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
//
// Truncate res: abcdefgh… (truncate_at = 8)
// Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: efgh, len:
// 4, ... }, Run2 { string: …, len: 3, ... }
perform_test("abcdefgh…", &[4, 4, 4], &[4, 4, 3]);
}
#[test]
fn test_is_word_char() {
#[track_caller]

View file

@ -3038,7 +3038,7 @@ impl<'a> WindowContext<'a> {
return true;
}
if let Some(input) = keystroke.ime_key {
if let Some(input) = keystroke.key_char {
if let Some(mut input_handler) = self.window.platform_window.take_input_handler() {
input_handler.dispatch_input(&input, self);
self.window.platform_window.set_input_handler(input_handler);
@ -3267,7 +3267,7 @@ impl<'a> WindowContext<'a> {
if let Some(key) = key {
keystroke = Some(Keystroke {
key: key.to_string(),
ime_key: None,
key_char: None,
modifiers: Modifiers::default(),
});
}
@ -3482,7 +3482,7 @@ impl<'a> WindowContext<'a> {
if !self.propagate_event {
continue 'replay;
}
if let Some(input) = replay.keystroke.ime_key.as_ref().cloned() {
if let Some(input) = replay.keystroke.key_char.as_ref().cloned() {
if let Some(mut input_handler) = self.window.platform_window.take_input_handler() {
input_handler.dispatch_input(&input, self);
self.window.platform_window.set_input_handler(input_handler)

View file

@ -116,7 +116,7 @@ impl Item for ImageView {
.map(Icon::from_path)
}
fn breadcrumb_location(&self) -> ToolbarItemLocation {
fn breadcrumb_location(&self, _: &AppContext) -> ToolbarItemLocation {
ToolbarItemLocation::PrimaryLeft
}

View file

@ -0,0 +1,18 @@
[package]
name = "inline_completion"
version = "0.1.0"
edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/inline_completion.rs"
[dependencies]
gpui.workspace = true
language.workspace = true
project.workspace = true
text.workspace = true

View file

@ -0,0 +1 @@
../../LICENSE-GPL

View file

@ -1,9 +1,18 @@
use crate::Direction;
use gpui::{AppContext, Model, ModelContext};
use language::Buffer;
use std::ops::Range;
use text::{Anchor, Rope};
// TODO: Find a better home for `Direction`.
//
// This should live in an ancestor crate of `editor` and `inline_completion`,
// but at time of writing there isn't an obvious spot.
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum Direction {
Prev,
Next,
}
pub enum InlayProposal {
Hint(Anchor, project::InlayHint),
Suggestion(Anchor, Rope),

View file

@ -23,7 +23,6 @@ paths.workspace = true
settings.workspace = true
supermaven.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
zed_actions.workspace = true

View file

@ -1,5 +1,5 @@
use anyhow::Result;
use copilot::{Copilot, CopilotCodeVerification, Status};
use copilot::{Copilot, Status};
use editor::{scroll::Autoscroll, Editor};
use fs::Fs;
use gpui::{
@ -15,7 +15,6 @@ use language::{
use settings::{update_settings_file, Settings, SettingsStore};
use std::{path::Path, sync::Arc};
use supermaven::{AccountStatus, Supermaven};
use util::ResultExt;
use workspace::{
create_and_open_local_file,
item::ItemHandle,
@ -29,8 +28,6 @@ use zed_actions::OpenBrowser;
const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot";
struct CopilotStartingToast;
struct CopilotErrorToast;
pub struct InlineCompletionButton {
@ -221,7 +218,7 @@ impl InlineCompletionButton {
pub fn build_copilot_start_menu(&mut self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
let fs = self.fs.clone();
ContextMenu::build(cx, |menu, _| {
menu.entry("Sign In", None, initiate_sign_in)
menu.entry("Sign In", None, copilot::initiate_sign_in)
.entry("Disable Copilot", None, {
let fs = fs.clone();
move |cx| hide_copilot(fs.clone(), cx)
@ -484,68 +481,3 @@ fn hide_copilot(fs: Arc<dyn Fs>, cx: &mut AppContext) {
.inline_completion_provider = Some(InlineCompletionProvider::None);
});
}
pub fn initiate_sign_in(cx: &mut WindowContext) {
let Some(copilot) = Copilot::global(cx) else {
return;
};
let status = copilot.read(cx).status();
let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
return;
};
match status {
Status::Starting { task } => {
let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
return;
};
let Ok(workspace) = workspace.update(cx, |workspace, cx| {
workspace.show_toast(
Toast::new(
NotificationId::unique::<CopilotStartingToast>(),
"Copilot is starting...",
),
cx,
);
workspace.weak_handle()
}) else {
return;
};
cx.spawn(|mut cx| async move {
task.await;
if let Some(copilot) = cx.update(|cx| Copilot::global(cx)).ok().flatten() {
workspace
.update(&mut cx, |workspace, cx| match copilot.read(cx).status() {
Status::Authorized => workspace.show_toast(
Toast::new(
NotificationId::unique::<CopilotStartingToast>(),
"Copilot has started!",
),
cx,
),
_ => {
workspace.dismiss_toast(
&NotificationId::unique::<CopilotStartingToast>(),
cx,
);
copilot
.update(cx, |copilot, cx| copilot.sign_in(cx))
.detach_and_log_err(cx);
}
})
.log_err();
}
})
.detach();
}
_ => {
copilot.update(cx, |this, cx| this.sign_in(cx)).detach();
workspace
.update(cx, |this, cx| {
this.toggle_modal(cx, |cx| CopilotCodeVerification::new(&copilot, cx));
})
.ok();
}
}
}

View file

@ -31,6 +31,7 @@ async-watch.workspace = true
clock.workspace = true
collections.workspace = true
ec4rs.workspace = true
fs.workspace = true
futures.workspace = true
fuzzy.workspace = true
git.workspace = true

View file

@ -21,6 +21,7 @@ use async_watch as watch;
use clock::Lamport;
pub use clock::ReplicaId;
use collections::HashMap;
use fs::MTime;
use futures::channel::oneshot;
use gpui::{
AnyElement, AppContext, Context as _, EventEmitter, HighlightStyle, Model, ModelContext,
@ -51,7 +52,7 @@ use std::{
path::{Path, PathBuf},
str,
sync::{Arc, LazyLock},
time::{Duration, Instant, SystemTime},
time::{Duration, Instant},
vec,
};
use sum_tree::TreeMap;
@ -108,7 +109,7 @@ pub struct Buffer {
file: Option<Arc<dyn File>>,
/// The mtime of the file when this buffer was last loaded from
/// or saved to disk.
saved_mtime: Option<SystemTime>,
saved_mtime: Option<MTime>,
/// The version vector when this buffer was last loaded from
/// or saved to disk.
saved_version: clock::Global,
@ -406,22 +407,19 @@ pub trait File: Send + Sync {
/// modified. In the case where the file is not stored, it can be either `New` or `Deleted`. In the
/// UI these two states are distinguished. For example, the buffer tab does not display a deletion
/// indicator for new files.
#[derive(Clone, Copy, Debug, PartialEq)]
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum DiskState {
/// File created in Zed that has not been saved.
New,
/// File present on the filesystem.
Present {
/// Last known mtime (modification time).
mtime: SystemTime,
},
Present { mtime: MTime },
/// Deleted file that was previously present.
Deleted,
}
impl DiskState {
/// Returns the file's last known modification time on disk.
pub fn mtime(self) -> Option<SystemTime> {
pub fn mtime(self) -> Option<MTime> {
match self {
DiskState::New => None,
DiskState::Present { mtime } => Some(mtime),
@ -976,7 +974,7 @@ impl Buffer {
}
/// The mtime of the buffer's file when the buffer was last saved or reloaded from disk.
pub fn saved_mtime(&self) -> Option<SystemTime> {
pub fn saved_mtime(&self) -> Option<MTime> {
self.saved_mtime
}
@ -1011,7 +1009,7 @@ impl Buffer {
pub fn did_save(
&mut self,
version: clock::Global,
mtime: Option<SystemTime>,
mtime: Option<MTime>,
cx: &mut ModelContext<Self>,
) {
self.saved_version = version;
@ -1077,7 +1075,7 @@ impl Buffer {
&mut self,
version: clock::Global,
line_ending: LineEnding,
mtime: Option<SystemTime>,
mtime: Option<MTime>,
cx: &mut ModelContext<Self>,
) {
self.saved_version = version;
@ -1777,7 +1775,9 @@ impl Buffer {
match file.disk_state() {
DiskState::New => false,
DiskState::Present { mtime } => match self.saved_mtime {
Some(saved_mtime) => mtime > saved_mtime && self.has_unsaved_edits(),
Some(saved_mtime) => {
mtime.bad_is_greater_than(saved_mtime) && self.has_unsaved_edits()
}
None => true,
},
DiskState::Deleted => true,

View file

@ -20,6 +20,8 @@ pub struct Toolchain {
pub name: SharedString,
pub path: SharedString,
pub language_name: LanguageName,
/// Full toolchain data (including language-specific details)
pub as_json: serde_json::Value,
}
#[async_trait(?Send)]
@ -29,6 +31,8 @@ pub trait ToolchainLister: Send + Sync {
worktree_root: PathBuf,
project_env: Option<HashMap<String, String>>,
) -> ToolchainList;
// Returns a term which we should use in UI to refer to a toolchain.
fn term(&self) -> SharedString;
}
#[async_trait(?Send)]

View file

@ -13,57 +13,31 @@ path = "src/language_model.rs"
doctest = false
[features]
test-support = [
"editor/test-support",
"language/test-support",
"project/test-support",
"text/test-support",
]
test-support = []
[dependencies]
anthropic = { workspace = true, features = ["schemars"] }
anyhow.workspace = true
client.workspace = true
base64.workspace = true
collections.workspace = true
copilot = { workspace = true, features = ["schemars"] }
editor.workspace = true
feature_flags.workspace = true
futures.workspace = true
google_ai = { workspace = true, features = ["schemars"] }
gpui.workspace = true
http_client.workspace = true
inline_completion_button.workspace = true
image.workspace = true
log.workspace = true
menu.workspace = true
ollama = { workspace = true, features = ["schemars"] }
open_ai = { workspace = true, features = ["schemars"] }
parking_lot.workspace = true
proto.workspace = true
project.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
smol.workspace = true
strum.workspace = true
telemetry.workspace = true
theme.workspace = true
thiserror.workspace = true
tiktoken-rs.workspace = true
ui.workspace = true
util.workspace = true
base64.workspace = true
image.workspace = true
[dev-dependencies]
ctor.workspace = true
editor = { workspace = true, features = ["test-support"] }
env_logger.workspace = true
language = { workspace = true, features = ["test-support"] }
log.workspace = true
project = { workspace = true, features = ["test-support"] }
proto = { workspace = true, features = ["test-support"] }
rand.workspace = true
text = { workspace = true, features = ["test-support"] }
unindent.workspace = true
gpui = { workspace = true, features = ["test-support"] }

View file

@ -1,23 +1,19 @@
pub mod logging;
mod model;
pub mod provider;
mod rate_limiter;
mod registry;
mod request;
mod role;
pub mod settings;
#[cfg(any(test, feature = "test-support"))]
pub mod fake_provider;
use anyhow::Result;
use client::{Client, UserStore};
use futures::FutureExt;
use futures::{future::BoxFuture, stream::BoxStream, StreamExt, TryStreamExt as _};
use gpui::{
AnyElement, AnyView, AppContext, AsyncAppContext, Model, SharedString, Task, WindowContext,
};
use gpui::{AnyElement, AnyView, AppContext, AsyncAppContext, SharedString, Task, WindowContext};
pub use model::*;
use project::Fs;
use proto::Plan;
pub(crate) use rate_limiter::*;
pub use rate_limiter::*;
pub use registry::*;
pub use request::*;
pub use role::*;
@ -27,14 +23,10 @@ use std::fmt;
use std::{future::Future, sync::Arc};
use ui::IconName;
pub fn init(
user_store: Model<UserStore>,
client: Arc<Client>,
fs: Arc<dyn Fs>,
cx: &mut AppContext,
) {
settings::init(fs, cx);
registry::init(user_store, client, cx);
pub const ZED_CLOUD_PROVIDER_ID: &str = "zed.dev";
pub fn init(cx: &mut AppContext) {
registry::init(cx);
}
/// The availability of a [`LanguageModel`].
@ -184,7 +176,7 @@ pub trait LanguageModel: Send + Sync {
}
#[cfg(any(test, feature = "test-support"))]
fn as_fake(&self) -> &provider::fake::FakeLanguageModel {
fn as_fake(&self) -> &fake_provider::FakeLanguageModel {
unimplemented!()
}
}

View file

@ -1,76 +1,17 @@
use crate::provider::cloud::RefreshLlmTokenListener;
use crate::{
provider::{
anthropic::AnthropicLanguageModelProvider, cloud::CloudLanguageModelProvider,
copilot_chat::CopilotChatLanguageModelProvider, google::GoogleLanguageModelProvider,
ollama::OllamaLanguageModelProvider, open_ai::OpenAiLanguageModelProvider,
},
LanguageModel, LanguageModelId, LanguageModelProvider, LanguageModelProviderId,
LanguageModelProviderState,
};
use client::{Client, UserStore};
use collections::BTreeMap;
use gpui::{AppContext, EventEmitter, Global, Model, ModelContext};
use std::sync::Arc;
use ui::Context;
pub fn init(user_store: Model<UserStore>, client: Arc<Client>, cx: &mut AppContext) {
let registry = cx.new_model(|cx| {
let mut registry = LanguageModelRegistry::default();
register_language_model_providers(&mut registry, user_store, client, cx);
registry
});
pub fn init(cx: &mut AppContext) {
let registry = cx.new_model(|_cx| LanguageModelRegistry::default());
cx.set_global(GlobalLanguageModelRegistry(registry));
}
fn register_language_model_providers(
registry: &mut LanguageModelRegistry,
user_store: Model<UserStore>,
client: Arc<Client>,
cx: &mut ModelContext<LanguageModelRegistry>,
) {
use feature_flags::FeatureFlagAppExt;
RefreshLlmTokenListener::register(client.clone(), cx);
registry.register_provider(
AnthropicLanguageModelProvider::new(client.http_client(), cx),
cx,
);
registry.register_provider(
OpenAiLanguageModelProvider::new(client.http_client(), cx),
cx,
);
registry.register_provider(
OllamaLanguageModelProvider::new(client.http_client(), cx),
cx,
);
registry.register_provider(
GoogleLanguageModelProvider::new(client.http_client(), cx),
cx,
);
registry.register_provider(CopilotChatLanguageModelProvider::new(cx), cx);
cx.observe_flag::<feature_flags::LanguageModels, _>(move |enabled, cx| {
let user_store = user_store.clone();
let client = client.clone();
LanguageModelRegistry::global(cx).update(cx, move |registry, cx| {
if enabled {
registry.register_provider(
CloudLanguageModelProvider::new(user_store.clone(), client.clone(), cx),
cx,
);
} else {
registry.unregister_provider(
LanguageModelProviderId::from(crate::provider::cloud::PROVIDER_ID.to_string()),
cx,
);
}
});
})
.detach();
}
struct GlobalLanguageModelRegistry(Model<LanguageModelRegistry>);
impl Global for GlobalLanguageModelRegistry {}
@ -106,8 +47,8 @@ impl LanguageModelRegistry {
}
#[cfg(any(test, feature = "test-support"))]
pub fn test(cx: &mut AppContext) -> crate::provider::fake::FakeLanguageModelProvider {
let fake_provider = crate::provider::fake::FakeLanguageModelProvider;
pub fn test(cx: &mut AppContext) -> crate::fake_provider::FakeLanguageModelProvider {
let fake_provider = crate::fake_provider::FakeLanguageModelProvider;
let registry = cx.new_model(|cx| {
let mut registry = Self::default();
registry.register_provider(fake_provider.clone(), cx);
@ -148,7 +89,7 @@ impl LanguageModelRegistry {
}
pub fn providers(&self) -> Vec<Arc<dyn LanguageModelProvider>> {
let zed_provider_id = LanguageModelProviderId(crate::provider::cloud::PROVIDER_ID.into());
let zed_provider_id = LanguageModelProviderId("zed.dev".into());
let mut providers = Vec::with_capacity(self.providers.len());
if let Some(provider) = self.providers.get(&zed_provider_id) {
providers.push(provider.clone());
@ -269,7 +210,7 @@ impl LanguageModelRegistry {
#[cfg(test)]
mod tests {
use super::*;
use crate::provider::fake::FakeLanguageModelProvider;
use crate::fake_provider::FakeLanguageModelProvider;
#[gpui::test]
fn test_register_providers(cx: &mut AppContext) {
@ -281,10 +222,10 @@ mod tests {
let providers = registry.read(cx).providers();
assert_eq!(providers.len(), 1);
assert_eq!(providers[0].id(), crate::provider::fake::provider_id());
assert_eq!(providers[0].id(), crate::fake_provider::provider_id());
registry.update(cx, |registry, cx| {
registry.unregister_provider(crate::provider::fake::provider_id(), cx);
registry.unregister_provider(crate::fake_provider::provider_id(), cx);
});
let providers = registry.read(cx).providers();

View file

@ -0,0 +1,49 @@
[package]
name = "language_models"
version = "0.1.0"
edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/language_models.rs"
[dependencies]
anthropic = { workspace = true, features = ["schemars"] }
anyhow.workspace = true
client.workspace = true
collections.workspace = true
copilot = { workspace = true, features = ["schemars"] }
editor.workspace = true
feature_flags.workspace = true
fs.workspace = true
futures.workspace = true
google_ai = { workspace = true, features = ["schemars"] }
gpui.workspace = true
http_client.workspace = true
language_model.workspace = true
menu.workspace = true
ollama = { workspace = true, features = ["schemars"] }
open_ai = { workspace = true, features = ["schemars"] }
project.workspace = true
proto.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
smol.workspace = true
strum.workspace = true
telemetry.workspace = true
theme.workspace = true
thiserror.workspace = true
tiktoken-rs.workspace = true
ui.workspace = true
util.workspace = true
[dev-dependencies]
editor = { workspace = true, features = ["test-support"] }
language_model = { workspace = true, features = ["test-support"] }
project = { workspace = true, features = ["test-support"] }

View file

@ -0,0 +1 @@
../../LICENSE-GPL

View file

@ -0,0 +1,80 @@
use std::sync::Arc;
use client::{Client, UserStore};
use fs::Fs;
use gpui::{AppContext, Model, ModelContext};
use language_model::{LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
mod logging;
pub mod provider;
mod settings;
use crate::provider::anthropic::AnthropicLanguageModelProvider;
use crate::provider::cloud::{CloudLanguageModelProvider, RefreshLlmTokenListener};
use crate::provider::copilot_chat::CopilotChatLanguageModelProvider;
use crate::provider::google::GoogleLanguageModelProvider;
use crate::provider::ollama::OllamaLanguageModelProvider;
use crate::provider::open_ai::OpenAiLanguageModelProvider;
pub use crate::settings::*;
pub use logging::report_assistant_event;
pub fn init(
user_store: Model<UserStore>,
client: Arc<Client>,
fs: Arc<dyn Fs>,
cx: &mut AppContext,
) {
crate::settings::init(fs, cx);
let registry = LanguageModelRegistry::global(cx);
registry.update(cx, |registry, cx| {
register_language_model_providers(registry, user_store, client, cx);
});
}
fn register_language_model_providers(
registry: &mut LanguageModelRegistry,
user_store: Model<UserStore>,
client: Arc<Client>,
cx: &mut ModelContext<LanguageModelRegistry>,
) {
use feature_flags::FeatureFlagAppExt;
RefreshLlmTokenListener::register(client.clone(), cx);
registry.register_provider(
AnthropicLanguageModelProvider::new(client.http_client(), cx),
cx,
);
registry.register_provider(
OpenAiLanguageModelProvider::new(client.http_client(), cx),
cx,
);
registry.register_provider(
OllamaLanguageModelProvider::new(client.http_client(), cx),
cx,
);
registry.register_provider(
GoogleLanguageModelProvider::new(client.http_client(), cx),
cx,
);
registry.register_provider(CopilotChatLanguageModelProvider::new(cx), cx);
cx.observe_flag::<feature_flags::LanguageModels, _>(move |enabled, cx| {
let user_store = user_store.clone();
let client = client.clone();
LanguageModelRegistry::global(cx).update(cx, move |registry, cx| {
if enabled {
registry.register_provider(
CloudLanguageModelProvider::new(user_store.clone(), client.clone(), cx),
cx,
);
} else {
registry.unregister_provider(
LanguageModelProviderId::from(ZED_CLOUD_PROVIDER_ID.to_string()),
cx,
);
}
});
})
.detach();
}

View file

@ -1,8 +1,6 @@
pub mod anthropic;
pub mod cloud;
pub mod copilot_chat;
#[cfg(any(test, feature = "test-support"))]
pub mod fake;
pub mod google;
pub mod ollama;
pub mod open_ai;

View file

@ -1,9 +1,4 @@
use crate::{
settings::AllLanguageModelSettings, LanguageModel, LanguageModelCacheConfiguration,
LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest, RateLimiter, Role,
};
use crate::{LanguageModelCompletionEvent, LanguageModelToolUse, StopReason};
use crate::AllLanguageModelSettings;
use anthropic::{AnthropicError, ContentDelta, Event, ResponseContent};
use anyhow::{anyhow, Context as _, Result};
use collections::{BTreeMap, HashMap};
@ -15,6 +10,12 @@ use gpui::{
View, WhiteSpace,
};
use http_client::HttpClient;
use language_model::{
LanguageModel, LanguageModelCacheConfiguration, LanguageModelId, LanguageModelName,
LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
LanguageModelProviderState, LanguageModelRequest, RateLimiter, Role,
};
use language_model::{LanguageModelCompletionEvent, LanguageModelToolUse, StopReason};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore};
@ -256,7 +257,7 @@ pub fn count_anthropic_tokens(
let mut string_messages = Vec::with_capacity(messages.len());
for message in messages {
use crate::MessageContent;
use language_model::MessageContent;
let mut string_contents = String::new();

View file

@ -1,10 +1,4 @@
use super::open_ai::count_open_ai_tokens;
use crate::provider::anthropic::map_to_language_model_completion_events;
use crate::{
settings::AllLanguageModelSettings, CloudModel, LanguageModel, LanguageModelCacheConfiguration,
LanguageModelId, LanguageModelName, LanguageModelProviderId, LanguageModelProviderName,
LanguageModelProviderState, LanguageModelRequest, RateLimiter,
};
use anthropic::AnthropicError;
use anyhow::{anyhow, Result};
use client::{
@ -22,6 +16,14 @@ use gpui::{
ModelContext, ReadGlobal, Subscription, Task,
};
use http_client::{AsyncBody, HttpClient, Method, Response, StatusCode};
use language_model::{
CloudModel, LanguageModel, LanguageModelCacheConfiguration, LanguageModelId, LanguageModelName,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
LanguageModelRequest, RateLimiter, ZED_CLOUD_PROVIDER_ID,
};
use language_model::{
LanguageModelAvailability, LanguageModelCompletionEvent, LanguageModelProvider,
};
use proto::TypedEnvelope;
use schemars::JsonSchema;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
@ -40,11 +42,11 @@ use strum::IntoEnumIterator;
use thiserror::Error;
use ui::{prelude::*, TintColor};
use crate::{LanguageModelAvailability, LanguageModelCompletionEvent, LanguageModelProvider};
use crate::provider::anthropic::map_to_language_model_completion_events;
use crate::AllLanguageModelSettings;
use super::anthropic::count_anthropic_tokens;
pub const PROVIDER_ID: &str = "zed.dev";
pub const PROVIDER_NAME: &str = "Zed";
const ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: Option<&str> =
@ -255,7 +257,7 @@ impl LanguageModelProviderState for CloudLanguageModelProvider {
impl LanguageModelProvider for CloudLanguageModelProvider {
fn id(&self) -> LanguageModelProviderId {
LanguageModelProviderId(PROVIDER_ID.into())
LanguageModelProviderId(ZED_CLOUD_PROVIDER_ID.into())
}
fn name(&self) -> LanguageModelProviderName {
@ -535,7 +537,7 @@ impl LanguageModel for CloudLanguageModel {
}
fn provider_id(&self) -> LanguageModelProviderId {
LanguageModelProviderId(PROVIDER_ID.into())
LanguageModelProviderId(ZED_CLOUD_PROVIDER_ID.into())
}
fn provider_name(&self) -> LanguageModelProviderName {

View file

@ -14,6 +14,11 @@ use gpui::{
percentage, svg, Animation, AnimationExt, AnyView, AppContext, AsyncAppContext, Model, Render,
Subscription, Task, Transformation,
};
use language_model::{
LanguageModel, LanguageModelCompletionEvent, LanguageModelId, LanguageModelName,
LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
LanguageModelProviderState, LanguageModelRequest, RateLimiter, Role,
};
use settings::SettingsStore;
use std::time::Duration;
use strum::IntoEnumIterator;
@ -23,12 +28,6 @@ use ui::{
ViewContext, VisualContext, WindowContext,
};
use crate::{
LanguageModel, LanguageModelId, LanguageModelName, LanguageModelProvider,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelRequest, RateLimiter, Role,
};
use crate::{LanguageModelCompletionEvent, LanguageModelProviderState};
use super::anthropic::count_anthropic_tokens;
use super::open_ai::count_open_ai_tokens;
@ -383,9 +382,7 @@ impl Render for ConfigurationView {
.icon_size(IconSize::Medium)
.style(ui::ButtonStyle::Filled)
.full_width()
.on_click(|_, cx| {
inline_completion_button::initiate_sign_in(cx)
}),
.on_click(|_, cx| copilot::initiate_sign_in(cx)),
)
.child(
div().flex().w_full().items_center().child(

View file

@ -8,6 +8,12 @@ use gpui::{
View, WhiteSpace,
};
use http_client::HttpClient;
use language_model::LanguageModelCompletionEvent;
use language_model::{
LanguageModel, LanguageModelId, LanguageModelName, LanguageModelProvider,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
LanguageModelRequest, RateLimiter,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore};
@ -17,12 +23,7 @@ use theme::ThemeSettings;
use ui::{prelude::*, Icon, IconName, Tooltip};
use util::ResultExt;
use crate::LanguageModelCompletionEvent;
use crate::{
settings::AllLanguageModelSettings, LanguageModel, LanguageModelId, LanguageModelName,
LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
LanguageModelProviderState, LanguageModelRequest, RateLimiter,
};
use crate::AllLanguageModelSettings;
const PROVIDER_ID: &str = "google";
const PROVIDER_NAME: &str = "Google AI";

View file

@ -2,6 +2,12 @@ use anyhow::{anyhow, bail, Result};
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
use gpui::{AnyView, AppContext, AsyncAppContext, ModelContext, Subscription, Task};
use http_client::HttpClient;
use language_model::LanguageModelCompletionEvent;
use language_model::{
LanguageModel, LanguageModelId, LanguageModelName, LanguageModelProvider,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
LanguageModelRequest, RateLimiter, Role,
};
use ollama::{
get_models, preload_model, stream_chat_completion, ChatMessage, ChatOptions, ChatRequest,
ChatResponseDelta, KeepAlive, OllamaToolCall,
@ -13,12 +19,7 @@ use std::{collections::BTreeMap, sync::Arc};
use ui::{prelude::*, ButtonLike, Indicator};
use util::ResultExt;
use crate::LanguageModelCompletionEvent;
use crate::{
settings::AllLanguageModelSettings, LanguageModel, LanguageModelId, LanguageModelName,
LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
LanguageModelProviderState, LanguageModelRequest, RateLimiter, Role,
};
use crate::AllLanguageModelSettings;
const OLLAMA_DOWNLOAD_URL: &str = "https://ollama.com/download";
const OLLAMA_LIBRARY_URL: &str = "https://ollama.com/library";

View file

@ -7,6 +7,11 @@ use gpui::{
View, WhiteSpace,
};
use http_client::HttpClient;
use language_model::{
LanguageModel, LanguageModelCompletionEvent, LanguageModelId, LanguageModelName,
LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
LanguageModelProviderState, LanguageModelRequest, RateLimiter, Role,
};
use open_ai::{
stream_completion, FunctionDefinition, ResponseStreamEvent, ToolChoice, ToolDefinition,
};
@ -19,12 +24,7 @@ use theme::ThemeSettings;
use ui::{prelude::*, Icon, IconName, Tooltip};
use util::ResultExt;
use crate::LanguageModelCompletionEvent;
use crate::{
settings::AllLanguageModelSettings, LanguageModel, LanguageModelId, LanguageModelName,
LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
LanguageModelProviderState, LanguageModelRequest, RateLimiter, Role,
};
use crate::AllLanguageModelSettings;
const PROVIDER_ID: &str = "openai";
const PROVIDER_NAME: &str = "OpenAI";

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