feat: migrate LSP to opt-in native language servers
This commit is contained in:
+24
-2
@@ -896,8 +896,20 @@ impl Editor {
|
||||
name: "core".into(),
|
||||
required_capabilities: Vec::new(),
|
||||
commands: [
|
||||
"w", "q", "q!", "wq", "plugins", "edit", "buffers", "buffer", "bnext",
|
||||
"bprev", "bdelete", "bdelete!",
|
||||
"w",
|
||||
"q",
|
||||
"q!",
|
||||
"wq",
|
||||
"plugins",
|
||||
"edit",
|
||||
"buffers",
|
||||
"buffer",
|
||||
"bnext",
|
||||
"bprev",
|
||||
"bdelete",
|
||||
"bdelete!",
|
||||
"lsp_definition",
|
||||
"lsp_diagnostics",
|
||||
]
|
||||
.into_iter()
|
||||
.map(str::to_owned)
|
||||
@@ -2384,6 +2396,16 @@ impl Editor {
|
||||
});
|
||||
}
|
||||
match name {
|
||||
"lsp_definition" | "lsp_diagnostics" if !args.is_empty() => {
|
||||
self.set_message(format!("The {name} command takes no arguments"));
|
||||
}
|
||||
"lsp_definition" | "lsp_diagnostics" => {
|
||||
return Some(Invocation {
|
||||
plugin: "core".into(),
|
||||
command: name.into(),
|
||||
args: String::new(),
|
||||
});
|
||||
}
|
||||
"w" | "wq" => {
|
||||
if self.save(if args.is_empty() {
|
||||
None
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
pub mod clipboard;
|
||||
pub mod editor;
|
||||
pub mod files;
|
||||
pub mod lsp;
|
||||
mod lsp_config;
|
||||
pub mod plugin;
|
||||
pub mod ruby;
|
||||
pub mod session;
|
||||
|
||||
+1800
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,154 @@
|
||||
use crate::lsp::ServerConfig;
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
|
||||
pub fn resolve(value: Value) -> Result<Vec<ServerConfig>, String> {
|
||||
let Value::Object(languages) = value else {
|
||||
return Err("LSP configuration must be a language-keyed object".into());
|
||||
};
|
||||
let mut servers = Vec::new();
|
||||
for (language_id, value) in languages {
|
||||
if language_id.is_empty() || language_id.chars().any(char::is_control) {
|
||||
return Err("LSP language keys must not be empty or contain control characters".into());
|
||||
}
|
||||
let preset = match language_id.as_str() {
|
||||
"rust" => Some(("rust-analyzer", "rs")),
|
||||
"python" => Some(("pylsp", "py")),
|
||||
_ => None,
|
||||
};
|
||||
let mut options = match value {
|
||||
Value::Bool(false) => continue,
|
||||
Value::Bool(true) => {
|
||||
if preset.is_none() {
|
||||
return Err(format!(
|
||||
"LSP language {language_id:?} has no built-in preset; provide command and extensions"
|
||||
));
|
||||
}
|
||||
serde_json::Map::new()
|
||||
}
|
||||
Value::Object(options) => options,
|
||||
_ => {
|
||||
return Err(format!(
|
||||
"LSP language {language_id:?} must be true, false, or a configuration object"
|
||||
));
|
||||
}
|
||||
};
|
||||
for key in options.keys() {
|
||||
if !matches!(
|
||||
key.as_str(),
|
||||
"command" | "extensions" | "initialization_options" | "settings"
|
||||
) {
|
||||
return Err(format!(
|
||||
"LSP language {language_id:?} has unknown option {key:?}"
|
||||
));
|
||||
}
|
||||
}
|
||||
let command = match options.remove("command") {
|
||||
Some(value) => command(value, &language_id)?,
|
||||
None => match preset {
|
||||
Some((command, _)) => vec![command.into()],
|
||||
None => {
|
||||
return Err(format!(
|
||||
"LSP language {language_id:?} requires command argv"
|
||||
));
|
||||
}
|
||||
},
|
||||
};
|
||||
let extensions = match options.remove("extensions") {
|
||||
Some(value) => extensions(value, &language_id)?,
|
||||
None => match preset {
|
||||
Some((_, extension)) => vec![extension.into()],
|
||||
None => return Err(format!("LSP language {language_id:?} requires extensions")),
|
||||
},
|
||||
};
|
||||
let initialization_options = object(
|
||||
options.remove("initialization_options"),
|
||||
&language_id,
|
||||
"initialization_options",
|
||||
)?;
|
||||
let settings = object(options.remove("settings"), &language_id, "settings")?;
|
||||
servers.push(ServerConfig {
|
||||
language_id,
|
||||
command,
|
||||
extensions,
|
||||
initialization_options,
|
||||
settings,
|
||||
});
|
||||
if servers.len() > 16 {
|
||||
return Err("LSP configuration supports at most 16 enabled languages".into());
|
||||
}
|
||||
}
|
||||
Ok(servers)
|
||||
}
|
||||
|
||||
pub(crate) fn deserialize<'de, D: serde::Deserializer<'de>>(
|
||||
deserializer: D,
|
||||
) -> Result<Vec<ServerConfig>, D::Error> {
|
||||
resolve(Value::deserialize(deserializer)?).map_err(serde::de::Error::custom)
|
||||
}
|
||||
|
||||
fn command(value: Value, language: &str) -> Result<Vec<String>, String> {
|
||||
let error = || {
|
||||
format!(
|
||||
"LSP language {language:?} command must be nonempty argv with a nonempty executable and no NUL bytes"
|
||||
)
|
||||
};
|
||||
let Value::Array(values) = value else {
|
||||
return Err(error());
|
||||
};
|
||||
if values.is_empty() {
|
||||
return Err(error());
|
||||
}
|
||||
let mut command = Vec::with_capacity(values.len());
|
||||
for value in values {
|
||||
let Value::String(argument) = value else {
|
||||
return Err(error());
|
||||
};
|
||||
if argument.contains('\0') || (command.is_empty() && argument.is_empty()) {
|
||||
return Err(error());
|
||||
}
|
||||
command.push(argument);
|
||||
}
|
||||
Ok(command)
|
||||
}
|
||||
|
||||
fn extensions(value: Value, language: &str) -> Result<Vec<String>, String> {
|
||||
let error = || {
|
||||
format!(
|
||||
"LSP language {language:?} extensions must be a nonempty array of suffixes without dots, separators, whitespace, or control characters"
|
||||
)
|
||||
};
|
||||
let Value::Array(values) = value else {
|
||||
return Err(error());
|
||||
};
|
||||
if values.is_empty() {
|
||||
return Err(error());
|
||||
}
|
||||
let mut extensions = Vec::with_capacity(values.len());
|
||||
for value in values {
|
||||
let Value::String(extension) = value else {
|
||||
return Err(error());
|
||||
};
|
||||
if extension.is_empty()
|
||||
|| extension.chars().any(|character| {
|
||||
matches!(character, '.' | '/' | '\\')
|
||||
|| character.is_whitespace()
|
||||
|| character.is_control()
|
||||
})
|
||||
{
|
||||
return Err(error());
|
||||
}
|
||||
extensions.push(extension);
|
||||
}
|
||||
Ok(extensions)
|
||||
}
|
||||
|
||||
fn object(value: Option<Value>, language: &str, option: &str) -> Result<Value, String> {
|
||||
match value {
|
||||
None => Ok(Value::Object(serde_json::Map::new())),
|
||||
Some(value) if value.is_object() => Ok(value),
|
||||
Some(_) => Err(format!(
|
||||
"LSP language {language:?} {option} must be a JSON object"
|
||||
)),
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -36,8 +36,9 @@ Buffers: :edit PATH, :buffers, :buffer N, :bnext, :bprev, :bdelete[!] [N]
|
||||
Ruby config: $XDG_CONFIG_HOME/vamp/config.rb or ~/.config/vamp/config.rb
|
||||
Set VAMP_RUBY to choose the Ruby executable.
|
||||
|
||||
Optional LSP Ruby plugin: config.plugin PATH, options: { servers: [...] }
|
||||
LSP commands when enabled: :lsp_definition, :lsp_diagnostics";
|
||||
Native LSP is disabled by default: config.lsp[\"rust\"] = true enables a preset
|
||||
Custom languages: config.lsp[\"my-language\"] = { command: [...], extensions: [...] }
|
||||
LSP commands: :lsp_definition, :lsp_diagnostics";
|
||||
|
||||
struct Options {
|
||||
file: Option<PathBuf>,
|
||||
|
||||
+215
-3
@@ -19,6 +19,7 @@ const INVOKE_TIMEOUT: Duration = Duration::from_millis(500);
|
||||
#[derive(Debug)]
|
||||
pub struct Config {
|
||||
pub plugins: Vec<PluginConfig>,
|
||||
pub lsp: Vec<crate::lsp::ServerConfig>,
|
||||
pub bindings: Vec<Binding>,
|
||||
pub leader: String,
|
||||
pub file_explorer_side: String,
|
||||
@@ -52,6 +53,7 @@ impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
plugins: Vec::new(),
|
||||
lsp: Vec::new(),
|
||||
bindings: Vec::new(),
|
||||
leader: "space".into(),
|
||||
file_explorer_side: "right".into(),
|
||||
@@ -71,6 +73,7 @@ pub fn load_config(path: &Path) -> Result<Config, String> {
|
||||
match response {
|
||||
Response::Config {
|
||||
plugins,
|
||||
lsp,
|
||||
bindings,
|
||||
leader,
|
||||
file_explorer_side,
|
||||
@@ -92,6 +95,7 @@ pub fn load_config(path: &Path) -> Result<Config, String> {
|
||||
});
|
||||
}
|
||||
config.bindings = bindings;
|
||||
config.lsp = lsp;
|
||||
config.leader = leader;
|
||||
config.file_explorer_side = file_explorer_side;
|
||||
config.file_picker_view = file_picker_view;
|
||||
@@ -375,6 +379,8 @@ enum Response {
|
||||
Config {
|
||||
version: u8,
|
||||
plugins: Vec<PluginConfig>,
|
||||
#[serde(deserialize_with = "crate::lsp_config::deserialize")]
|
||||
lsp: Vec<crate::lsp::ServerConfig>,
|
||||
bindings: Vec<Binding>,
|
||||
leader: String,
|
||||
file_explorer_side: String,
|
||||
@@ -879,6 +885,7 @@ end
|
||||
let defaults = Config::default();
|
||||
assert_eq!(defaults.leader, "space");
|
||||
assert!(defaults.plugins.is_empty() && defaults.bindings.is_empty());
|
||||
assert!(defaults.lsp.is_empty());
|
||||
let path = fixture.file("config.rb", "Vamp.configure { |_| }");
|
||||
assert_eq!(load_config(&path).unwrap().leader, "space");
|
||||
for (source, key) in [
|
||||
@@ -900,14 +907,219 @@ end
|
||||
assert!(load_config(&path).unwrap_err().contains(error), "{value}");
|
||||
}
|
||||
for frame in [
|
||||
r#"{"type":"config","version":1,"plugins":[],"bindings":[],"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system"}"#,
|
||||
r#"{"type":"config","version":1,"plugins":[],"bindings":[],"leader":null,"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system"}"#,
|
||||
r#"{"type":"config","version":1,"plugins":[],"bindings":[],"leader":7,"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system"}"#,
|
||||
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"bindings":[],"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system"}"#,
|
||||
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"}"#,
|
||||
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"}"#,
|
||||
] {
|
||||
assert!(serde_json::from_str::<Response>(frame).is_err(), "{frame}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_configuration_is_disabled_unless_explicitly_enabled() {
|
||||
let fixture = Fixture::new();
|
||||
for source in [
|
||||
"Vamp.configure { |_| }",
|
||||
r#"Vamp.configure do |config|
|
||||
config.lsp["rust"] = false
|
||||
config.lsp["python"] = false
|
||||
config.lsp["not-a-preset"] = false
|
||||
end"#,
|
||||
] {
|
||||
let path = fixture.file("config.rb", source);
|
||||
let config = load_config(&path).unwrap();
|
||||
assert!(config.lsp.is_empty());
|
||||
assert!(config.plugins.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_presets_custom_languages_and_overrides_reach_native_configuration() {
|
||||
let fixture = Fixture::new();
|
||||
let path = fixture.file(
|
||||
"config.rb",
|
||||
r#"Vamp.configure do |config|
|
||||
config.lsp["rust"] = true
|
||||
config.lsp["python"] = true
|
||||
end"#,
|
||||
);
|
||||
let config = load_config(&path).unwrap();
|
||||
assert_eq!(config.lsp.len(), 2);
|
||||
for (language, command, extension) in
|
||||
[("rust", "rust-analyzer", "rs"), ("python", "pylsp", "py")]
|
||||
{
|
||||
let server = config
|
||||
.lsp
|
||||
.iter()
|
||||
.find(|server| server.language_id == language)
|
||||
.unwrap();
|
||||
assert_eq!(server.command, [command]);
|
||||
assert_eq!(server.extensions, [extension]);
|
||||
assert_eq!(server.initialization_options, serde_json::json!({}));
|
||||
assert_eq!(server.settings, serde_json::json!({}));
|
||||
}
|
||||
|
||||
let path = fixture.file(
|
||||
"config.rb",
|
||||
r#"Vamp.configure do |config|
|
||||
config.lsp["rust"] = {
|
||||
extensions: ["rs", "ron"],
|
||||
initialization_options: {cargo: {features: ["α"]}},
|
||||
settings: {"rust-analyzer" => {check: {command: "clippy"}}}
|
||||
}
|
||||
config.lsp["python"] = {command: ["custom pylsp", "--stdio"]}
|
||||
config.lsp["my-language.c++"] = {
|
||||
command: ["custom server", "--flag", ""],
|
||||
extensions: ["custom", "لغة"],
|
||||
settings: {nested: {enabled: true, limit: 3, unset: nil}}
|
||||
}
|
||||
end"#,
|
||||
);
|
||||
let config = load_config(&path).unwrap();
|
||||
assert_eq!(config.lsp.len(), 3);
|
||||
let rust = config
|
||||
.lsp
|
||||
.iter()
|
||||
.find(|server| server.language_id == "rust")
|
||||
.unwrap();
|
||||
assert_eq!(rust.command, ["rust-analyzer"]);
|
||||
assert_eq!(rust.extensions, ["rs", "ron"]);
|
||||
assert_eq!(
|
||||
rust.initialization_options,
|
||||
serde_json::json!({"cargo": {"features": ["α"]}})
|
||||
);
|
||||
assert_eq!(
|
||||
rust.settings,
|
||||
serde_json::json!({"rust-analyzer": {"check": {"command": "clippy"}}})
|
||||
);
|
||||
let python = config
|
||||
.lsp
|
||||
.iter()
|
||||
.find(|server| server.language_id == "python")
|
||||
.unwrap();
|
||||
assert_eq!(python.command, ["custom pylsp", "--stdio"]);
|
||||
assert_eq!(python.extensions, ["py"]);
|
||||
assert_eq!(python.initialization_options, serde_json::json!({}));
|
||||
assert_eq!(python.settings, serde_json::json!({}));
|
||||
let custom = config
|
||||
.lsp
|
||||
.iter()
|
||||
.find(|server| server.language_id == "my-language.c++")
|
||||
.unwrap();
|
||||
assert_eq!(custom.command, ["custom server", "--flag", ""]);
|
||||
assert_eq!(custom.extensions, ["custom", "لغة"]);
|
||||
assert_eq!(custom.initialization_options, serde_json::json!({}));
|
||||
assert_eq!(
|
||||
custom.settings,
|
||||
serde_json::json!({"nested": {"enabled": true, "limit": 3, "unset": null}})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_configuration_rejects_invalid_keys_values_and_json_options() {
|
||||
let fixture = Fixture::new();
|
||||
for assignment in [
|
||||
r#"config.lsp[:rust] = true"#,
|
||||
r#"config.lsp[7] = false"#,
|
||||
r#"config.lsp[""] = false"#,
|
||||
r#"config.lsp["bad\nkey"] = false"#,
|
||||
r#"config.lsp["\xFF".b] = false"#,
|
||||
r#"config.lsp["rust"] = nil"#,
|
||||
r#"config.lsp["rust"] = []"#,
|
||||
r#"config.lsp["rust"] = "yes""#,
|
||||
r#"config.lsp["custom"] = {}"#,
|
||||
r#"config.lsp["custom"] = {command: ["server"]}"#,
|
||||
r#"config.lsp["custom"] = {extensions: ["custom"]}"#,
|
||||
r#"config.lsp["rust"] = {command: "server"}"#,
|
||||
r#"config.lsp["rust"] = {command: []}"#,
|
||||
r#"config.lsp["rust"] = {command: [""]}"#,
|
||||
r#"config.lsp["rust"] = {command: ["server", 7]}"#,
|
||||
r#"config.lsp["rust"] = {command: ["server", "bad\0argument"]}"#,
|
||||
r#"config.lsp["rust"] = {extensions: "rs"}"#,
|
||||
r#"config.lsp["rust"] = {extensions: []}"#,
|
||||
r#"config.lsp["rust"] = {extensions: [""]}"#,
|
||||
r#"config.lsp["rust"] = {extensions: [7]}"#,
|
||||
r#"config.lsp["rust"] = {extensions: [".rs"]}"#,
|
||||
r#"config.lsp["rust"] = {extensions: ["path/rs"]}"#,
|
||||
r#"config.lsp["rust"] = {extensions: ["path\\rs"]}"#,
|
||||
r#"config.lsp["rust"] = {extensions: ["r s"]}"#,
|
||||
r#"config.lsp["rust"] = {extensions: ["r\u0001s"]}"#,
|
||||
r#"config.lsp["rust"] = {initialization_options: []}"#,
|
||||
r#"config.lsp["rust"] = {settings: nil}"#,
|
||||
r#"config.lsp["rust"] = {settings: {bad: Object.new}}"#,
|
||||
r#"config.lsp["rust"] = {settings: {bad: Float::NAN}}"#,
|
||||
r#"config.lsp["rust"] = {settings: {7 => true}}"#,
|
||||
r#"config.lsp["rust"] = {unknown: true}"#,
|
||||
] {
|
||||
let path = fixture.file(
|
||||
"invalid.rb",
|
||||
&format!("Vamp.configure {{ |config| {assignment} }}"),
|
||||
);
|
||||
assert!(load_config(&path).is_err(), "{assignment}");
|
||||
}
|
||||
let path = fixture.file(
|
||||
"invalid.rb",
|
||||
r#"Vamp.configure { |config| config.lsp["unlisted"] = true }"#,
|
||||
);
|
||||
let error = load_config(&path).unwrap_err();
|
||||
assert!(
|
||||
error.contains("unlisted") && error.contains("preset"),
|
||||
"{error}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_wire_configuration_is_validated_before_being_consumed() {
|
||||
let mut frame = serde_json::json!({
|
||||
"version": 1, "type": "config", "plugins": [], "lsp": {},
|
||||
"bindings": [], "leader": "space", "file_explorer_side": "right",
|
||||
"file_picker_view": "tree", "file_explorer_view": "tree",
|
||||
"clipboard_mode": "system"
|
||||
});
|
||||
assert!(matches!(
|
||||
serde_json::from_value::<Response>(frame.clone()).unwrap(),
|
||||
Response::Config { lsp, .. } if lsp.is_empty()
|
||||
));
|
||||
frame["lsp"] = serde_json::json!({"rust": true});
|
||||
assert!(matches!(
|
||||
serde_json::from_value::<Response>(frame.clone()).unwrap(),
|
||||
Response::Config { lsp, .. }
|
||||
if lsp.len() == 1 && lsp[0].command == ["rust-analyzer"]
|
||||
));
|
||||
for value in [
|
||||
serde_json::json!(null),
|
||||
serde_json::json!([]),
|
||||
serde_json::json!({"": false}),
|
||||
serde_json::json!({"bad\u{0085}key": false}),
|
||||
serde_json::json!({"rust": null}),
|
||||
serde_json::json!({"unknown": true}),
|
||||
serde_json::json!({"custom": {}}),
|
||||
serde_json::json!({"rust": {"command": ["tool", "bad\0argument"]}}),
|
||||
serde_json::json!({"rust": {"extensions": ["bad\nextension"]}}),
|
||||
serde_json::json!({"rust": {"initialization_options": []}}),
|
||||
serde_json::json!({"rust": {"settings": "not-an-object"}}),
|
||||
serde_json::json!({"rust": {"unexpected": true}}),
|
||||
] {
|
||||
frame["lsp"] = value;
|
||||
assert!(
|
||||
serde_json::from_value::<Response>(frame.clone()).is_err(),
|
||||
"{frame}"
|
||||
);
|
||||
}
|
||||
let languages: serde_json::Map<String, serde_json::Value> = (0..17)
|
||||
.map(|index| {
|
||||
(
|
||||
format!("custom-{index}"),
|
||||
serde_json::json!({"command": ["server"], "extensions": ["custom"]}),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
frame["lsp"] = serde_json::Value::Object(languages);
|
||||
assert!(serde_json::from_value::<Response>(frame.clone()).is_err());
|
||||
frame.as_object_mut().unwrap().remove("lsp");
|
||||
assert!(serde_json::from_value::<Response>(frame).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crash_hang_callback_failure_and_fresh_host_recovery() {
|
||||
let fixture = Fixture::new();
|
||||
|
||||
+19
-1
@@ -81,8 +81,11 @@ module Vamp
|
||||
Mode.private_class_method :new
|
||||
|
||||
class Config
|
||||
attr_reader :lsp
|
||||
|
||||
def initialize
|
||||
@plugins = []
|
||||
@lsp = {}
|
||||
@bindings = []
|
||||
@leader = "space"
|
||||
@file_explorer_side = "right"
|
||||
@@ -159,8 +162,23 @@ module Vamp
|
||||
end
|
||||
|
||||
def response
|
||||
lsp = @lsp.to_h do |language, value|
|
||||
language = Vamp.string!(language, "LSP language key")
|
||||
if language.empty? || language.match?(/[[:cntrl:]]/)
|
||||
raise ArgumentError, "LSP language keys must not be empty or contain control characters"
|
||||
end
|
||||
value = case value
|
||||
when TrueClass, FalseClass
|
||||
value
|
||||
when Hash
|
||||
Vamp.json_value!(value)
|
||||
else
|
||||
raise TypeError, "LSP language #{language.inspect} must be true, false, or a Hash"
|
||||
end
|
||||
[language, value]
|
||||
end
|
||||
{ "version" => 1, "type" => "config", "plugins" => @plugins,
|
||||
"bindings" => @bindings, "leader" => @leader,
|
||||
"lsp" => lsp, "bindings" => @bindings, "leader" => @leader,
|
||||
"file_explorer_side" => @file_explorer_side,
|
||||
"file_picker_view" => @file_picker_view,
|
||||
"file_explorer_view" => @file_explorer_view,
|
||||
|
||||
+55
-3
@@ -5,7 +5,8 @@ use std::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
editor::{Editor, Effect, Input, Invocation},
|
||||
editor::{Cursor, Editor, Effect, Input, Invocation, Mode},
|
||||
lsp::Lsp,
|
||||
plugin::{
|
||||
DocumentEvent, DocumentEventKind, DocumentSnapshot, PluginEvent, PluginUpdate,
|
||||
TextPosition, UpdateEffect,
|
||||
@@ -37,6 +38,9 @@ pub struct EditorSession {
|
||||
plugins: BTreeMap<String, RubyPlugin>,
|
||||
documents: BTreeMap<usize, TrackedDocument>,
|
||||
requests: BTreeMap<String, AsyncRequests>,
|
||||
lsp: Lsp,
|
||||
lsp_request_id: u64,
|
||||
lsp_source_cursor: Option<Cursor>,
|
||||
}
|
||||
|
||||
impl EditorSession {
|
||||
@@ -117,6 +121,9 @@ impl EditorSession {
|
||||
plugins,
|
||||
documents: BTreeMap::new(),
|
||||
requests: BTreeMap::new(),
|
||||
lsp: Lsp::new(config.lsp),
|
||||
lsp_request_id: 0,
|
||||
lsp_source_cursor: None,
|
||||
};
|
||||
session.synchronize_documents();
|
||||
Ok(session)
|
||||
@@ -145,6 +152,31 @@ impl EditorSession {
|
||||
}
|
||||
|
||||
fn dispatch(&mut self, invocation: Invocation) {
|
||||
if invocation.plugin == "core" {
|
||||
let result = match invocation.command.as_str() {
|
||||
"lsp_definition" => {
|
||||
if let Some(id) = self.lsp_request_id.checked_add(1) {
|
||||
self.lsp_request_id = id;
|
||||
let context = self.editor.context();
|
||||
let source = InvocationSource {
|
||||
buffer_id: context.buffer_id,
|
||||
revision: context.revision,
|
||||
};
|
||||
self.requests.entry("core".into()).or_default().latest = Some((id, source));
|
||||
self.lsp_source_cursor = Some(self.editor.cursor());
|
||||
self.lsp.definition(&context, id)
|
||||
} else {
|
||||
Err("LSP request IDs exhausted".into())
|
||||
}
|
||||
}
|
||||
"lsp_diagnostics" => self.lsp.diagnostics(self.editor.buffer_id()),
|
||||
_ => unreachable!("session-dispatched core command"),
|
||||
};
|
||||
if let Err(error) = result {
|
||||
self.editor.set_message(format!("LSP: {error}"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
let context = self.editor.context();
|
||||
let result = match self.plugins.get_mut(&invocation.plugin) {
|
||||
Some(plugin) if plugin.is_async() => {
|
||||
@@ -178,7 +210,7 @@ impl EditorSession {
|
||||
}
|
||||
|
||||
fn synchronize_documents(&mut self) -> bool {
|
||||
if !self.plugins.values().any(RubyPlugin::is_async) {
|
||||
if !self.lsp.is_enabled() && !self.plugins.values().any(RubyPlugin::is_async) {
|
||||
self.documents.clear();
|
||||
return false;
|
||||
}
|
||||
@@ -250,6 +282,13 @@ impl EditorSession {
|
||||
});
|
||||
}
|
||||
let mut failures = Vec::new();
|
||||
let mut changed = false;
|
||||
for event in &events {
|
||||
if let Err(error) = self.lsp.notify_document(event) {
|
||||
self.editor.set_message(format!("LSP: {error}"));
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
for (owner, plugin) in &mut self.plugins {
|
||||
if !plugin.is_async() {
|
||||
continue;
|
||||
@@ -261,7 +300,7 @@ impl EditorSession {
|
||||
}
|
||||
}
|
||||
}
|
||||
let changed = !failures.is_empty();
|
||||
changed |= !failures.is_empty();
|
||||
for (owner, error) in failures {
|
||||
self.retire(&owner, &error);
|
||||
}
|
||||
@@ -282,6 +321,9 @@ impl EditorSession {
|
||||
) -> bool {
|
||||
self.editor.buffer_id() == source.buffer_id
|
||||
&& self.source_is_current(source)
|
||||
&& (owner != "core"
|
||||
|| (self.lsp_source_cursor == Some(self.editor.cursor())
|
||||
&& self.editor.mode() == Mode::Normal))
|
||||
&& self
|
||||
.requests
|
||||
.get(owner)
|
||||
@@ -406,6 +448,16 @@ impl EditorSession {
|
||||
/// Drains ready plugin work without waiting for command or server responses.
|
||||
pub fn poll(&mut self) -> bool {
|
||||
let mut changed = self.synchronize_documents();
|
||||
for update in self.lsp.poll() {
|
||||
match self.apply_update("core", update) {
|
||||
Ok(applied) => changed |= applied,
|
||||
Err(error) => {
|
||||
self.editor.clear_plugin_diagnostics("core");
|
||||
self.editor.set_message(format!("LSP: {error}"));
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut ready = Vec::new();
|
||||
for (owner, plugin) in &mut self.plugins {
|
||||
if plugin.is_async() {
|
||||
|
||||
Reference in New Issue
Block a user