feat: persist local reviews automatically in SQLite
This commit is contained in:
+111
-4
@@ -33,6 +33,7 @@ const INVOKE_TIMEOUT: Duration = Duration::from_millis(500);
|
||||
pub struct Config {
|
||||
pub plugins: Vec<PluginConfig>,
|
||||
pub lsp: Vec<crate::lsp::ServerConfig>,
|
||||
pub review_directory: Option<PathBuf>,
|
||||
pub bindings: Vec<Binding>,
|
||||
pub leader: String,
|
||||
pub file_explorer_side: String,
|
||||
@@ -85,6 +86,7 @@ impl Default for Config {
|
||||
Self {
|
||||
plugins: Vec::new(),
|
||||
lsp: Vec::new(),
|
||||
review_directory: None,
|
||||
bindings: Vec::new(),
|
||||
leader: "space".into(),
|
||||
file_explorer_side: "right".into(),
|
||||
@@ -113,7 +115,7 @@ impl Default for Config {
|
||||
}
|
||||
}
|
||||
|
||||
/// Relative plugin paths are resolved against the configuration file's directory.
|
||||
/// Relative plugin and review paths are resolved against the configuration file's directory.
|
||||
pub fn load_config(path: &Path) -> Result<Config, String> {
|
||||
let deadline = Instant::now() + STARTUP_TIMEOUT;
|
||||
let path = absolute_file(path)?;
|
||||
@@ -124,6 +126,7 @@ pub fn load_config(path: &Path) -> Result<Config, String> {
|
||||
let ConfigFrame {
|
||||
plugins,
|
||||
lsp,
|
||||
review_directory,
|
||||
bindings,
|
||||
leader,
|
||||
file_explorer_side,
|
||||
@@ -152,6 +155,7 @@ pub fn load_config(path: &Path) -> Result<Config, String> {
|
||||
} = *response;
|
||||
let mut config = Config::default();
|
||||
let directory = path.parent().expect("absolute file has a parent");
|
||||
config.review_directory = review_directory.map(|path| directory.join(path));
|
||||
for plugin in plugins {
|
||||
let plugin_path = plugin.path.to_string_lossy();
|
||||
if plugin_path.is_empty() || plugin_path.contains('\0') {
|
||||
@@ -490,6 +494,21 @@ fn scroll_distance<'de, D: serde::Deserializer<'de>>(
|
||||
})
|
||||
}
|
||||
|
||||
fn review_directory<'de, D: serde::Deserializer<'de>>(
|
||||
deserializer: D,
|
||||
) -> Result<Option<PathBuf>, D::Error> {
|
||||
let directory = Option::<String>::deserialize(deserializer)?;
|
||||
if directory
|
||||
.as_ref()
|
||||
.is_some_and(|path| path.is_empty() || path.contains('\0'))
|
||||
{
|
||||
return Err(serde::de::Error::custom(
|
||||
"review_directory must not be empty or contain NUL",
|
||||
));
|
||||
}
|
||||
Ok(directory.map(PathBuf::from))
|
||||
}
|
||||
|
||||
// Startup-only configuration does not inflate every queued plugin response.
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -499,6 +518,8 @@ struct ConfigFrame {
|
||||
// Keep this wire field required; resolve its configuration after decoding.
|
||||
#[serde(deserialize_with = "serde_json::Value::deserialize")]
|
||||
lsp: serde_json::Value,
|
||||
#[serde(deserialize_with = "review_directory")]
|
||||
review_directory: Option<PathBuf>,
|
||||
bindings: Vec<Binding>,
|
||||
leader: String,
|
||||
file_explorer_side: String,
|
||||
@@ -1003,6 +1024,87 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn review_directory_resolves_config_paths_and_keeps_session_startup_lazy() {
|
||||
let fixture = Fixture::new();
|
||||
let absolute = fixture.0.join("absolute-reviews");
|
||||
for (value, expected) in [
|
||||
(
|
||||
r#""relative-reviews""#.to_owned(),
|
||||
Some(fixture.0.join("relative-reviews")),
|
||||
),
|
||||
(serde_json::to_string(&absolute).unwrap(), Some(absolute)),
|
||||
("nil".to_owned(), None),
|
||||
] {
|
||||
let path = fixture.file(
|
||||
"config.rb",
|
||||
&format!("Vamp.configure {{ |config| config.review_directory = {value} }}"),
|
||||
);
|
||||
assert_eq!(load_config(&path).unwrap().review_directory, expected);
|
||||
let session = EditorSession::open(None, vec![], Some(&path)).unwrap();
|
||||
assert!(session.editor().startup_diagnostic().is_none());
|
||||
if let Some(directory) = expected {
|
||||
assert!(
|
||||
!directory.exists(),
|
||||
"startup created {}",
|
||||
directory.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
let path = fixture.file("config.rb", "Vamp.configure { |_| }");
|
||||
assert_eq!(load_config(&path).unwrap().review_directory, None);
|
||||
for value in [
|
||||
"7",
|
||||
"false",
|
||||
"[]",
|
||||
"{}",
|
||||
":reviews",
|
||||
r#""""#,
|
||||
r#""bad\0path""#,
|
||||
] {
|
||||
let path = fixture.file(
|
||||
"config.rb",
|
||||
&format!("Vamp.configure {{ |config| config.review_directory = {value} }}"),
|
||||
);
|
||||
let error = load_config(&path).unwrap_err();
|
||||
assert!(error.contains("review_directory"), "{value}: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn review_directory_wire_field_is_required_nullable_and_validated() {
|
||||
let mut frame = serde_json::json!({
|
||||
"version": 1, "type": "config", "plugins": [], "lsp": {}, "bindings": [],
|
||||
"review_directory": null, "leader": "space",
|
||||
"file_explorer_side": "right", "file_picker_view": "tree", "file_explorer_view": "tree",
|
||||
"clipboard_mode": "system", "git_blame": true, "git_gutter": "bar",
|
||||
"top_bar": {}, "bottom_bar": {}, "popup": {"command": {}, "search": {}}, "labels": {},
|
||||
"scroll_distance": 0, "scroll_step_lines": null, "highlight_trailing_whitespace": false,
|
||||
"whitespace_characters": {}, "whitespace_scope": "all",
|
||||
"tab_spaces": null, "language_tab_spaces": {},
|
||||
"diff_context_lines": 10, "diff_expand_lines": 15,
|
||||
"diff_view_mode": "split", "diff_split_orientation": "vertical",
|
||||
"diff_follow_working_copy": true
|
||||
});
|
||||
for value in [serde_json::Value::Null, serde_json::json!("reviews")] {
|
||||
frame["review_directory"] = value;
|
||||
assert!(serde_json::from_value::<Response>(frame.clone()).is_ok());
|
||||
}
|
||||
for value in [
|
||||
serde_json::json!(""),
|
||||
serde_json::json!("bad\0path"),
|
||||
serde_json::json!(7),
|
||||
serde_json::json!(false),
|
||||
serde_json::json!([]),
|
||||
serde_json::json!({}),
|
||||
] {
|
||||
frame["review_directory"] = value;
|
||||
assert!(serde_json::from_value::<Response>(frame.clone()).is_err());
|
||||
}
|
||||
frame.as_object_mut().unwrap().remove("review_directory");
|
||||
assert!(serde_json::from_value::<Response>(frame).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_spaces_follow_active_language_and_preserve_native_edit_semantics() {
|
||||
let fixture = Fixture::new();
|
||||
@@ -1410,9 +1512,9 @@ end
|
||||
assert!(load_config(&path).unwrap_err().contains(error), "{value}");
|
||||
}
|
||||
for frame in [
|
||||
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"bindings":[],"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","git_blame":true,"git_gutter":"bar","scroll_distance":0,"highlight_trailing_whitespace":false,"whitespace_characters":{},"whitespace_scope":"all","tab_spaces":null,"language_tab_spaces":{},"top_bar":{},"bottom_bar":{},"popup":{"command":{},"search":{}},"labels":{},"diff_context_lines":10,"diff_expand_lines":15,"diff_view_mode":"split","diff_split_orientation":"vertical","diff_follow_working_copy":true}"#,
|
||||
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"bindings":[],"leader":null,"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","git_blame":true,"git_gutter":"bar","scroll_distance":0,"highlight_trailing_whitespace":false,"whitespace_characters":{},"whitespace_scope":"all","tab_spaces":null,"language_tab_spaces":{},"top_bar":{},"bottom_bar":{},"popup":{"command":{},"search":{}},"labels":{},"diff_context_lines":10,"diff_expand_lines":15,"diff_view_mode":"split","diff_split_orientation":"vertical","diff_follow_working_copy":true}"#,
|
||||
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"bindings":[],"leader":7,"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","git_blame":true,"git_gutter":"bar","scroll_distance":0,"highlight_trailing_whitespace":false,"whitespace_characters":{},"whitespace_scope":"all","tab_spaces":null,"language_tab_spaces":{},"top_bar":{},"bottom_bar":{},"popup":{"command":{},"search":{}},"labels":{},"diff_context_lines":10,"diff_expand_lines":15,"diff_view_mode":"split","diff_split_orientation":"vertical","diff_follow_working_copy":true}"#,
|
||||
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"review_directory":null,"bindings":[],"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","git_blame":true,"git_gutter":"bar","scroll_distance":0,"highlight_trailing_whitespace":false,"whitespace_characters":{},"whitespace_scope":"all","tab_spaces":null,"language_tab_spaces":{},"top_bar":{},"bottom_bar":{},"popup":{"command":{},"search":{}},"labels":{},"diff_context_lines":10,"diff_expand_lines":15,"diff_view_mode":"split","diff_split_orientation":"vertical","diff_follow_working_copy":true}"#,
|
||||
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"review_directory":null,"bindings":[],"leader":null,"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","git_blame":true,"git_gutter":"bar","scroll_distance":0,"highlight_trailing_whitespace":false,"whitespace_characters":{},"whitespace_scope":"all","tab_spaces":null,"language_tab_spaces":{},"top_bar":{},"bottom_bar":{},"popup":{"command":{},"search":{}},"labels":{},"diff_context_lines":10,"diff_expand_lines":15,"diff_view_mode":"split","diff_split_orientation":"vertical","diff_follow_working_copy":true}"#,
|
||||
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"review_directory":null,"bindings":[],"leader":7,"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","git_blame":true,"git_gutter":"bar","scroll_distance":0,"highlight_trailing_whitespace":false,"whitespace_characters":{},"whitespace_scope":"all","tab_spaces":null,"language_tab_spaces":{},"top_bar":{},"bottom_bar":{},"popup":{"command":{},"search":{}},"labels":{},"diff_context_lines":10,"diff_expand_lines":15,"diff_view_mode":"split","diff_split_orientation":"vertical","diff_follow_working_copy":true}"#,
|
||||
] {
|
||||
assert!(serde_json::from_str::<Response>(frame).is_err(), "{frame}");
|
||||
}
|
||||
@@ -1485,6 +1587,7 @@ end
|
||||
}
|
||||
let mut frame = serde_json::json!({
|
||||
"type": "config", "version": 1, "plugins": [], "lsp": {}, "bindings": [],
|
||||
"review_directory": null,
|
||||
"leader": "space", "file_explorer_side": "right", "file_picker_view": "tree",
|
||||
"file_explorer_view": "tree", "clipboard_mode": "system", "git_blame": true,
|
||||
"git_gutter": "bar",
|
||||
@@ -1627,6 +1730,7 @@ end"#,
|
||||
}
|
||||
let mut frame = serde_json::json!({
|
||||
"type": "config", "version": 1, "plugins": [], "lsp": {}, "bindings": [],
|
||||
"review_directory": null,
|
||||
"leader": "space", "file_explorer_side": "right", "file_picker_view": "tree",
|
||||
"file_explorer_view": "tree", "clipboard_mode": "system", "git_blame": true,
|
||||
"git_gutter": "bar", "scroll_distance": 0, "highlight_trailing_whitespace": false,
|
||||
@@ -1886,6 +1990,7 @@ end
|
||||
}
|
||||
let frame = serde_json::json!({
|
||||
"version": 1, "type": "config", "plugins": [], "lsp": {},
|
||||
"review_directory": null,
|
||||
"bindings": [], "leader": "space", "file_explorer_side": "right",
|
||||
"file_picker_view": "tree", "file_explorer_view": "tree",
|
||||
"top_bar": {}, "bottom_bar": {}, "labels": {},
|
||||
@@ -1997,6 +2102,7 @@ end"
|
||||
let fixture = Fixture::new();
|
||||
let frame = serde_json::json!({
|
||||
"version": 1, "type": "config", "plugins": [], "lsp": {},
|
||||
"review_directory": null,
|
||||
"bindings": [], "leader": "space", "file_explorer_side": "right",
|
||||
"file_picker_view": "tree", "file_explorer_view": "tree",
|
||||
"clipboard_mode": "system", "git_blame": true, "git_gutter": "bar",
|
||||
@@ -2427,6 +2533,7 @@ end"#,
|
||||
fn lsp_wire_configuration_is_resolved_after_decoding() {
|
||||
let mut frame = serde_json::json!({
|
||||
"version": 1, "type": "config", "plugins": [], "lsp": {},
|
||||
"review_directory": null,
|
||||
"bindings": [], "leader": "space", "file_explorer_side": "right",
|
||||
"file_picker_view": "tree", "file_explorer_view": "tree",
|
||||
"git_blame": true,
|
||||
|
||||
Reference in New Issue
Block a user