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:
Ryan Ward
2026-05-18 12:13:05 -05:00
co-authored by Claude Opus 4.6
parent 95b4708e44
commit f37a744692
27 changed files with 2423 additions and 65 deletions
+40 -6
View File
@@ -6,10 +6,14 @@ use anyhow::Result;
#[cfg(not(target_arch = "wasm32"))]
use command::r#async::Command;
use lsp_types::{
ClientCapabilities, ClientInfo, DidChangeWatchedFilesClientCapabilities, GotoCapability,
HoverClientCapabilities, InitializeParams, MarkupKind, PublishDiagnosticsClientCapabilities,
TextDocumentClientCapabilities, TextDocumentSyncClientCapabilities, Uri,
WindowClientCapabilities, WorkDoneProgressParams, WorkspaceClientCapabilities, WorkspaceFolder,
ClientCapabilities, ClientInfo, CodeActionClientCapabilities,
CompletionClientCapabilities, CompletionItemCapability,
CompletionItemCapabilityResolveSupport, DidChangeWatchedFilesClientCapabilities,
GotoCapability, HoverClientCapabilities, InitializeParams, MarkupKind,
PublishDiagnosticsClientCapabilities, RenameClientCapabilities,
SignatureHelpClientCapabilities, TextDocumentClientCapabilities,
TextDocumentSyncClientCapabilities, Uri, WindowClientCapabilities, WorkDoneProgressParams,
WorkspaceClientCapabilities, WorkspaceFolder,
};
use crate::supported_servers::LSPServerType;
@@ -301,14 +305,31 @@ fn default_client_capabilities() -> ClientCapabilities {
will_save_wait_until: Some(false),
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 {
dynamic_registration: Some(false),
link_support: Some(true),
}),
hover: Some(HoverClientCapabilities {
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]),
}),
publish_diagnostics: Some(PublishDiagnosticsClientCapabilities {
@@ -316,6 +337,19 @@ fn default_client_capabilities() -> ClientCapabilities {
related_information: Some(true),
..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()
+6 -2
View File
@@ -23,14 +23,18 @@ pub use config::{default_init_params, LanguageId, LspServerConfig};
pub use jsonrpc::{JsonRpcService, ServerNotificationEvent, Transport};
pub use lsp_types::{
notification::{self},
Position, Range,
CompletionItem, Position, Range,
};
pub use manager::{LspManagerModel, LspManagerModelEvent};
pub use model::{
BackgroundTaskInfo, DocumentDiagnostics, LanguageServerId, LspEvent, LspServerModel, LspState,
};
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)]
pub enum LspServerLogLevel {
+95 -4
View File
@@ -3,16 +3,17 @@ use crate::{
server_repo_watcher::LspRepoWatcher,
supported_servers::LSPServerType,
types::{
DefinitionLocation, DocumentVersion, HoverResult, Location, ReferenceLocation,
TextDocumentContentChangeEvent, TextEdit, WatchedFileChangeEvent,
CodeActionData, CompletionResult, CompletionTrigger, DefinitionLocation, DocumentVersion,
HoverResult, Location, PrepareRenameResult, ReferenceLocation, RenameResult,
SignatureHelpResult, TextDocumentContentChangeEvent, TextEdit, WatchedFileChangeEvent,
},
LspServerConfig, LspServerLogLevel, LspService,
};
use instant::Instant;
use lsp_types::{
notification::{self, Notification},
FormattingOptions, NumberOrString, ProgressParams, ProgressParamsValue,
PublishDiagnosticsParams, WorkDoneProgress,
CompletionItem, CompletionTriggerKind, FormattingOptions, NumberOrString, ProgressParams,
ProgressParamsValue, PublishDiagnosticsParams, Range as LspRange, WorkDoneProgress,
};
use std::{
collections::HashMap,
@@ -735,6 +736,96 @@ impl LspServerModel {
.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 {
+265 -8
View File
@@ -7,8 +7,9 @@ use std::{
use crate::{
config::{lsp_uri_to_path, path_to_lsp_uri, LanguageId},
types::{
HoverResult, LspDefinitionLocation, ReferenceLocation, TextDocumentContentChangeEvent,
TextEdit, WatchedFileChangeEvent,
CodeActionData, CompletionResult, FileEdits, HoverResult, Location, LspDefinitionLocation,
PrepareRenameResult, Range, ReferenceLocation, RenameResult, SignatureHelpResult,
TextDocumentContentChangeEvent, TextEdit, WatchedFileChangeEvent,
},
LspServerLogLevel,
};
@@ -19,12 +20,14 @@ use jsonrpc::{JsonRpcService, RequestId, ServerNotificationEvent};
use lsp_types::{
notification::{self, Notification},
request::{self, Request},
CancelParams, DidChangeTextDocumentParams, DidChangeWatchedFilesParams,
DidChangeWatchedFilesRegistrationOptions, DidCloseTextDocumentParams,
DidOpenTextDocumentParams, DocumentFormattingParams, FileChangeType, FileSystemWatcher,
FormattingOptions, GlobPattern, GotoDefinitionParams, GotoDefinitionResponse, HoverParams,
InitializeParams, InitializedParams, NumberOrString, OneOf, Position, ReferenceParams,
RegistrationParams, RelativePattern, TextDocumentIdentifier, TextDocumentItem,
CancelParams, CodeActionContext, CodeActionParams, CompletionContext, CompletionItem,
CompletionParams, CompletionTriggerKind, DidChangeTextDocumentParams,
DidChangeWatchedFilesParams, DidChangeWatchedFilesRegistrationOptions,
DidCloseTextDocumentParams, DidOpenTextDocumentParams, DocumentFormattingParams,
FileChangeType, FileSystemWatcher, FormattingOptions, GlobPattern, GotoDefinitionParams,
GotoDefinitionResponse, HoverParams, InitializeParams, InitializedParams, NumberOrString,
OneOf, Position, Range as LspRange, ReferenceParams, RegistrationParams, RelativePattern,
RenameParams, SignatureHelpParams, TextDocumentIdentifier, TextDocumentItem,
TextDocumentPositionParams, UnregistrationParams, VersionedTextDocumentIdentifier, WatchKind,
};
use serde_json::Value;
@@ -739,4 +742,258 @@ impl<'a> TextDocumentService<'a> {
.filter_map(|loc| ReferenceLocation::try_from(loc).ok())
.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
View File
@@ -1,7 +1,8 @@
use std::path::PathBuf;
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,
};
@@ -141,6 +142,7 @@ impl TryFrom<LspLocation> for ReferenceLocation {
}
/// Edit returned by the LSP.
#[derive(Debug, Clone)]
pub struct TextEdit {
pub range: Range,
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>,
}