Fix Warping indicator stuck after Bedrock LLM finishes responding
Bedrock was calling suggest_next_prompt tool which created a SuggestPrompt action that waited forever on a oneshot channel for UI interaction that never fires in the Bedrock path, keeping the conversation permanently InProgress. Fixed by filtering the tool from the Bedrock tool list and skipping it at the stream level when the LLM calls it from context history. Also includes: Bedrock cache token tracking, cost estimation, LSP improvements, conversation usage view updates, and external config support. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
95b4708e44
commit
f37a744692
File diff suppressed because one or more lines are too long
@@ -68,6 +68,18 @@ Global rules are loaded from `~/.galaxy-ai/rules/*.md` (filename = rule name, co
|
|||||||
|
|
||||||
Project rules are loaded from `GALAXY.md` or `AGENTS.md` files found in the project directory tree.
|
Project rules are loaded from `GALAXY.md` or `AGENTS.md` files found in the project directory tree.
|
||||||
|
|
||||||
|
**External Config Fallback** (`app/src/ai/bedrock/external_config.rs`):
|
||||||
|
When Galaxy's own Bedrock settings are at defaults, it falls back to configurations from:
|
||||||
|
1. **Claude Code** (`~/.claude/settings.json`) — reads `env.AWS_PROFILE`, `env.AWS_REGION`, and `env.DCP_MODEL_MAP` (ARN-based model mappings)
|
||||||
|
2. **OpenCode** (`~/.config/opencode/opencode.json`) — reads `provider.amazon-bedrock.options.profile` and `.region`
|
||||||
|
|
||||||
|
Priority: Galaxy explicit settings > Claude Code > OpenCode > hardcoded defaults. Fallback only applies when profile is `"default"` (for profile) or empty (for region/models). External model ARNs are merged with Galaxy's built-in default model list.
|
||||||
|
|
||||||
|
**Token Usage & Cost Tracking** (`app/src/ai/bedrock/stream.rs`):
|
||||||
|
The Bedrock stream extracts full token metadata from responses: `input_tokens`, `output_tokens`, `cache_read_input_tokens`, `cache_write_input_tokens`. These flow through `build_stream_finished` → `TokenUsage` struct → `conversation.update_cost_and_usage_for_request()`. Cost is estimated using Sonnet-tier Bedrock pricing as a conservative default. Displayed in:
|
||||||
|
- **Agent management cards** — total token count in metadata row
|
||||||
|
- **Conversation usage footer** — full breakdown (input/output/cache read/cache write) + estimated cost
|
||||||
|
|
||||||
### Key Architectural Patterns
|
### Key Architectural Patterns
|
||||||
|
|
||||||
1. **Entity-Handle System**: Views reference other views via handles, not direct ownership
|
1. **Entity-Handle System**: Views reference other views via handles, not direct ownership
|
||||||
|
|||||||
@@ -3062,6 +3062,20 @@ impl AIConversation {
|
|||||||
self.total_token_usage_by_model.values().cloned().collect()
|
self.total_token_usage_by_model.values().cloned().collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn total_tokens(&self) -> u32 {
|
||||||
|
self.total_token_usage_by_model
|
||||||
|
.values()
|
||||||
|
.map(Self::total_tokens_for_usage)
|
||||||
|
.sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn total_cost_cents(&self) -> f32 {
|
||||||
|
self.total_token_usage_by_model
|
||||||
|
.values()
|
||||||
|
.map(|u| u.cost_in_cents)
|
||||||
|
.sum()
|
||||||
|
}
|
||||||
|
|
||||||
fn total_tokens_for_usage(usage: &TokenUsage) -> u32 {
|
fn total_tokens_for_usage(usage: &TokenUsage) -> u32 {
|
||||||
usage.total_input + usage.output + usage.input_cache_read + usage.input_cache_write
|
usage.total_input + usage.output + usage.input_cache_read + usage.input_cache_write
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ use crate::ai::agent::conversation::{AIConversationId, ConversationStatus};
|
|||||||
use crate::ai::ambient_agents::AmbientAgentTaskId;
|
use crate::ai::ambient_agents::AmbientAgentTaskId;
|
||||||
use crate::ai::ambient_agents::{AgentSource, AmbientAgentTask, AmbientAgentTaskState};
|
use crate::ai::ambient_agents::{AgentSource, AmbientAgentTask, AmbientAgentTaskState};
|
||||||
use crate::ai::artifacts::Artifact;
|
use crate::ai::artifacts::Artifact;
|
||||||
use crate::ai::blocklist::{format_credits, BlocklistAIHistoryEvent, BlocklistAIHistoryModel};
|
use crate::ai::blocklist::{
|
||||||
|
format_credits, format_token_count, BlocklistAIHistoryEvent, BlocklistAIHistoryModel,
|
||||||
|
};
|
||||||
use crate::ai::cloud_environments::CloudAmbientAgentEnvironment;
|
use crate::ai::cloud_environments::CloudAmbientAgentEnvironment;
|
||||||
use crate::ai::conversation_navigation::ConversationNavigationData;
|
use crate::ai::conversation_navigation::ConversationNavigationData;
|
||||||
use crate::auth::auth_manager::{AuthManager, AuthManagerEvent};
|
use crate::auth::auth_manager::{AuthManager, AuthManagerEvent};
|
||||||
@@ -511,6 +513,20 @@ impl ConversationOrTask<'_> {
|
|||||||
self.request_usage(app).map(format_credits)
|
self.request_usage(app).map(format_credits)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn display_total_tokens(&self, app: &AppContext) -> Option<String> {
|
||||||
|
match self {
|
||||||
|
ConversationOrTask::Task(_) => None,
|
||||||
|
ConversationOrTask::Conversation(metadata) => {
|
||||||
|
let history_model = BlocklistAIHistoryModel::as_ref(app);
|
||||||
|
history_model
|
||||||
|
.conversation(&metadata.nav_data.id)
|
||||||
|
.map(|conv| conv.total_tokens())
|
||||||
|
.filter(|&t| t > 0)
|
||||||
|
.map(format_token_count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn last_updated(&self) -> DateTime<Utc> {
|
pub fn last_updated(&self) -> DateTime<Utc> {
|
||||||
match self {
|
match self {
|
||||||
ConversationOrTask::Task(task) => task.updated_at,
|
ConversationOrTask::Task(task) => task.updated_at,
|
||||||
|
|||||||
@@ -1824,6 +1824,10 @@ impl AgentManagementView {
|
|||||||
metadata_parts.push(format!("Credits used: {usage}"));
|
metadata_parts.push(format!("Credits used: {usage}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(tokens) = card_data.display_total_tokens(app) {
|
||||||
|
metadata_parts.push(format!("Tokens: {tokens}"));
|
||||||
|
}
|
||||||
|
|
||||||
let metadata_text = metadata_parts.join(" • ");
|
let metadata_text = metadata_parts.join(" • ");
|
||||||
|
|
||||||
Text::new(metadata_text, font_family, font_size)
|
Text::new(metadata_text, font_family, font_size)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use aws_sdk_bedrockruntime::Client as BedrockRuntimeClient;
|
|||||||
|
|
||||||
use crate::settings::ai::BedrockAuthMethod;
|
use crate::settings::ai::BedrockAuthMethod;
|
||||||
|
|
||||||
|
use super::external_config::ExternalBedrockConfig;
|
||||||
use super::convert::{build_converse_request, ConversationMessage, ToolDefinition};
|
use super::convert::{build_converse_request, ConversationMessage, ToolDefinition};
|
||||||
use super::diagnostic::BedrockDiagnosticLogger;
|
use super::diagnostic::BedrockDiagnosticLogger;
|
||||||
use super::models::apply_cross_region_prefix;
|
use super::models::apply_cross_region_prefix;
|
||||||
@@ -29,6 +30,33 @@ pub struct BedrockClientConfig {
|
|||||||
pub fallback_to_warp: bool,
|
pub fallback_to_warp: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl BedrockClientConfig {
|
||||||
|
/// Applies external config (from Claude Code / OpenCode) as fallback values
|
||||||
|
/// when Galaxy's own settings are at their defaults.
|
||||||
|
pub fn with_external_fallbacks(mut self) -> Self {
|
||||||
|
let external = ExternalBedrockConfig::load();
|
||||||
|
if external.is_empty() {
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.profile == "default" {
|
||||||
|
if let Some(profile) = external.profile {
|
||||||
|
log::info!("[bedrock] Using profile from external config: {profile}");
|
||||||
|
self.profile = profile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.region.is_empty() {
|
||||||
|
if let Some(region) = external.region {
|
||||||
|
log::info!("[bedrock] Using region from external config: {region}");
|
||||||
|
self.region = region;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum BedrockError {
|
pub enum BedrockError {
|
||||||
#[error("Bedrock credentials not configured")]
|
#[error("Bedrock credentials not configured")]
|
||||||
|
|||||||
@@ -872,6 +872,11 @@ pub fn extract_tools(request: &api::Request) -> Vec<ToolDefinition> {
|
|||||||
tools = default_tool_definitions();
|
tools = default_tool_definitions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter out suggest_next_prompt — its action executor waits on a oneshot
|
||||||
|
// channel for UI interaction that never fires in the Bedrock path, causing
|
||||||
|
// the conversation to stay InProgress forever.
|
||||||
|
tools.retain(|t| t.name != "suggest_next_prompt");
|
||||||
|
|
||||||
tools
|
tools
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -963,7 +968,6 @@ fn default_tool_definitions() -> Vec<ToolDefinition> {
|
|||||||
tool_definition_for_name("apply_file_diffs"),
|
tool_definition_for_name("apply_file_diffs"),
|
||||||
tool_definition_for_name("grep"),
|
tool_definition_for_name("grep"),
|
||||||
tool_definition_for_name("file_glob"),
|
tool_definition_for_name("file_glob"),
|
||||||
tool_definition_for_name("suggest_next_prompt"),
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,229 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use crate::settings::ai::BedrockModelConfig;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct ExternalBedrockConfig {
|
||||||
|
pub profile: Option<String>,
|
||||||
|
pub region: Option<String>,
|
||||||
|
pub models: Vec<BedrockModelConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExternalBedrockConfig {
|
||||||
|
pub fn load() -> Self {
|
||||||
|
let claude_config = load_claude_code_config();
|
||||||
|
let opencode_config = load_opencode_config();
|
||||||
|
|
||||||
|
// Claude Code takes priority over OpenCode since it has richer model mappings
|
||||||
|
Self {
|
||||||
|
profile: claude_config
|
||||||
|
.profile
|
||||||
|
.or(opencode_config.profile),
|
||||||
|
region: claude_config
|
||||||
|
.region
|
||||||
|
.or(opencode_config.region),
|
||||||
|
models: if claude_config.models.is_empty() {
|
||||||
|
opencode_config.models
|
||||||
|
} else {
|
||||||
|
claude_config.models
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.profile.is_none() && self.region.is_none() && self.models.is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_claude_code_config() -> ExternalBedrockConfig {
|
||||||
|
let path = dirs::home_dir()
|
||||||
|
.map(|h| h.join(".claude").join("settings.json"))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
parse_claude_code_config(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_claude_code_config(path: PathBuf) -> ExternalBedrockConfig {
|
||||||
|
let contents = match std::fs::read_to_string(&path) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(_) => return ExternalBedrockConfig::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let json: serde_json::Value = match serde_json::from_str(&contents) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => return ExternalBedrockConfig::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let env = match json.get("env").and_then(|v| v.as_object()) {
|
||||||
|
Some(e) => e,
|
||||||
|
None => return ExternalBedrockConfig::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let profile = env
|
||||||
|
.get("AWS_PROFILE")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(|s| s.to_string());
|
||||||
|
|
||||||
|
let region = env
|
||||||
|
.get("AWS_REGION")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(|s| s.to_string());
|
||||||
|
|
||||||
|
let models = parse_claude_code_model_map(env);
|
||||||
|
|
||||||
|
ExternalBedrockConfig {
|
||||||
|
profile,
|
||||||
|
region,
|
||||||
|
models,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_claude_code_model_map(
|
||||||
|
env: &serde_json::Map<String, serde_json::Value>,
|
||||||
|
) -> Vec<BedrockModelConfig> {
|
||||||
|
let model_map_str = match env.get("DCP_MODEL_MAP").and_then(|v| v.as_str()) {
|
||||||
|
Some(s) => s,
|
||||||
|
None => return Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let model_map: HashMap<String, String> = match serde_json::from_str(model_map_str) {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(_) => return Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
model_map
|
||||||
|
.into_iter()
|
||||||
|
.map(|(source_id, arn)| {
|
||||||
|
let display_name = derive_display_name(&source_id);
|
||||||
|
BedrockModelConfig {
|
||||||
|
model_id: arn,
|
||||||
|
display_name,
|
||||||
|
vision_supported: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn derive_display_name(model_id: &str) -> String {
|
||||||
|
// Strip region prefix (e.g. "us." from "us.anthropic.claude-opus-4-6-v1")
|
||||||
|
let id = model_id
|
||||||
|
.strip_prefix("us.")
|
||||||
|
.or_else(|| model_id.strip_prefix("eu."))
|
||||||
|
.or_else(|| model_id.strip_prefix("ap."))
|
||||||
|
.or_else(|| model_id.strip_prefix("global."))
|
||||||
|
.unwrap_or(model_id);
|
||||||
|
|
||||||
|
// Strip vendor prefix
|
||||||
|
let id = id
|
||||||
|
.strip_prefix("anthropic.")
|
||||||
|
.or_else(|| id.strip_prefix("amazon."))
|
||||||
|
.or_else(|| id.strip_prefix("meta."))
|
||||||
|
.unwrap_or(id);
|
||||||
|
|
||||||
|
// Convert model slug to display name
|
||||||
|
// e.g. "claude-opus-4-6-v1" -> "Claude Opus 4.6"
|
||||||
|
// e.g. "claude-sonnet-4-5-20250929-v1:0" -> "Claude Sonnet 4.5"
|
||||||
|
prettify_model_slug(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prettify_model_slug(slug: &str) -> String {
|
||||||
|
// Remove version suffixes like "-v1:0", "-v1", "-v2:0"
|
||||||
|
let slug = slug
|
||||||
|
.split("-v")
|
||||||
|
.next()
|
||||||
|
.unwrap_or(slug);
|
||||||
|
|
||||||
|
// Remove date suffixes like "-20250514" or "-20251001"
|
||||||
|
let parts: Vec<&str> = slug.split('-').collect();
|
||||||
|
let mut cleaned: Vec<&str> = Vec::new();
|
||||||
|
let mut i = 0;
|
||||||
|
while i < parts.len() {
|
||||||
|
// Skip parts that look like dates (8 digits)
|
||||||
|
if parts[i].len() == 8 && parts[i].chars().all(|c| c.is_ascii_digit()) {
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
cleaned.push(parts[i]);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to detect version numbers: consecutive single-digit parts become "X.Y"
|
||||||
|
let mut result = String::new();
|
||||||
|
let mut i = 0;
|
||||||
|
while i < cleaned.len() {
|
||||||
|
if i > 0 {
|
||||||
|
result.push(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this and the next part form a version number (e.g. "4" "6" -> "4.6")
|
||||||
|
if cleaned[i].len() == 1
|
||||||
|
&& cleaned[i].chars().all(|c| c.is_ascii_digit())
|
||||||
|
&& i + 1 < cleaned.len()
|
||||||
|
&& cleaned[i + 1].len() == 1
|
||||||
|
&& cleaned[i + 1].chars().all(|c| c.is_ascii_digit())
|
||||||
|
{
|
||||||
|
result.push_str(cleaned[i]);
|
||||||
|
result.push('.');
|
||||||
|
result.push_str(cleaned[i + 1]);
|
||||||
|
i += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capitalize first letter of each word
|
||||||
|
let word = cleaned[i];
|
||||||
|
let mut chars = word.chars();
|
||||||
|
if let Some(first) = chars.next() {
|
||||||
|
result.push(first.to_ascii_uppercase());
|
||||||
|
result.extend(chars);
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_opencode_config() -> ExternalBedrockConfig {
|
||||||
|
let path = dirs::home_dir()
|
||||||
|
.map(|h| h.join(".config").join("opencode").join("opencode.json"))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
parse_opencode_config(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_opencode_config(path: PathBuf) -> ExternalBedrockConfig {
|
||||||
|
let contents = match std::fs::read_to_string(&path) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(_) => return ExternalBedrockConfig::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let json: serde_json::Value = match serde_json::from_str(&contents) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => return ExternalBedrockConfig::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let bedrock_opts = json
|
||||||
|
.get("provider")
|
||||||
|
.and_then(|p| p.get("amazon-bedrock"))
|
||||||
|
.and_then(|b| b.get("options"));
|
||||||
|
|
||||||
|
let profile = bedrock_opts
|
||||||
|
.and_then(|o| o.get("profile"))
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(|s| s.to_string());
|
||||||
|
|
||||||
|
let region = bedrock_opts
|
||||||
|
.and_then(|o| o.get("region"))
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(|s| s.to_string());
|
||||||
|
|
||||||
|
ExternalBedrockConfig {
|
||||||
|
profile,
|
||||||
|
region,
|
||||||
|
models: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "external_config_tests.rs"]
|
||||||
|
mod tests;
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_claude_code_config_extracts_profile_and_region() {
|
||||||
|
let mut file = NamedTempFile::new().unwrap();
|
||||||
|
write!(
|
||||||
|
file,
|
||||||
|
r#"{{
|
||||||
|
"env": {{
|
||||||
|
"AWS_PROFILE": "coding-assistant",
|
||||||
|
"AWS_REGION": "us-west-2",
|
||||||
|
"CLAUDE_CODE_USE_BEDROCK": "1"
|
||||||
|
}}
|
||||||
|
}}"#
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let config = parse_claude_code_config(file.path().to_path_buf());
|
||||||
|
assert_eq!(config.profile, Some("coding-assistant".to_string()));
|
||||||
|
assert_eq!(config.region, Some("us-west-2".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_claude_code_config_extracts_model_map() {
|
||||||
|
let mut file = NamedTempFile::new().unwrap();
|
||||||
|
write!(
|
||||||
|
file,
|
||||||
|
r#"{{
|
||||||
|
"env": {{
|
||||||
|
"AWS_PROFILE": "test",
|
||||||
|
"AWS_REGION": "us-east-1",
|
||||||
|
"DCP_MODEL_MAP": "{{\"us.anthropic.claude-opus-4-6-v1\": \"arn:aws:bedrock:us-east-1:123456:application-inference-profile/abc123\"}}"
|
||||||
|
}}
|
||||||
|
}}"#
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let config = parse_claude_code_config(file.path().to_path_buf());
|
||||||
|
assert_eq!(config.models.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
config.models[0].model_id,
|
||||||
|
"arn:aws:bedrock:us-east-1:123456:application-inference-profile/abc123"
|
||||||
|
);
|
||||||
|
assert!(config.models[0].vision_supported);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_claude_code_config_missing_file() {
|
||||||
|
let config = parse_claude_code_config(PathBuf::from("/nonexistent/path/settings.json"));
|
||||||
|
assert!(config.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_opencode_config_extracts_bedrock_options() {
|
||||||
|
let mut file = NamedTempFile::new().unwrap();
|
||||||
|
write!(
|
||||||
|
file,
|
||||||
|
r#"{{
|
||||||
|
"provider": {{
|
||||||
|
"amazon-bedrock": {{
|
||||||
|
"options": {{
|
||||||
|
"region": "eu-west-1",
|
||||||
|
"profile": "my-profile"
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}}"#
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let config = parse_opencode_config(file.path().to_path_buf());
|
||||||
|
assert_eq!(config.profile, Some("my-profile".to_string()));
|
||||||
|
assert_eq!(config.region, Some("eu-west-1".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_opencode_config_missing_file() {
|
||||||
|
let config = parse_opencode_config(PathBuf::from("/nonexistent/opencode.json"));
|
||||||
|
assert!(config.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_claude_code_takes_priority_over_opencode() {
|
||||||
|
// When both configs provide a profile, Claude Code wins
|
||||||
|
let claude = ExternalBedrockConfig {
|
||||||
|
profile: Some("claude-profile".to_string()),
|
||||||
|
region: Some("us-east-1".to_string()),
|
||||||
|
models: Vec::new(),
|
||||||
|
};
|
||||||
|
let opencode = ExternalBedrockConfig {
|
||||||
|
profile: Some("opencode-profile".to_string()),
|
||||||
|
region: Some("eu-west-1".to_string()),
|
||||||
|
models: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let merged = ExternalBedrockConfig {
|
||||||
|
profile: claude.profile.or(opencode.profile),
|
||||||
|
region: claude.region.or(opencode.region),
|
||||||
|
models: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(merged.profile, Some("claude-profile".to_string()));
|
||||||
|
assert_eq!(merged.region, Some("us-east-1".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_prettify_model_slug() {
|
||||||
|
assert_eq!(prettify_model_slug("claude-opus-4-6"), "Claude Opus 4.6");
|
||||||
|
assert_eq!(
|
||||||
|
prettify_model_slug("claude-sonnet-4-5-20250929"),
|
||||||
|
"Claude Sonnet 4.5"
|
||||||
|
);
|
||||||
|
assert_eq!(prettify_model_slug("claude-haiku-4-5"), "Claude Haiku 4.5");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_derive_display_name_strips_prefixes() {
|
||||||
|
assert_eq!(
|
||||||
|
derive_display_name("us.anthropic.claude-opus-4-6-v1"),
|
||||||
|
"Claude Opus 4.6"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
derive_display_name("global.anthropic.claude-haiku-4-5-20251001-v1:0"),
|
||||||
|
"Claude Haiku 4.5"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ pub mod convert;
|
|||||||
pub mod convert_request;
|
pub mod convert_request;
|
||||||
pub mod diagnostic;
|
pub mod diagnostic;
|
||||||
pub mod discovery;
|
pub mod discovery;
|
||||||
|
pub mod external_config;
|
||||||
pub mod models;
|
pub mod models;
|
||||||
pub mod stream;
|
pub mod stream;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use crate::settings::ai::BedrockModelConfig;
|
use crate::settings::ai::BedrockModelConfig;
|
||||||
|
|
||||||
|
use super::external_config::ExternalBedrockConfig;
|
||||||
|
|
||||||
pub struct DefaultModel {
|
pub struct DefaultModel {
|
||||||
pub model_id: &'static str,
|
pub model_id: &'static str,
|
||||||
pub display_name: &'static str,
|
pub display_name: &'static str,
|
||||||
@@ -55,18 +57,43 @@ pub const DEFAULT_BEDROCK_MODELS: &[DefaultModel] = &[
|
|||||||
];
|
];
|
||||||
|
|
||||||
pub fn get_effective_models(user_models: &[BedrockModelConfig]) -> Vec<BedrockModelConfig> {
|
pub fn get_effective_models(user_models: &[BedrockModelConfig]) -> Vec<BedrockModelConfig> {
|
||||||
if user_models.is_empty() {
|
if !user_models.is_empty() {
|
||||||
DEFAULT_BEDROCK_MODELS
|
return user_models.to_vec();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to models from external configs (Claude Code / OpenCode)
|
||||||
|
let external = ExternalBedrockConfig::load();
|
||||||
|
if !external.models.is_empty() {
|
||||||
|
log::info!(
|
||||||
|
"[bedrock] Using {} model(s) from external config",
|
||||||
|
external.models.len()
|
||||||
|
);
|
||||||
|
// Merge external models with defaults so the user still sees all defaults
|
||||||
|
let mut models = external.models;
|
||||||
|
let defaults: Vec<BedrockModelConfig> = DEFAULT_BEDROCK_MODELS
|
||||||
.iter()
|
.iter()
|
||||||
.map(|m| BedrockModelConfig {
|
.map(|m| BedrockModelConfig {
|
||||||
model_id: m.model_id.to_string(),
|
model_id: m.model_id.to_string(),
|
||||||
display_name: m.display_name.to_string(),
|
display_name: m.display_name.to_string(),
|
||||||
vision_supported: m.vision_supported,
|
vision_supported: m.vision_supported,
|
||||||
})
|
})
|
||||||
.collect()
|
.collect();
|
||||||
} else {
|
for default in defaults {
|
||||||
user_models.to_vec()
|
if !models.iter().any(|m| m.model_id == default.model_id) {
|
||||||
|
models.push(default);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return models;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DEFAULT_BEDROCK_MODELS
|
||||||
|
.iter()
|
||||||
|
.map(|m| BedrockModelConfig {
|
||||||
|
model_id: m.model_id.to_string(),
|
||||||
|
display_name: m.display_name.to_string(),
|
||||||
|
vision_supported: m.vision_supported,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn apply_cross_region_prefix(model_id: &str, region: &str) -> String {
|
pub fn apply_cross_region_prefix(model_id: &str, region: &str) -> String {
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ pub fn bedrock_stream_to_response_events(
|
|||||||
let mut _has_tool_calls = false;
|
let mut _has_tool_calls = false;
|
||||||
let mut input_tokens: i32 = 0;
|
let mut input_tokens: i32 = 0;
|
||||||
let mut output_tokens: i32 = 0;
|
let mut output_tokens: i32 = 0;
|
||||||
|
let mut cache_read_input_tokens: i32 = 0;
|
||||||
|
let mut cache_write_input_tokens: i32 = 0;
|
||||||
let mut stop_reason = stream_finished::Reason::Done(stream_finished::Done {});
|
let mut stop_reason = stream_finished::Reason::Done(stream_finished::Done {});
|
||||||
|
|
||||||
// Track full assistant text and tool calls for bedrock_message_history
|
// Track full assistant text and tool calls for bedrock_message_history
|
||||||
@@ -155,31 +157,41 @@ pub fn bedrock_stream_to_response_events(
|
|||||||
StreamEvent::ContentBlockStop(_) => {
|
StreamEvent::ContentBlockStop(_) => {
|
||||||
log::info!("[bedrock-debug] Event #{event_count}: ContentBlockStop (tool_use_id={:?})", if current_tool_use_id.is_empty() { "none" } else { ¤t_tool_use_id });
|
log::info!("[bedrock-debug] Event #{event_count}: ContentBlockStop (tool_use_id={:?})", if current_tool_use_id.is_empty() { "none" } else { ¤t_tool_use_id });
|
||||||
if !current_tool_use_id.is_empty() {
|
if !current_tool_use_id.is_empty() {
|
||||||
log::debug!("[bedrock] Tool call complete: {} ({})", current_tool_name, current_tool_use_id);
|
// Skip suggest_next_prompt — its executor hangs forever
|
||||||
// Track for bedrock_message_history
|
// waiting for UI interaction that doesn't exist in the
|
||||||
let input_json: serde_json::Value = serde_json::from_str(¤t_tool_input_json)
|
// Bedrock path.
|
||||||
.unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
|
if current_tool_name == "suggest_next_prompt" {
|
||||||
history_tool_calls.push(ContentPart::ToolUse {
|
log::info!("[bedrock] Skipping suggest_next_prompt tool call");
|
||||||
tool_use_id: current_tool_use_id.clone(),
|
current_tool_use_id.clear();
|
||||||
name: current_tool_name.clone(),
|
current_tool_name.clear();
|
||||||
input: input_json,
|
current_tool_input_json.clear();
|
||||||
});
|
} else {
|
||||||
if let Some(ref logger) = diagnostic_logger {
|
log::debug!("[bedrock] Tool call complete: {} ({})", current_tool_name, current_tool_use_id);
|
||||||
logger.log_stream_event(&format!(
|
// Track for bedrock_message_history
|
||||||
"ToolCall: name={}, id={}, input={}",
|
let input_json: serde_json::Value = serde_json::from_str(¤t_tool_input_json)
|
||||||
current_tool_name, current_tool_use_id, current_tool_input_json
|
.unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
|
||||||
));
|
history_tool_calls.push(ContentPart::ToolUse {
|
||||||
|
tool_use_id: current_tool_use_id.clone(),
|
||||||
|
name: current_tool_name.clone(),
|
||||||
|
input: input_json,
|
||||||
|
});
|
||||||
|
if let Some(ref logger) = diagnostic_logger {
|
||||||
|
logger.log_stream_event(&format!(
|
||||||
|
"ToolCall: name={}, id={}, input={}",
|
||||||
|
current_tool_name, current_tool_use_id, current_tool_input_json
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let tool_msg = build_tool_call_message(
|
||||||
|
&task_id,
|
||||||
|
¤t_tool_use_id,
|
||||||
|
¤t_tool_name,
|
||||||
|
¤t_tool_input_json,
|
||||||
|
);
|
||||||
|
yield Ok(tool_msg);
|
||||||
|
current_tool_use_id.clear();
|
||||||
|
current_tool_name.clear();
|
||||||
|
current_tool_input_json.clear();
|
||||||
}
|
}
|
||||||
let tool_msg = build_tool_call_message(
|
|
||||||
&task_id,
|
|
||||||
¤t_tool_use_id,
|
|
||||||
¤t_tool_name,
|
|
||||||
¤t_tool_input_json,
|
|
||||||
);
|
|
||||||
yield Ok(tool_msg);
|
|
||||||
current_tool_use_id.clear();
|
|
||||||
current_tool_name.clear();
|
|
||||||
current_tool_input_json.clear();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
StreamEvent::MessageStop(stop) => {
|
StreamEvent::MessageStop(stop) => {
|
||||||
@@ -204,6 +216,8 @@ pub fn bedrock_stream_to_response_events(
|
|||||||
if let Some(usage) = metadata.usage() {
|
if let Some(usage) = metadata.usage() {
|
||||||
input_tokens = usage.input_tokens();
|
input_tokens = usage.input_tokens();
|
||||||
output_tokens = usage.output_tokens();
|
output_tokens = usage.output_tokens();
|
||||||
|
cache_read_input_tokens = usage.cache_read_input_tokens().unwrap_or(0);
|
||||||
|
cache_write_input_tokens = usage.cache_write_input_tokens().unwrap_or(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
@@ -265,7 +279,9 @@ pub fn bedrock_stream_to_response_events(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log::info!("[bedrock] Stream finished: input_tokens={input_tokens}, output_tokens={output_tokens}");
|
log::info!(
|
||||||
|
"[bedrock] Stream finished: input_tokens={input_tokens}, output_tokens={output_tokens}, cache_read={cache_read_input_tokens}, cache_write={cache_write_input_tokens}"
|
||||||
|
);
|
||||||
|
|
||||||
// Build and store the assistant message into bedrock_messages_sent
|
// Build and store the assistant message into bedrock_messages_sent
|
||||||
// so the controller can persist it as part of conversation history.
|
// so the controller can persist it as part of conversation history.
|
||||||
@@ -317,7 +333,13 @@ pub fn bedrock_stream_to_response_events(
|
|||||||
};
|
};
|
||||||
logger.log_result_success(input_tokens, output_tokens, stop_reason_str);
|
logger.log_result_success(input_tokens, output_tokens, stop_reason_str);
|
||||||
}
|
}
|
||||||
let finished_event = build_stream_finished(stop_reason, input_tokens, output_tokens);
|
let finished_event = build_stream_finished(
|
||||||
|
stop_reason,
|
||||||
|
input_tokens,
|
||||||
|
output_tokens,
|
||||||
|
cache_read_input_tokens,
|
||||||
|
cache_write_input_tokens,
|
||||||
|
);
|
||||||
yield Ok(finished_event);
|
yield Ok(finished_event);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -365,8 +387,11 @@ pub(super) fn build_stream_finished(
|
|||||||
reason: stream_finished::Reason,
|
reason: stream_finished::Reason,
|
||||||
input_tokens: i32,
|
input_tokens: i32,
|
||||||
output_tokens: i32,
|
output_tokens: i32,
|
||||||
|
cache_read_input_tokens: i32,
|
||||||
|
cache_write_input_tokens: i32,
|
||||||
) -> ResponseEvent {
|
) -> ResponseEvent {
|
||||||
let total_tokens = (input_tokens + output_tokens) as u32;
|
let total_tokens =
|
||||||
|
(input_tokens + output_tokens + cache_read_input_tokens + cache_write_input_tokens) as u32;
|
||||||
|
|
||||||
let mut byok_token_usage = std::collections::HashMap::new();
|
let mut byok_token_usage = std::collections::HashMap::new();
|
||||||
if total_tokens > 0 {
|
if total_tokens > 0 {
|
||||||
@@ -381,6 +406,20 @@ pub(super) fn build_stream_finished(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let token_usage = vec![stream_finished::TokenUsage {
|
||||||
|
model_id: "bedrock".to_string(),
|
||||||
|
total_input: input_tokens as u32,
|
||||||
|
output: output_tokens as u32,
|
||||||
|
input_cache_read: cache_read_input_tokens as u32,
|
||||||
|
input_cache_write: cache_write_input_tokens as u32,
|
||||||
|
cost_in_cents: estimate_cost_cents(
|
||||||
|
input_tokens as u32,
|
||||||
|
output_tokens as u32,
|
||||||
|
cache_read_input_tokens as u32,
|
||||||
|
cache_write_input_tokens as u32,
|
||||||
|
),
|
||||||
|
}];
|
||||||
|
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
let conversation_usage_metadata = Some(stream_finished::ConversationUsageMetadata {
|
let conversation_usage_metadata = Some(stream_finished::ConversationUsageMetadata {
|
||||||
context_window_usage: 0.0,
|
context_window_usage: 0.0,
|
||||||
@@ -396,7 +435,7 @@ pub(super) fn build_stream_finished(
|
|||||||
r#type: Some(api::response_event::Type::Finished(
|
r#type: Some(api::response_event::Type::Finished(
|
||||||
api::response_event::StreamFinished {
|
api::response_event::StreamFinished {
|
||||||
reason: Some(reason),
|
reason: Some(reason),
|
||||||
token_usage: vec![],
|
token_usage,
|
||||||
should_refresh_model_config: false,
|
should_refresh_model_config: false,
|
||||||
request_cost: None,
|
request_cost: None,
|
||||||
conversation_usage_metadata,
|
conversation_usage_metadata,
|
||||||
@@ -405,6 +444,23 @@ pub(super) fn build_stream_finished(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Estimates cost in cents based on Anthropic Claude Bedrock pricing.
|
||||||
|
/// Uses Sonnet-tier pricing as a conservative default since we don't
|
||||||
|
/// know the exact model at this layer.
|
||||||
|
/// Pricing (per 1M tokens): input $3, output $15, cache_read $0.30, cache_write $3.75
|
||||||
|
fn estimate_cost_cents(
|
||||||
|
input_tokens: u32,
|
||||||
|
output_tokens: u32,
|
||||||
|
cache_read_tokens: u32,
|
||||||
|
cache_write_tokens: u32,
|
||||||
|
) -> f32 {
|
||||||
|
let input_cost = input_tokens as f64 * 0.3 / 100_000.0;
|
||||||
|
let output_cost = output_tokens as f64 * 1.5 / 100_000.0;
|
||||||
|
let cache_read_cost = cache_read_tokens as f64 * 0.03 / 100_000.0;
|
||||||
|
let cache_write_cost = cache_write_tokens as f64 * 0.375 / 100_000.0;
|
||||||
|
(input_cost + output_cost + cache_read_cost + cache_write_cost) as f32
|
||||||
|
}
|
||||||
|
|
||||||
fn build_add_agent_output_message(
|
fn build_add_agent_output_message(
|
||||||
task_id: &str,
|
task_id: &str,
|
||||||
message_id: &str,
|
message_id: &str,
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ fn test_build_stream_init_has_valid_ids() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_build_stream_finished_done_reason() {
|
fn test_build_stream_finished_done_reason() {
|
||||||
let reason = stream_finished::Reason::Done(stream_finished::Done {});
|
let reason = stream_finished::Reason::Done(stream_finished::Done {});
|
||||||
let event = build_stream_finished(reason, 100, 50);
|
let event = build_stream_finished(reason, 100, 50, 20, 10);
|
||||||
|
|
||||||
match event.r#type {
|
match event.r#type {
|
||||||
Some(api::response_event::Type::Finished(finished)) => {
|
Some(api::response_event::Type::Finished(finished)) => {
|
||||||
@@ -35,8 +35,16 @@ fn test_build_stream_finished_done_reason() {
|
|||||||
.get("bedrock")
|
.get("bedrock")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.total_tokens,
|
.total_tokens,
|
||||||
150
|
180
|
||||||
);
|
);
|
||||||
|
// Verify token_usage includes cache breakdown
|
||||||
|
assert_eq!(finished.token_usage.len(), 1);
|
||||||
|
let usage = &finished.token_usage[0];
|
||||||
|
assert_eq!(usage.total_input, 100);
|
||||||
|
assert_eq!(usage.output, 50);
|
||||||
|
assert_eq!(usage.input_cache_read, 20);
|
||||||
|
assert_eq!(usage.input_cache_write, 10);
|
||||||
|
assert!(usage.cost_in_cents > 0.0);
|
||||||
}
|
}
|
||||||
other => panic!("Expected Finished event, got {:?}", other),
|
other => panic!("Expected Finished event, got {:?}", other),
|
||||||
}
|
}
|
||||||
@@ -45,7 +53,7 @@ fn test_build_stream_finished_done_reason() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_build_stream_finished_max_token_limit() {
|
fn test_build_stream_finished_max_token_limit() {
|
||||||
let reason = stream_finished::Reason::MaxTokenLimit(stream_finished::ReachedMaxTokenLimit {});
|
let reason = stream_finished::Reason::MaxTokenLimit(stream_finished::ReachedMaxTokenLimit {});
|
||||||
let event = build_stream_finished(reason, 200, 100);
|
let event = build_stream_finished(reason, 200, 100, 0, 0);
|
||||||
|
|
||||||
match event.r#type {
|
match event.r#type {
|
||||||
Some(api::response_event::Type::Finished(finished)) => {
|
Some(api::response_event::Type::Finished(finished)) => {
|
||||||
@@ -61,7 +69,7 @@ fn test_build_stream_finished_max_token_limit() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_build_stream_finished_other_reason() {
|
fn test_build_stream_finished_other_reason() {
|
||||||
let reason = stream_finished::Reason::Other(stream_finished::Other {});
|
let reason = stream_finished::Reason::Other(stream_finished::Other {});
|
||||||
let event = build_stream_finished(reason, 0, 0);
|
let event = build_stream_finished(reason, 0, 0, 0, 0);
|
||||||
|
|
||||||
match event.r#type {
|
match event.r#type {
|
||||||
Some(api::response_event::Type::Finished(finished)) => {
|
Some(api::response_event::Type::Finished(finished)) => {
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ impl ResponseStream {
|
|||||||
secret_access_key: settings.bedrock_secret_access_key.value().clone(),
|
secret_access_key: settings.bedrock_secret_access_key.value().clone(),
|
||||||
cross_region_inference: *settings.bedrock_cross_region_inference.value(),
|
cross_region_inference: *settings.bedrock_cross_region_inference.value(),
|
||||||
fallback_to_warp: *settings.bedrock_fallback_to_warp.value(),
|
fallback_to_warp: *settings.bedrock_fallback_to_warp.value(),
|
||||||
})
|
}.with_external_fallbacks())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ pub(crate) use view_util::{
|
|||||||
NEW_AGENT_PANE_LABEL,
|
NEW_AGENT_PANE_LABEL,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) use view_util::format_credits;
|
pub(crate) use view_util::{format_credits, format_token_count};
|
||||||
|
|
||||||
pub use crate::ai::blocklist::block::{secret_redaction, AIBlockResponseRating, TextLocation};
|
pub use crate::ai::blocklist::block::{secret_redaction, AIBlockResponseRating, TextLocation};
|
||||||
pub use block::keyboard_navigable_buttons;
|
pub use block::keyboard_navigable_buttons;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::ai::blocklist::usage::render_context_window_usage_icon;
|
use crate::ai::blocklist::usage::render_context_window_usage_icon;
|
||||||
use crate::ai::blocklist::view_util::format_credits;
|
use crate::ai::blocklist::view_util::{format_cost_cents, format_credits, format_token_count};
|
||||||
use crate::appearance::Appearance;
|
use crate::appearance::Appearance;
|
||||||
use crate::persistence::model::{
|
use crate::persistence::model::{
|
||||||
token_usage_category_display_name, ModelTokenUsage, FULL_TERMINAL_USE_CATEGORY,
|
token_usage_category_display_name, ModelTokenUsage, FULL_TERMINAL_USE_CATEGORY,
|
||||||
@@ -37,6 +37,11 @@ pub struct ConversationUsageInfo {
|
|||||||
pub lines_added: i32,
|
pub lines_added: i32,
|
||||||
pub lines_removed: i32,
|
pub lines_removed: i32,
|
||||||
pub commands_executed: i32,
|
pub commands_executed: i32,
|
||||||
|
pub total_input_tokens: u32,
|
||||||
|
pub total_output_tokens: u32,
|
||||||
|
pub total_cache_read_tokens: u32,
|
||||||
|
pub total_cache_write_tokens: u32,
|
||||||
|
pub estimated_cost_cents: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Timing information for the last set of agent responses
|
/// Timing information for the last set of agent responses
|
||||||
@@ -263,6 +268,55 @@ impl ConversationUsageView {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Token usage section
|
||||||
|
let total_tokens = self.usage_info.total_input_tokens
|
||||||
|
+ self.usage_info.total_output_tokens
|
||||||
|
+ self.usage_info.total_cache_read_tokens
|
||||||
|
+ self.usage_info.total_cache_write_tokens;
|
||||||
|
if total_tokens > 0 {
|
||||||
|
labels.push(render_label_text("Total tokens", appearance));
|
||||||
|
values.push(render_value_text(
|
||||||
|
format_token_count(total_tokens),
|
||||||
|
appearance,
|
||||||
|
));
|
||||||
|
|
||||||
|
labels.push(render_label_text(" Input", appearance));
|
||||||
|
values.push(render_value_text(
|
||||||
|
format_token_count(self.usage_info.total_input_tokens),
|
||||||
|
appearance,
|
||||||
|
));
|
||||||
|
|
||||||
|
labels.push(render_label_text(" Output", appearance));
|
||||||
|
values.push(render_value_text(
|
||||||
|
format_token_count(self.usage_info.total_output_tokens),
|
||||||
|
appearance,
|
||||||
|
));
|
||||||
|
|
||||||
|
if self.usage_info.total_cache_read_tokens > 0 {
|
||||||
|
labels.push(render_label_text(" Cache read", appearance));
|
||||||
|
values.push(render_value_text(
|
||||||
|
format_token_count(self.usage_info.total_cache_read_tokens),
|
||||||
|
appearance,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.usage_info.total_cache_write_tokens > 0 {
|
||||||
|
labels.push(render_label_text(" Cache write", appearance));
|
||||||
|
values.push(render_value_text(
|
||||||
|
format_token_count(self.usage_info.total_cache_write_tokens),
|
||||||
|
appearance,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.usage_info.estimated_cost_cents > 0.0 {
|
||||||
|
labels.push(render_label_text("Estimated cost", appearance));
|
||||||
|
values.push(render_value_text(
|
||||||
|
format_cost_cents(self.usage_info.estimated_cost_cents),
|
||||||
|
appearance,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
labels.push(render_label_text("Context window used", appearance));
|
labels.push(render_label_text("Context window used", appearance));
|
||||||
let context_usage_str =
|
let context_usage_str =
|
||||||
format!("{}%", (self.usage_info.context_window_usage * 100.).round());
|
format!("{}%", (self.usage_info.context_window_usage * 100.).round());
|
||||||
|
|||||||
@@ -152,6 +152,28 @@ pub fn get_ai_block_overflow_menu_element_position_id(view_id: EntityId) -> Stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Formats credit count to display as whole numbers when the value is effectively a whole number,
|
/// Formats credit count to display as whole numbers when the value is effectively a whole number,
|
||||||
|
pub fn format_token_count(tokens: u32) -> String {
|
||||||
|
if tokens >= 1_000_000 {
|
||||||
|
format!("{:.1}M tokens", tokens as f64 / 1_000_000.0)
|
||||||
|
} else if tokens >= 1_000 {
|
||||||
|
format!("{:.1}k tokens", tokens as f64 / 1_000.0)
|
||||||
|
} else {
|
||||||
|
format!("{tokens} tokens")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format_cost_cents(cents: f32) -> String {
|
||||||
|
if cents >= 100.0 {
|
||||||
|
format!("${:.2}", cents / 100.0)
|
||||||
|
} else if cents >= 1.0 {
|
||||||
|
format!("{:.1}\u{00A2}", cents)
|
||||||
|
} else if cents > 0.0 {
|
||||||
|
format!("{:.2}\u{00A2}", cents)
|
||||||
|
} else {
|
||||||
|
"$0.00".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// otherwise displays with one decimal place.
|
/// otherwise displays with one decimal place.
|
||||||
/// Returns a formatted string with proper pluralization ("credit" vs "credits").
|
/// Returns a formatted string with proper pluralization ("credit" vs "credits").
|
||||||
pub fn format_credits(credits: f32) -> String {
|
pub fn format_credits(credits: f32) -> String {
|
||||||
|
|||||||
+1
-1
@@ -605,7 +605,7 @@ impl LLMPreferences {
|
|||||||
secret_access_key: settings.bedrock_secret_access_key.value().clone(),
|
secret_access_key: settings.bedrock_secret_access_key.value().clone(),
|
||||||
cross_region_inference: *settings.bedrock_cross_region_inference.value(),
|
cross_region_inference: *settings.bedrock_cross_region_inference.value(),
|
||||||
fallback_to_warp: *settings.bedrock_fallback_to_warp.value(),
|
fallback_to_warp: *settings.bedrock_fallback_to_warp.value(),
|
||||||
};
|
}.with_external_fallbacks();
|
||||||
|
|
||||||
ctx.spawn(
|
ctx.spawn(
|
||||||
async move { discover_inference_profiles(&config).await },
|
async move { discover_inference_profiles(&config).await },
|
||||||
|
|||||||
@@ -2743,7 +2743,7 @@ impl TypedActionView for AISettingsPageView {
|
|||||||
secret_access_key: ai_settings.bedrock_secret_access_key.value().clone(),
|
secret_access_key: ai_settings.bedrock_secret_access_key.value().clone(),
|
||||||
cross_region_inference: *ai_settings.bedrock_cross_region_inference.value(),
|
cross_region_inference: *ai_settings.bedrock_cross_region_inference.value(),
|
||||||
fallback_to_warp: *ai_settings.bedrock_fallback_to_warp.value(),
|
fallback_to_warp: *ai_settings.bedrock_fallback_to_warp.value(),
|
||||||
};
|
}.with_external_fallbacks();
|
||||||
|
|
||||||
ctx.spawn(
|
ctx.spawn(
|
||||||
async move { discover_inference_profiles(&config).await },
|
async move { discover_inference_profiles(&config).await },
|
||||||
|
|||||||
@@ -5627,6 +5627,14 @@ impl TerminalView {
|
|||||||
let wall_to_wall_response_time_ms =
|
let wall_to_wall_response_time_ms =
|
||||||
conversation.wall_to_wall_response_time_since_last_query();
|
conversation.wall_to_wall_response_time_since_last_query();
|
||||||
|
|
||||||
|
let token_usage_list = conversation.total_token_usage();
|
||||||
|
let total_input_tokens: u32 = token_usage_list.iter().map(|u| u.total_input).sum();
|
||||||
|
let total_output_tokens: u32 = token_usage_list.iter().map(|u| u.output).sum();
|
||||||
|
let total_cache_read_tokens: u32 = token_usage_list.iter().map(|u| u.input_cache_read).sum();
|
||||||
|
let total_cache_write_tokens: u32 =
|
||||||
|
token_usage_list.iter().map(|u| u.input_cache_write).sum();
|
||||||
|
let estimated_cost_cents: f32 = token_usage_list.iter().map(|u| u.cost_in_cents).sum();
|
||||||
|
|
||||||
let conversation_usage_info = ConversationUsageInfo {
|
let conversation_usage_info = ConversationUsageInfo {
|
||||||
credits_spent: conversation.credits_spent(),
|
credits_spent: conversation.credits_spent(),
|
||||||
credits_spent_for_last_block: conversation.credits_spent_for_last_block(),
|
credits_spent_for_last_block: conversation.credits_spent_for_last_block(),
|
||||||
@@ -5637,6 +5645,11 @@ impl TerminalView {
|
|||||||
lines_added: tool_usage.apply_file_diff_stats.lines_added,
|
lines_added: tool_usage.apply_file_diff_stats.lines_added,
|
||||||
lines_removed: tool_usage.apply_file_diff_stats.lines_removed,
|
lines_removed: tool_usage.apply_file_diff_stats.lines_removed,
|
||||||
commands_executed: tool_usage.run_command_stats.commands_executed,
|
commands_executed: tool_usage.run_command_stats.commands_executed,
|
||||||
|
total_input_tokens,
|
||||||
|
total_output_tokens,
|
||||||
|
total_cache_read_tokens,
|
||||||
|
total_cache_write_tokens,
|
||||||
|
estimated_cost_cents,
|
||||||
};
|
};
|
||||||
|
|
||||||
let timing_info = TimingInfo {
|
let timing_info = TimingInfo {
|
||||||
|
|||||||
@@ -264,6 +264,11 @@ impl From<&gql_usage::ConversationUsage> for ConversationUsageInfo {
|
|||||||
lines_added: tool.apply_file_diff_stats.lines_added,
|
lines_added: tool.apply_file_diff_stats.lines_added,
|
||||||
lines_removed: tool.apply_file_diff_stats.lines_removed,
|
lines_removed: tool.apply_file_diff_stats.lines_removed,
|
||||||
commands_executed: tool.run_command_stats.commands_executed,
|
commands_executed: tool.run_command_stats.commands_executed,
|
||||||
|
total_input_tokens: 0,
|
||||||
|
total_output_tokens: 0,
|
||||||
|
total_cache_read_tokens: 0,
|
||||||
|
total_cache_write_tokens: 0,
|
||||||
|
estimated_cost_cents: 0.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -831,6 +831,18 @@ pub enum FeatureFlag {
|
|||||||
VerticalTabsSummaryMode,
|
VerticalTabsSummaryMode,
|
||||||
|
|
||||||
CloudModeInputV2,
|
CloudModeInputV2,
|
||||||
|
|
||||||
|
/// Enables LSP-based code completion (autocomplete) in the code editor.
|
||||||
|
LspCompletion,
|
||||||
|
|
||||||
|
/// Enables LSP code actions (quick fixes, refactorings) in the code editor.
|
||||||
|
LspCodeActions,
|
||||||
|
|
||||||
|
/// Enables LSP-based symbol rename in the code editor.
|
||||||
|
LspRename,
|
||||||
|
|
||||||
|
/// Enables LSP signature help (parameter hints) in the code editor.
|
||||||
|
LspSignatureHelp,
|
||||||
}
|
}
|
||||||
|
|
||||||
static FLAG_STATES: [AtomicBool; cardinality::<FeatureFlag>()] =
|
static FLAG_STATES: [AtomicBool; cardinality::<FeatureFlag>()] =
|
||||||
|
|||||||
@@ -6,10 +6,14 @@ use anyhow::Result;
|
|||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use command::r#async::Command;
|
use command::r#async::Command;
|
||||||
use lsp_types::{
|
use lsp_types::{
|
||||||
ClientCapabilities, ClientInfo, DidChangeWatchedFilesClientCapabilities, GotoCapability,
|
ClientCapabilities, ClientInfo, CodeActionClientCapabilities,
|
||||||
HoverClientCapabilities, InitializeParams, MarkupKind, PublishDiagnosticsClientCapabilities,
|
CompletionClientCapabilities, CompletionItemCapability,
|
||||||
TextDocumentClientCapabilities, TextDocumentSyncClientCapabilities, Uri,
|
CompletionItemCapabilityResolveSupport, DidChangeWatchedFilesClientCapabilities,
|
||||||
WindowClientCapabilities, WorkDoneProgressParams, WorkspaceClientCapabilities, WorkspaceFolder,
|
GotoCapability, HoverClientCapabilities, InitializeParams, MarkupKind,
|
||||||
|
PublishDiagnosticsClientCapabilities, RenameClientCapabilities,
|
||||||
|
SignatureHelpClientCapabilities, TextDocumentClientCapabilities,
|
||||||
|
TextDocumentSyncClientCapabilities, Uri, WindowClientCapabilities, WorkDoneProgressParams,
|
||||||
|
WorkspaceClientCapabilities, WorkspaceFolder,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::supported_servers::LSPServerType;
|
use crate::supported_servers::LSPServerType;
|
||||||
@@ -301,14 +305,31 @@ fn default_client_capabilities() -> ClientCapabilities {
|
|||||||
will_save_wait_until: Some(false),
|
will_save_wait_until: Some(false),
|
||||||
did_save: Some(true),
|
did_save: Some(true),
|
||||||
}),
|
}),
|
||||||
|
completion: Some(CompletionClientCapabilities {
|
||||||
|
dynamic_registration: Some(false),
|
||||||
|
completion_item: Some(CompletionItemCapability {
|
||||||
|
snippet_support: Some(true),
|
||||||
|
documentation_format: Some(vec![
|
||||||
|
MarkupKind::Markdown,
|
||||||
|
MarkupKind::PlainText,
|
||||||
|
]),
|
||||||
|
resolve_support: Some(CompletionItemCapabilityResolveSupport {
|
||||||
|
properties: vec![
|
||||||
|
"documentation".into(),
|
||||||
|
"detail".into(),
|
||||||
|
"additionalTextEdits".into(),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
definition: Some(GotoCapability {
|
definition: Some(GotoCapability {
|
||||||
dynamic_registration: Some(false),
|
dynamic_registration: Some(false),
|
||||||
link_support: Some(true),
|
link_support: Some(true),
|
||||||
}),
|
}),
|
||||||
hover: Some(HoverClientCapabilities {
|
hover: Some(HoverClientCapabilities {
|
||||||
dynamic_registration: Some(false),
|
dynamic_registration: Some(false),
|
||||||
// Request Markdown content from the LSP for hover responses.
|
|
||||||
// This enables proper syntax highlighting in hover tooltips.
|
|
||||||
content_format: Some(vec![MarkupKind::Markdown, MarkupKind::PlainText]),
|
content_format: Some(vec![MarkupKind::Markdown, MarkupKind::PlainText]),
|
||||||
}),
|
}),
|
||||||
publish_diagnostics: Some(PublishDiagnosticsClientCapabilities {
|
publish_diagnostics: Some(PublishDiagnosticsClientCapabilities {
|
||||||
@@ -316,6 +337,19 @@ fn default_client_capabilities() -> ClientCapabilities {
|
|||||||
related_information: Some(true),
|
related_information: Some(true),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
|
signature_help: Some(SignatureHelpClientCapabilities {
|
||||||
|
dynamic_registration: Some(false),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
rename: Some(RenameClientCapabilities {
|
||||||
|
dynamic_registration: Some(false),
|
||||||
|
prepare_support: Some(true),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
code_action: Some(CodeActionClientCapabilities {
|
||||||
|
dynamic_registration: Some(false),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|||||||
@@ -23,14 +23,18 @@ pub use config::{default_init_params, LanguageId, LspServerConfig};
|
|||||||
pub use jsonrpc::{JsonRpcService, ServerNotificationEvent, Transport};
|
pub use jsonrpc::{JsonRpcService, ServerNotificationEvent, Transport};
|
||||||
pub use lsp_types::{
|
pub use lsp_types::{
|
||||||
notification::{self},
|
notification::{self},
|
||||||
Position, Range,
|
CompletionItem, Position, Range,
|
||||||
};
|
};
|
||||||
pub use manager::{LspManagerModel, LspManagerModelEvent};
|
pub use manager::{LspManagerModel, LspManagerModelEvent};
|
||||||
pub use model::{
|
pub use model::{
|
||||||
BackgroundTaskInfo, DocumentDiagnostics, LanguageServerId, LspEvent, LspServerModel, LspState,
|
BackgroundTaskInfo, DocumentDiagnostics, LanguageServerId, LspEvent, LspServerModel, LspState,
|
||||||
};
|
};
|
||||||
pub use service::LspService;
|
pub use service::LspService;
|
||||||
pub use types::{HoverContents, HoverResult, MarkupKind, ReferenceLocation};
|
pub use types::{
|
||||||
|
CodeActionData, CompletionItemData, CompletionKind, CompletionResult, CompletionTrigger,
|
||||||
|
FileEdits, HoverContents, HoverResult, MarkupKind, ParameterInfo, ParameterLabel,
|
||||||
|
PrepareRenameResult, ReferenceLocation, RenameResult, SignatureHelpResult, SignatureInfo,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||||
pub enum LspServerLogLevel {
|
pub enum LspServerLogLevel {
|
||||||
|
|||||||
+95
-4
@@ -3,16 +3,17 @@ use crate::{
|
|||||||
server_repo_watcher::LspRepoWatcher,
|
server_repo_watcher::LspRepoWatcher,
|
||||||
supported_servers::LSPServerType,
|
supported_servers::LSPServerType,
|
||||||
types::{
|
types::{
|
||||||
DefinitionLocation, DocumentVersion, HoverResult, Location, ReferenceLocation,
|
CodeActionData, CompletionResult, CompletionTrigger, DefinitionLocation, DocumentVersion,
|
||||||
TextDocumentContentChangeEvent, TextEdit, WatchedFileChangeEvent,
|
HoverResult, Location, PrepareRenameResult, ReferenceLocation, RenameResult,
|
||||||
|
SignatureHelpResult, TextDocumentContentChangeEvent, TextEdit, WatchedFileChangeEvent,
|
||||||
},
|
},
|
||||||
LspServerConfig, LspServerLogLevel, LspService,
|
LspServerConfig, LspServerLogLevel, LspService,
|
||||||
};
|
};
|
||||||
use instant::Instant;
|
use instant::Instant;
|
||||||
use lsp_types::{
|
use lsp_types::{
|
||||||
notification::{self, Notification},
|
notification::{self, Notification},
|
||||||
FormattingOptions, NumberOrString, ProgressParams, ProgressParamsValue,
|
CompletionItem, CompletionTriggerKind, FormattingOptions, NumberOrString, ProgressParams,
|
||||||
PublishDiagnosticsParams, WorkDoneProgress,
|
ProgressParamsValue, PublishDiagnosticsParams, Range as LspRange, WorkDoneProgress,
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
@@ -735,6 +736,96 @@ impl LspServerModel {
|
|||||||
.await
|
.await
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn completion(
|
||||||
|
&self,
|
||||||
|
path: PathBuf,
|
||||||
|
position: Location,
|
||||||
|
trigger: CompletionTrigger,
|
||||||
|
) -> Result<impl Future<Output = Result<Option<CompletionResult>>>> {
|
||||||
|
let service = self.service()?;
|
||||||
|
let (trigger_kind, trigger_character) = match trigger {
|
||||||
|
CompletionTrigger::Invoked => (CompletionTriggerKind::INVOKED, None),
|
||||||
|
CompletionTrigger::TriggerCharacter(ch) => {
|
||||||
|
(CompletionTriggerKind::TRIGGER_CHARACTER, Some(ch))
|
||||||
|
}
|
||||||
|
CompletionTrigger::Incomplete => {
|
||||||
|
(CompletionTriggerKind::TRIGGER_FOR_INCOMPLETE_COMPLETIONS, None)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(async move {
|
||||||
|
service
|
||||||
|
.text_document()
|
||||||
|
.completion(&path, position.into_lsp(), Some(trigger_kind), trigger_character)
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn completion_resolve(
|
||||||
|
&self,
|
||||||
|
item: CompletionItem,
|
||||||
|
) -> Result<impl Future<Output = Result<CompletionItem>>> {
|
||||||
|
let service = self.service()?;
|
||||||
|
Ok(async move { service.text_document().completion_resolve(item).await })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn signature_help(
|
||||||
|
&self,
|
||||||
|
path: PathBuf,
|
||||||
|
position: Location,
|
||||||
|
) -> Result<impl Future<Output = Result<Option<SignatureHelpResult>>>> {
|
||||||
|
let service = self.service()?;
|
||||||
|
Ok(async move {
|
||||||
|
service
|
||||||
|
.text_document()
|
||||||
|
.signature_help(&path, position.into_lsp())
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn code_actions(
|
||||||
|
&self,
|
||||||
|
path: PathBuf,
|
||||||
|
range: LspRange,
|
||||||
|
diagnostics: Vec<lsp_types::Diagnostic>,
|
||||||
|
) -> Result<impl Future<Output = Result<Vec<CodeActionData>>>> {
|
||||||
|
let service = self.service()?;
|
||||||
|
Ok(async move {
|
||||||
|
service
|
||||||
|
.text_document()
|
||||||
|
.code_action(&path, range, diagnostics)
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prepare_rename(
|
||||||
|
&self,
|
||||||
|
path: PathBuf,
|
||||||
|
position: Location,
|
||||||
|
) -> Result<impl Future<Output = Result<Option<PrepareRenameResult>>>> {
|
||||||
|
let service = self.service()?;
|
||||||
|
Ok(async move {
|
||||||
|
service
|
||||||
|
.text_document()
|
||||||
|
.prepare_rename(&path, position.into_lsp())
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rename(
|
||||||
|
&self,
|
||||||
|
path: PathBuf,
|
||||||
|
position: Location,
|
||||||
|
new_name: String,
|
||||||
|
) -> Result<impl Future<Output = Result<Option<RenameResult>>>> {
|
||||||
|
let service = self.service()?;
|
||||||
|
Ok(async move {
|
||||||
|
service
|
||||||
|
.text_document()
|
||||||
|
.rename(&path, position.into_lsp(), new_name)
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Entity for LspServerModel {
|
impl Entity for LspServerModel {
|
||||||
|
|||||||
+265
-8
@@ -7,8 +7,9 @@ use std::{
|
|||||||
use crate::{
|
use crate::{
|
||||||
config::{lsp_uri_to_path, path_to_lsp_uri, LanguageId},
|
config::{lsp_uri_to_path, path_to_lsp_uri, LanguageId},
|
||||||
types::{
|
types::{
|
||||||
HoverResult, LspDefinitionLocation, ReferenceLocation, TextDocumentContentChangeEvent,
|
CodeActionData, CompletionResult, FileEdits, HoverResult, Location, LspDefinitionLocation,
|
||||||
TextEdit, WatchedFileChangeEvent,
|
PrepareRenameResult, Range, ReferenceLocation, RenameResult, SignatureHelpResult,
|
||||||
|
TextDocumentContentChangeEvent, TextEdit, WatchedFileChangeEvent,
|
||||||
},
|
},
|
||||||
LspServerLogLevel,
|
LspServerLogLevel,
|
||||||
};
|
};
|
||||||
@@ -19,12 +20,14 @@ use jsonrpc::{JsonRpcService, RequestId, ServerNotificationEvent};
|
|||||||
use lsp_types::{
|
use lsp_types::{
|
||||||
notification::{self, Notification},
|
notification::{self, Notification},
|
||||||
request::{self, Request},
|
request::{self, Request},
|
||||||
CancelParams, DidChangeTextDocumentParams, DidChangeWatchedFilesParams,
|
CancelParams, CodeActionContext, CodeActionParams, CompletionContext, CompletionItem,
|
||||||
DidChangeWatchedFilesRegistrationOptions, DidCloseTextDocumentParams,
|
CompletionParams, CompletionTriggerKind, DidChangeTextDocumentParams,
|
||||||
DidOpenTextDocumentParams, DocumentFormattingParams, FileChangeType, FileSystemWatcher,
|
DidChangeWatchedFilesParams, DidChangeWatchedFilesRegistrationOptions,
|
||||||
FormattingOptions, GlobPattern, GotoDefinitionParams, GotoDefinitionResponse, HoverParams,
|
DidCloseTextDocumentParams, DidOpenTextDocumentParams, DocumentFormattingParams,
|
||||||
InitializeParams, InitializedParams, NumberOrString, OneOf, Position, ReferenceParams,
|
FileChangeType, FileSystemWatcher, FormattingOptions, GlobPattern, GotoDefinitionParams,
|
||||||
RegistrationParams, RelativePattern, TextDocumentIdentifier, TextDocumentItem,
|
GotoDefinitionResponse, HoverParams, InitializeParams, InitializedParams, NumberOrString,
|
||||||
|
OneOf, Position, Range as LspRange, ReferenceParams, RegistrationParams, RelativePattern,
|
||||||
|
RenameParams, SignatureHelpParams, TextDocumentIdentifier, TextDocumentItem,
|
||||||
TextDocumentPositionParams, UnregistrationParams, VersionedTextDocumentIdentifier, WatchKind,
|
TextDocumentPositionParams, UnregistrationParams, VersionedTextDocumentIdentifier, WatchKind,
|
||||||
};
|
};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
@@ -739,4 +742,258 @@ impl<'a> TextDocumentService<'a> {
|
|||||||
.filter_map(|loc| ReferenceLocation::try_from(loc).ok())
|
.filter_map(|loc| ReferenceLocation::try_from(loc).ok())
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn completion(
|
||||||
|
&self,
|
||||||
|
path: &Path,
|
||||||
|
position: Position,
|
||||||
|
trigger: Option<CompletionTriggerKind>,
|
||||||
|
trigger_character: Option<String>,
|
||||||
|
) -> anyhow::Result<Option<CompletionResult>> {
|
||||||
|
let uri = path_to_lsp_uri(path)?;
|
||||||
|
|
||||||
|
let completion_params = CompletionParams {
|
||||||
|
text_document_position: TextDocumentPositionParams {
|
||||||
|
text_document: TextDocumentIdentifier { uri },
|
||||||
|
position,
|
||||||
|
},
|
||||||
|
work_done_progress_params: Default::default(),
|
||||||
|
partial_result_params: Default::default(),
|
||||||
|
context: Some(CompletionContext {
|
||||||
|
trigger_kind: trigger.unwrap_or(CompletionTriggerKind::INVOKED),
|
||||||
|
trigger_character,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = self
|
||||||
|
.service
|
||||||
|
.send_request::<request::Completion>(completion_params)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Err(e) = &result {
|
||||||
|
self.service.log_to_server_log(
|
||||||
|
LspServerLogLevel::Error,
|
||||||
|
format!("textDocument/completion failed: {e}"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result?.map(Into::into))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn completion_resolve(
|
||||||
|
&self,
|
||||||
|
item: CompletionItem,
|
||||||
|
) -> anyhow::Result<CompletionItem> {
|
||||||
|
let result = self
|
||||||
|
.service
|
||||||
|
.send_request::<request::ResolveCompletionItem>(item)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Err(e) = &result {
|
||||||
|
self.service.log_to_server_log(
|
||||||
|
LspServerLogLevel::Error,
|
||||||
|
format!("completionItem/resolve failed: {e}"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn signature_help(
|
||||||
|
&self,
|
||||||
|
path: &Path,
|
||||||
|
position: Position,
|
||||||
|
) -> anyhow::Result<Option<SignatureHelpResult>> {
|
||||||
|
let uri = path_to_lsp_uri(path)?;
|
||||||
|
|
||||||
|
let params = SignatureHelpParams {
|
||||||
|
text_document_position_params: TextDocumentPositionParams {
|
||||||
|
text_document: TextDocumentIdentifier { uri },
|
||||||
|
position,
|
||||||
|
},
|
||||||
|
work_done_progress_params: Default::default(),
|
||||||
|
context: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = self
|
||||||
|
.service
|
||||||
|
.send_request::<request::SignatureHelpRequest>(params)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Err(e) = &result {
|
||||||
|
self.service.log_to_server_log(
|
||||||
|
LspServerLogLevel::Error,
|
||||||
|
format!("textDocument/signatureHelp failed: {e}"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result?.map(Into::into))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn code_action(
|
||||||
|
&self,
|
||||||
|
path: &Path,
|
||||||
|
range: LspRange,
|
||||||
|
diagnostics: Vec<lsp_types::Diagnostic>,
|
||||||
|
) -> anyhow::Result<Vec<CodeActionData>> {
|
||||||
|
let uri = path_to_lsp_uri(path)?;
|
||||||
|
|
||||||
|
let params = CodeActionParams {
|
||||||
|
text_document: TextDocumentIdentifier { uri },
|
||||||
|
range,
|
||||||
|
context: CodeActionContext {
|
||||||
|
diagnostics,
|
||||||
|
only: None,
|
||||||
|
trigger_kind: None,
|
||||||
|
},
|
||||||
|
work_done_progress_params: Default::default(),
|
||||||
|
partial_result_params: Default::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = self
|
||||||
|
.service
|
||||||
|
.send_request::<request::CodeActionRequest>(params)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Err(e) = &result {
|
||||||
|
self.service.log_to_server_log(
|
||||||
|
LspServerLogLevel::Error,
|
||||||
|
format!("textDocument/codeAction failed: {e}"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result?
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.map(Into::into)
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn prepare_rename(
|
||||||
|
&self,
|
||||||
|
path: &Path,
|
||||||
|
position: Position,
|
||||||
|
) -> anyhow::Result<Option<PrepareRenameResult>> {
|
||||||
|
let uri = path_to_lsp_uri(path)?;
|
||||||
|
|
||||||
|
let params = TextDocumentPositionParams {
|
||||||
|
text_document: TextDocumentIdentifier { uri },
|
||||||
|
position,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = self
|
||||||
|
.service
|
||||||
|
.send_request::<request::PrepareRenameRequest>(params)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Err(e) = &result {
|
||||||
|
self.service.log_to_server_log(
|
||||||
|
LspServerLogLevel::Error,
|
||||||
|
format!("textDocument/prepareRename failed: {e}"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result?.map(|response| match response {
|
||||||
|
lsp_types::PrepareRenameResponse::Range(range) => PrepareRenameResult {
|
||||||
|
range: range.into(),
|
||||||
|
placeholder: None,
|
||||||
|
},
|
||||||
|
lsp_types::PrepareRenameResponse::RangeWithPlaceholder { range, placeholder } => {
|
||||||
|
PrepareRenameResult {
|
||||||
|
range: range.into(),
|
||||||
|
placeholder: Some(placeholder),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lsp_types::PrepareRenameResponse::DefaultBehavior { .. } => PrepareRenameResult {
|
||||||
|
range: Range {
|
||||||
|
start: Location { line: 0, column: 0 },
|
||||||
|
end: Location { line: 0, column: 0 },
|
||||||
|
},
|
||||||
|
placeholder: None,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn rename(
|
||||||
|
&self,
|
||||||
|
path: &Path,
|
||||||
|
position: Position,
|
||||||
|
new_name: String,
|
||||||
|
) -> anyhow::Result<Option<RenameResult>> {
|
||||||
|
let uri = path_to_lsp_uri(path)?;
|
||||||
|
|
||||||
|
let params = RenameParams {
|
||||||
|
text_document_position: TextDocumentPositionParams {
|
||||||
|
text_document: TextDocumentIdentifier { uri },
|
||||||
|
position,
|
||||||
|
},
|
||||||
|
new_name,
|
||||||
|
work_done_progress_params: Default::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = self
|
||||||
|
.service
|
||||||
|
.send_request::<request::Rename>(params)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Err(e) = &result {
|
||||||
|
self.service.log_to_server_log(
|
||||||
|
LspServerLogLevel::Error,
|
||||||
|
format!("textDocument/rename failed: {e}"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let workspace_edit = match result? {
|
||||||
|
Some(edit) => edit,
|
||||||
|
None => return Ok(None),
|
||||||
|
};
|
||||||
|
|
||||||
|
let file_edits = workspace_edit_to_file_edits(workspace_edit)?;
|
||||||
|
Ok(Some(RenameResult { edits: file_edits }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn workspace_edit_to_file_edits(
|
||||||
|
workspace_edit: lsp_types::WorkspaceEdit,
|
||||||
|
) -> anyhow::Result<Vec<FileEdits>> {
|
||||||
|
let mut result: Vec<FileEdits> = Vec::new();
|
||||||
|
|
||||||
|
if let Some(changes) = workspace_edit.changes {
|
||||||
|
for (uri, edits) in changes {
|
||||||
|
let path = lsp_uri_to_path(&uri)?;
|
||||||
|
result.push(FileEdits {
|
||||||
|
path,
|
||||||
|
edits: edits.into_iter().map(Into::into).collect(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(document_changes) = workspace_edit.document_changes {
|
||||||
|
match document_changes {
|
||||||
|
lsp_types::DocumentChanges::Edits(edits) => {
|
||||||
|
for edit in edits {
|
||||||
|
let path = lsp_uri_to_path(&edit.text_document.uri)?;
|
||||||
|
result.push(FileEdits {
|
||||||
|
path,
|
||||||
|
edits: edit.edits.into_iter().map(|e| match e {
|
||||||
|
lsp_types::OneOf::Left(text_edit) => text_edit.into(),
|
||||||
|
lsp_types::OneOf::Right(annotated) => {
|
||||||
|
lsp_types::TextEdit {
|
||||||
|
range: annotated.text_edit.range,
|
||||||
|
new_text: annotated.text_edit.new_text,
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}).collect(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lsp_types::DocumentChanges::Operations(_) => {
|
||||||
|
// File create/rename/delete operations are not supported yet
|
||||||
|
log::warn!("workspace/rename returned file operations which are not yet supported");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|||||||
+301
-1
@@ -1,7 +1,8 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use lsp_types::{
|
use lsp_types::{
|
||||||
FileChangeType, FileEvent, Location as LspLocation, LocationLink, Position as LspPosition,
|
CompletionItem, CompletionItemKind, CompletionResponse, FileChangeType, FileEvent,
|
||||||
|
InsertTextFormat, Location as LspLocation, LocationLink, Position as LspPosition,
|
||||||
Range as LspRange,
|
Range as LspRange,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -141,6 +142,7 @@ impl TryFrom<LspLocation> for ReferenceLocation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Edit returned by the LSP.
|
/// Edit returned by the LSP.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub struct TextEdit {
|
pub struct TextEdit {
|
||||||
pub range: Range,
|
pub range: Range,
|
||||||
pub text: String,
|
pub text: String,
|
||||||
@@ -278,3 +280,301 @@ impl WatchedFileChangeEvent {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The kind of a completion item, mapped from LSP's CompletionItemKind.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum CompletionKind {
|
||||||
|
Text,
|
||||||
|
Method,
|
||||||
|
Function,
|
||||||
|
Constructor,
|
||||||
|
Field,
|
||||||
|
Variable,
|
||||||
|
Class,
|
||||||
|
Interface,
|
||||||
|
Module,
|
||||||
|
Property,
|
||||||
|
Unit,
|
||||||
|
Value,
|
||||||
|
Enum,
|
||||||
|
Keyword,
|
||||||
|
Snippet,
|
||||||
|
Color,
|
||||||
|
File,
|
||||||
|
Reference,
|
||||||
|
Folder,
|
||||||
|
EnumMember,
|
||||||
|
Constant,
|
||||||
|
Struct,
|
||||||
|
Event,
|
||||||
|
Operator,
|
||||||
|
TypeParameter,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CompletionItemKind> for CompletionKind {
|
||||||
|
fn from(kind: CompletionItemKind) -> Self {
|
||||||
|
match kind {
|
||||||
|
CompletionItemKind::TEXT => Self::Text,
|
||||||
|
CompletionItemKind::METHOD => Self::Method,
|
||||||
|
CompletionItemKind::FUNCTION => Self::Function,
|
||||||
|
CompletionItemKind::CONSTRUCTOR => Self::Constructor,
|
||||||
|
CompletionItemKind::FIELD => Self::Field,
|
||||||
|
CompletionItemKind::VARIABLE => Self::Variable,
|
||||||
|
CompletionItemKind::CLASS => Self::Class,
|
||||||
|
CompletionItemKind::INTERFACE => Self::Interface,
|
||||||
|
CompletionItemKind::MODULE => Self::Module,
|
||||||
|
CompletionItemKind::PROPERTY => Self::Property,
|
||||||
|
CompletionItemKind::UNIT => Self::Unit,
|
||||||
|
CompletionItemKind::VALUE => Self::Value,
|
||||||
|
CompletionItemKind::ENUM => Self::Enum,
|
||||||
|
CompletionItemKind::KEYWORD => Self::Keyword,
|
||||||
|
CompletionItemKind::SNIPPET => Self::Snippet,
|
||||||
|
CompletionItemKind::COLOR => Self::Color,
|
||||||
|
CompletionItemKind::FILE => Self::File,
|
||||||
|
CompletionItemKind::REFERENCE => Self::Reference,
|
||||||
|
CompletionItemKind::FOLDER => Self::Folder,
|
||||||
|
CompletionItemKind::ENUM_MEMBER => Self::EnumMember,
|
||||||
|
CompletionItemKind::CONSTANT => Self::Constant,
|
||||||
|
CompletionItemKind::STRUCT => Self::Struct,
|
||||||
|
CompletionItemKind::EVENT => Self::Event,
|
||||||
|
CompletionItemKind::OPERATOR => Self::Operator,
|
||||||
|
CompletionItemKind::TYPE_PARAMETER => Self::TypeParameter,
|
||||||
|
_ => Self::Text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A single completion item returned from an LSP completion request.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CompletionItemData {
|
||||||
|
/// The label displayed in the completion menu.
|
||||||
|
pub label: String,
|
||||||
|
/// Additional detail (e.g. type signature), displayed dimmed.
|
||||||
|
pub detail: Option<String>,
|
||||||
|
/// The kind of completion (function, variable, etc.).
|
||||||
|
pub kind: Option<CompletionKind>,
|
||||||
|
/// Text used for filtering. Falls back to `label` if None.
|
||||||
|
pub filter_text: Option<String>,
|
||||||
|
/// Text to sort completions by. Falls back to `label` if None.
|
||||||
|
pub sort_text: Option<String>,
|
||||||
|
/// The text to insert when this completion is accepted.
|
||||||
|
pub insert_text: String,
|
||||||
|
/// Whether the insert text is a snippet (contains tab stops like $1, ${2:placeholder}).
|
||||||
|
pub is_snippet: bool,
|
||||||
|
/// The range of text to replace when applying this completion.
|
||||||
|
/// If None, the editor should replace the current word prefix.
|
||||||
|
pub text_edit_range: Option<Range>,
|
||||||
|
/// Additional text edits (e.g. auto-imports) applied alongside the main insertion.
|
||||||
|
pub additional_edits: Vec<TextEdit>,
|
||||||
|
/// The original LSP CompletionItem, preserved for resolve requests.
|
||||||
|
pub raw_item: CompletionItem,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CompletionItemData {
|
||||||
|
pub fn effective_filter_text(&self) -> &str {
|
||||||
|
self.filter_text.as_deref().unwrap_or(&self.label)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn effective_sort_text(&self) -> &str {
|
||||||
|
self.sort_text.as_deref().unwrap_or(&self.label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The result of a completion request.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CompletionResult {
|
||||||
|
/// The completion items.
|
||||||
|
pub items: Vec<CompletionItemData>,
|
||||||
|
/// Whether the list is incomplete (server may have more results if the user continues typing).
|
||||||
|
pub is_incomplete: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CompletionResponse> for CompletionResult {
|
||||||
|
fn from(response: CompletionResponse) -> Self {
|
||||||
|
match response {
|
||||||
|
CompletionResponse::Array(items) => Self {
|
||||||
|
items: items.into_iter().map(completion_item_to_data).collect(),
|
||||||
|
is_incomplete: false,
|
||||||
|
},
|
||||||
|
CompletionResponse::List(list) => Self {
|
||||||
|
items: list
|
||||||
|
.items
|
||||||
|
.into_iter()
|
||||||
|
.map(completion_item_to_data)
|
||||||
|
.collect(),
|
||||||
|
is_incomplete: list.is_incomplete,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn completion_item_to_data(item: CompletionItem) -> CompletionItemData {
|
||||||
|
let is_snippet = item.insert_text_format == Some(InsertTextFormat::SNIPPET);
|
||||||
|
|
||||||
|
let (insert_text, text_edit_range) = if let Some(ref text_edit) = item.text_edit {
|
||||||
|
match text_edit {
|
||||||
|
lsp_types::CompletionTextEdit::Edit(edit) => {
|
||||||
|
(edit.new_text.clone(), Some(edit.range.into()))
|
||||||
|
}
|
||||||
|
lsp_types::CompletionTextEdit::InsertAndReplace(edit) => {
|
||||||
|
(edit.new_text.clone(), Some(edit.insert.into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let text = item
|
||||||
|
.insert_text
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| item.label.clone());
|
||||||
|
(text, None)
|
||||||
|
};
|
||||||
|
|
||||||
|
let additional_edits = item
|
||||||
|
.additional_text_edits
|
||||||
|
.as_ref()
|
||||||
|
.map(|edits| edits.iter().map(|e| TextEdit::from(e.clone())).collect())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
CompletionItemData {
|
||||||
|
label: item.label.clone(),
|
||||||
|
detail: item.detail.clone(),
|
||||||
|
kind: item.kind.map(Into::into),
|
||||||
|
filter_text: item.filter_text.clone(),
|
||||||
|
sort_text: item.sort_text.clone(),
|
||||||
|
insert_text,
|
||||||
|
is_snippet,
|
||||||
|
text_edit_range,
|
||||||
|
additional_edits,
|
||||||
|
raw_item: item,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trigger context for a completion request.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum CompletionTrigger {
|
||||||
|
/// User explicitly invoked completion (e.g. Ctrl+Space).
|
||||||
|
Invoked,
|
||||||
|
/// A trigger character was typed (e.g. '.', '::', '->').
|
||||||
|
TriggerCharacter(String),
|
||||||
|
/// Re-triggered for an incomplete completion list as the user continues typing.
|
||||||
|
Incomplete,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The result of a signature help request.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SignatureHelpResult {
|
||||||
|
pub signatures: Vec<SignatureInfo>,
|
||||||
|
pub active_signature: Option<usize>,
|
||||||
|
pub active_parameter: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SignatureInfo {
|
||||||
|
pub label: String,
|
||||||
|
pub documentation: Option<String>,
|
||||||
|
pub parameters: Vec<ParameterInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ParameterInfo {
|
||||||
|
pub label: ParameterLabel,
|
||||||
|
pub documentation: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ParameterLabel {
|
||||||
|
Simple(String),
|
||||||
|
Offsets(u32, u32),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<lsp_types::SignatureHelp> for SignatureHelpResult {
|
||||||
|
fn from(help: lsp_types::SignatureHelp) -> Self {
|
||||||
|
Self {
|
||||||
|
signatures: help.signatures.into_iter().map(Into::into).collect(),
|
||||||
|
active_signature: help.active_signature.map(|s| s as usize),
|
||||||
|
active_parameter: help.active_parameter.map(|p| p as usize),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<lsp_types::SignatureInformation> for SignatureInfo {
|
||||||
|
fn from(sig: lsp_types::SignatureInformation) -> Self {
|
||||||
|
Self {
|
||||||
|
label: sig.label,
|
||||||
|
documentation: sig.documentation.map(|doc| match doc {
|
||||||
|
lsp_types::Documentation::String(s) => s,
|
||||||
|
lsp_types::Documentation::MarkupContent(m) => m.value,
|
||||||
|
}),
|
||||||
|
parameters: sig
|
||||||
|
.parameters
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.map(Into::into)
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<lsp_types::ParameterInformation> for ParameterInfo {
|
||||||
|
fn from(param: lsp_types::ParameterInformation) -> Self {
|
||||||
|
Self {
|
||||||
|
label: match param.label {
|
||||||
|
lsp_types::ParameterLabel::Simple(s) => ParameterLabel::Simple(s),
|
||||||
|
lsp_types::ParameterLabel::LabelOffsets(offsets) => {
|
||||||
|
ParameterLabel::Offsets(offsets[0], offsets[1])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
documentation: param.documentation.map(|doc| match doc {
|
||||||
|
lsp_types::Documentation::String(s) => s,
|
||||||
|
lsp_types::Documentation::MarkupContent(m) => m.value,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A code action returned from the LSP.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CodeActionData {
|
||||||
|
pub title: String,
|
||||||
|
pub kind: Option<String>,
|
||||||
|
pub is_preferred: bool,
|
||||||
|
pub raw: lsp_types::CodeActionOrCommand,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<lsp_types::CodeActionOrCommand> for CodeActionData {
|
||||||
|
fn from(action_or_cmd: lsp_types::CodeActionOrCommand) -> Self {
|
||||||
|
match &action_or_cmd {
|
||||||
|
lsp_types::CodeActionOrCommand::CodeAction(action) => Self {
|
||||||
|
title: action.title.clone(),
|
||||||
|
kind: action.kind.as_ref().map(|k| k.as_str().to_string()),
|
||||||
|
is_preferred: action.is_preferred.unwrap_or(false),
|
||||||
|
raw: action_or_cmd,
|
||||||
|
},
|
||||||
|
lsp_types::CodeActionOrCommand::Command(cmd) => Self {
|
||||||
|
title: cmd.title.clone(),
|
||||||
|
kind: None,
|
||||||
|
is_preferred: false,
|
||||||
|
raw: action_or_cmd,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The result of a prepare-rename request.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct PrepareRenameResult {
|
||||||
|
pub range: Range,
|
||||||
|
pub placeholder: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The result of a rename request — a set of edits across files.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct RenameResult {
|
||||||
|
pub edits: Vec<FileEdits>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Edits for a single file as part of a workspace edit.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct FileEdits {
|
||||||
|
pub path: PathBuf,
|
||||||
|
pub edits: Vec<TextEdit>,
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user