use lsp_types::{GotoDefinitionResponse, Location, Position, TextEdit, Uri, WorkspaceEdit};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HierarchyDirection {
Subtypes,
Supertypes,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TypeHierarchyNode {
pub name: String,
pub path: String,
pub line: u32,
pub children: Vec<TypeHierarchyNode>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SymbolOverviewItem {
pub name: String,
pub kind: String,
pub line: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InspectionDiag {
pub path: String,
pub line: u32,
pub severity: String,
pub message: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InspectionInfo {
pub id: String,
pub name: String,
pub severity: String,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Truncation {
pub truncated: bool,
pub total: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TextRange0Based {
pub start_line: u32,
pub start_char: u32,
pub end_line: u32,
pub end_char: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RangeEdit {
pub abs_path: String,
pub rel_path: String,
pub range: TextRange0Based,
pub text: String,
pub expected_hash: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EditResult {
pub applied: bool,
pub new_range: TextRange0Based,
pub edited_text: String,
pub diff: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RenameQuery {
pub abs_path: String,
pub rel_path: String,
pub target_range: TextRange0Based,
pub new_name: String,
pub search_comments: bool,
pub search_text_occurrences: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UsageSite {
pub path: String,
pub range: TextRange0Based,
pub context: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Conflict {
pub path: String,
pub range: Option<TextRange0Based>,
pub message: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RenamePlan {
pub usages: Vec<UsageSite>,
pub conflicts: Vec<Conflict>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RenameApply {
pub abs_path: String,
pub rel_path: String,
pub target_range: TextRange0Based,
pub new_name: String,
pub force: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RenameResult {
pub applied: bool,
pub changed_paths: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MoveTarget {
Path { abs_path: String, rel_path: String },
Parent {
abs_path: String,
rel_path: String,
range: TextRange0Based,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MoveQuery {
pub abs_path: String,
pub rel_path: String,
pub src_range: TextRange0Based,
pub target: MoveTarget,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MoveApply {
pub query: MoveQuery,
pub force: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SafeDeleteQuery {
pub abs_path: String,
pub rel_path: String,
pub src_range: TextRange0Based,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SafeDeleteApply {
pub query: SafeDeleteQuery,
pub force: bool,
pub propagate: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InlineQuery {
pub abs_path: String,
pub rel_path: String,
pub src_range: TextRange0Based,
pub keep_definition: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InlineApply {
pub query: InlineQuery,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReformatScope {
File,
Region { range: TextRange0Based },
Symbol { range: TextRange0Based },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReformatQuery {
pub abs_path: String,
pub rel_path: String,
pub scope: ReformatScope,
pub optimize_imports: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReformatResult {
pub applied: bool,
pub changed_paths: Vec<String>,
}
pub trait LspBackend: Send {
fn open_file(&mut self, uri: &Uri, language_id: &str, text: &str) -> Result<(), String>;
fn references(
&mut self,
uri: &Uri,
position: Position,
scope: &str,
) -> Result<Vec<Location>, String>;
fn definition(
&mut self,
uri: &Uri,
position: Position,
) -> Result<GotoDefinitionResponse, String>;
fn implementations(
&mut self,
uri: &Uri,
position: Position,
scope: &str,
) -> Result<Vec<Location>, String>;
fn rename(
&mut self,
uri: &Uri,
position: Position,
new_name: &str,
) -> Result<Option<WorkspaceEdit>, String>;
fn declaration(&mut self, _uri: &Uri, _position: Position) -> Result<Vec<Location>, String> {
Err("declaration requires the JetBrains backend".to_string())
}
fn type_hierarchy(
&mut self,
_uri: &Uri,
_position: Position,
_direction: HierarchyDirection,
) -> Result<TypeHierarchyNode, String> {
Err("type_hierarchy requires the JetBrains backend".to_string())
}
fn symbols_overview(&mut self, uri: &Uri) -> Result<Vec<SymbolOverviewItem>, String> {
let abs = crate::lsp::client::uri_to_file_path(uri)
.ok_or_else(|| "symbols_overview: bad uri".to_string())?;
Ok(crate::lsp::edit_apply::overview_from_index(&abs))
}
fn format(&mut self, _uri: &Uri) -> Result<Vec<TextEdit>, String> {
Err("format requires the JetBrains backend".to_string())
}
fn inspections(&mut self, _uri: &Uri) -> Result<Vec<InspectionDiag>, String> {
Err("inspections requires the JetBrains backend".to_string())
}
fn list_inspections(&mut self) -> Result<Vec<InspectionInfo>, String> {
Err("list_inspections requires the JetBrains backend".to_string())
}
fn replace_symbol_body(&mut self, edit: &RangeEdit) -> Result<EditResult, String> {
crate::lsp::edit_apply::local_range_write(edit)
}
fn insert_before_symbol(&mut self, edit: &RangeEdit) -> Result<EditResult, String> {
crate::lsp::edit_apply::local_range_write(edit)
}
fn insert_after_symbol(&mut self, edit: &RangeEdit) -> Result<EditResult, String> {
crate::lsp::edit_apply::local_range_write(edit)
}
fn rename_preview(&mut self, _req: &RenameQuery) -> Result<RenamePlan, String> {
Err("BACKEND_REQUIRED: rename requires a running JetBrains IDE".to_string())
}
fn rename_apply(&mut self, _req: &RenameApply) -> Result<RenameResult, String> {
Err("BACKEND_REQUIRED: rename requires a running JetBrains IDE".to_string())
}
fn move_preview(&mut self, _req: &MoveQuery) -> Result<RenamePlan, String> {
Err("BACKEND_REQUIRED: move requires a running JetBrains IDE".to_string())
}
fn move_apply(&mut self, _req: &MoveApply) -> Result<RenameResult, String> {
Err("BACKEND_REQUIRED: move requires a running JetBrains IDE".to_string())
}
fn safe_delete_preview(&mut self, _req: &SafeDeleteQuery) -> Result<RenamePlan, String> {
Err("BACKEND_REQUIRED: safe_delete requires a running JetBrains IDE".to_string())
}
fn safe_delete_apply(&mut self, _req: &SafeDeleteApply) -> Result<RenameResult, String> {
Err("BACKEND_REQUIRED: safe_delete requires a running JetBrains IDE".to_string())
}
fn inline_preview(&mut self, _req: &InlineQuery) -> Result<RenamePlan, String> {
Err("BACKEND_REQUIRED: inline requires a running JetBrains IDE".to_string())
}
fn inline_apply(&mut self, _req: &InlineApply) -> Result<RenameResult, String> {
Err("BACKEND_REQUIRED: inline requires a running JetBrains IDE".to_string())
}
fn reformat(&mut self, _req: &ReformatQuery) -> Result<ReformatResult, String> {
Err("BACKEND_REQUIRED: reformat requires a running JetBrains IDE".to_string())
}
fn is_stale(&self, _project_root: &str) -> bool {
false
}
fn last_truncation(&self) -> Option<Truncation> {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rename_types_construct_and_clone() {
let q = RenameQuery {
abs_path: "/proj/a.rs".into(),
rel_path: "a.rs".into(),
target_range: TextRange0Based {
start_line: 0,
start_char: 0,
end_line: 0,
end_char: 3,
},
new_name: "bar".into(),
search_comments: false,
search_text_occurrences: false,
};
let q2 = q.clone();
assert_eq!(q2.new_name, "bar");
let plan = RenamePlan {
usages: vec![UsageSite {
path: "a.rs".into(),
range: TextRange0Based {
start_line: 1,
start_char: 4,
end_line: 1,
end_char: 7,
},
context: Some("foo()".into()),
}],
conflicts: vec![Conflict {
path: "a.rs".into(),
range: None,
message: "name already exists".into(),
}],
};
assert_eq!(plan.usages.len(), 1);
assert_eq!(plan.conflicts[0].message, "name already exists");
let apply = RenameApply {
abs_path: "/proj/a.rs".into(),
rel_path: "a.rs".into(),
target_range: q.target_range,
new_name: "bar".into(),
force: true,
};
let res = RenameResult {
applied: true,
changed_paths: vec!["a.rs".into()],
};
assert!(apply.force);
assert!(res.applied);
}
#[test]
fn move_and_safe_delete_types_construct_and_clone() {
let mt = MoveTarget::Path {
abs_path: "/proj/app/moved".into(),
rel_path: "app/moved".into(),
};
let mq = MoveQuery {
abs_path: "/proj/Widget.kt".into(),
rel_path: "Widget.kt".into(),
src_range: TextRange0Based {
start_line: 2,
start_char: 0,
end_line: 2,
end_char: 12,
},
target: mt.clone(),
};
let ma = MoveApply {
query: mq.clone(),
force: true,
};
assert_eq!(ma.query.target, mt);
let parent = MoveTarget::Parent {
abs_path: "/proj/Other.kt".into(),
rel_path: "Other.kt".into(),
range: TextRange0Based {
start_line: 0,
start_char: 0,
end_line: 5,
end_char: 1,
},
};
assert_ne!(parent, mt);
let sq = SafeDeleteQuery {
abs_path: "/proj/Widget.kt".into(),
rel_path: "Widget.kt".into(),
src_range: TextRange0Based {
start_line: 2,
start_char: 0,
end_line: 2,
end_char: 12,
},
};
let sa = SafeDeleteApply {
query: sq.clone(),
force: true,
propagate: false,
};
assert_eq!(sa.query, sq);
assert!(sa.force);
assert!(!sa.propagate);
}
#[test]
fn inline_and_reformat_types_construct_and_clone() {
let range = TextRange0Based {
start_line: 1,
start_char: 0,
end_line: 1,
end_char: 9,
};
let iq = InlineQuery {
abs_path: "/p/Calc.kt".into(),
rel_path: "Calc.kt".into(),
src_range: range,
keep_definition: false,
};
assert_eq!(iq.clone(), iq);
let ia = InlineApply { query: iq.clone() };
assert!(!ia.clone().query.keep_definition);
let rq = ReformatQuery {
abs_path: "/p/M.kt".into(),
rel_path: "M.kt".into(),
scope: ReformatScope::File,
optimize_imports: true,
};
assert_eq!(rq.clone(), rq);
assert!(matches!(
ReformatQuery {
scope: ReformatScope::Region { range },
..rq.clone()
}
.scope,
ReformatScope::Region { .. }
));
let rr = ReformatResult {
applied: true,
changed_paths: vec!["M.kt".into()],
};
assert_eq!(rr.clone(), rr);
}
#[test]
fn headless_inline_and_reformat_default_is_backend_required() {
struct Bare2;
impl LspBackend for Bare2 {
fn open_file(&mut self, _u: &Uri, _l: &str, _t: &str) -> Result<(), String> {
Ok(())
}
fn references(
&mut self,
_u: &Uri,
_p: Position,
_s: &str,
) -> Result<Vec<Location>, String> {
Ok(vec![])
}
fn definition(
&mut self,
_u: &Uri,
_p: Position,
) -> Result<GotoDefinitionResponse, String> {
Ok(GotoDefinitionResponse::Array(vec![]))
}
fn implementations(
&mut self,
_u: &Uri,
_p: Position,
_s: &str,
) -> Result<Vec<Location>, String> {
Ok(vec![])
}
fn rename(
&mut self,
_u: &Uri,
_p: Position,
_n: &str,
) -> Result<Option<WorkspaceEdit>, String> {
Ok(None)
}
}
let q = InlineQuery {
abs_path: "/a".into(),
rel_path: "a".into(),
src_range: TextRange0Based {
start_line: 0,
start_char: 0,
end_line: 0,
end_char: 0,
},
keep_definition: false,
};
assert!(Bare2
.inline_preview(&q)
.unwrap_err()
.contains("BACKEND_REQUIRED"));
assert!(Bare2
.inline_apply(&InlineApply { query: q.clone() })
.unwrap_err()
.contains("BACKEND_REQUIRED"));
let rq = ReformatQuery {
abs_path: "/a".into(),
rel_path: "a".into(),
scope: ReformatScope::File,
optimize_imports: false,
};
assert!(Bare2
.reformat(&rq)
.unwrap_err()
.contains("BACKEND_REQUIRED"));
}
#[test]
fn headless_rename_default_is_backend_required() {
let mut be = crate::lsp::edit_apply::HeadlessBackend;
let q = RenameQuery {
abs_path: "/x".into(),
rel_path: "x".into(),
target_range: TextRange0Based {
start_line: 0,
start_char: 0,
end_line: 0,
end_char: 1,
},
new_name: "y".into(),
search_comments: false,
search_text_occurrences: false,
};
let err = be.rename_preview(&q).unwrap_err();
assert!(err.starts_with("BACKEND_REQUIRED"), "got: {err}");
let a = RenameApply {
abs_path: "/x".into(),
rel_path: "x".into(),
target_range: q.target_range,
new_name: "y".into(),
force: false,
};
assert!(be
.rename_apply(&a)
.unwrap_err()
.starts_with("BACKEND_REQUIRED"));
}
#[test]
fn headless_move_and_safe_delete_default_is_backend_required() {
struct Bare;
impl LspBackend for Bare {
fn open_file(&mut self, _u: &lsp_types::Uri, _l: &str, _t: &str) -> Result<(), String> {
Ok(())
}
fn references(
&mut self,
_u: &lsp_types::Uri,
_p: lsp_types::Position,
_s: &str,
) -> Result<Vec<lsp_types::Location>, String> {
Ok(vec![])
}
fn definition(
&mut self,
_u: &lsp_types::Uri,
_p: lsp_types::Position,
) -> Result<lsp_types::GotoDefinitionResponse, String> {
Ok(lsp_types::GotoDefinitionResponse::Array(vec![]))
}
fn implementations(
&mut self,
_u: &lsp_types::Uri,
_p: lsp_types::Position,
_s: &str,
) -> Result<Vec<lsp_types::Location>, String> {
Ok(vec![])
}
fn rename(
&mut self,
_u: &lsp_types::Uri,
_p: lsp_types::Position,
_n: &str,
) -> Result<Option<lsp_types::WorkspaceEdit>, String> {
Ok(None)
}
}
let mut b = Bare;
let mq = MoveQuery {
abs_path: "/p/a.kt".into(),
rel_path: "a.kt".into(),
src_range: TextRange0Based {
start_line: 0,
start_char: 0,
end_line: 0,
end_char: 1,
},
target: MoveTarget::Path {
abs_path: "/p/x".into(),
rel_path: "x".into(),
},
};
assert!(b
.move_preview(&mq)
.unwrap_err()
.starts_with("BACKEND_REQUIRED"));
assert!(b
.move_apply(&MoveApply {
query: mq,
force: false
})
.unwrap_err()
.starts_with("BACKEND_REQUIRED"));
let sq = SafeDeleteQuery {
abs_path: "/p/a.kt".into(),
rel_path: "a.kt".into(),
src_range: TextRange0Based {
start_line: 0,
start_char: 0,
end_line: 0,
end_char: 1,
},
};
assert!(b
.safe_delete_preview(&sq)
.unwrap_err()
.starts_with("BACKEND_REQUIRED"));
assert!(b
.safe_delete_apply(&SafeDeleteApply {
query: sq,
force: false,
propagate: false
})
.unwrap_err()
.starts_with("BACKEND_REQUIRED"));
}
}