Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Harper-ls Error-Handling Improvements #349

Merged
merged 5 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 87 additions & 63 deletions harper-ls/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::collections::HashMap;
use std::path::{Component, PathBuf};
use std::sync::Arc;

use anyhow::{anyhow, Result};
use anyhow::{anyhow, Context, Result};
use harper_comments::CommentParser;
use harper_core::linting::{LintGroup, Linter};
use harper_core::parsers::{CollapseIdentifiers, IsolateEnglish, Markdown, Parser, PlainEnglish};
Expand Down Expand Up @@ -55,58 +55,70 @@ impl Backend {

/// Rewrites a path to a filename using the same conventions as
/// [Neovim's undo-files](https://neovim.io/doc/user/options.html#'undodir').
fn file_dict_name(url: &Url) -> Option<PathBuf> {
fn file_dict_name(url: &Url) -> anyhow::Result<PathBuf> {
let mut rewritten = String::new();

// We assume all URLs are local files and have a base.
for seg in url.to_file_path().ok()?.components() {
for seg in url
.to_file_path()
.map_err(|_| anyhow!("Unable to convert URL to file path."))?
.components()
{
if !matches!(seg, Component::RootDir) {
rewritten.push_str(&seg.as_os_str().to_string_lossy());
rewritten.push('%');
}
}

Some(rewritten.into())
Ok(rewritten.into())
}

/// Get the location of the file's specific dictionary
async fn get_file_dict_path(&self, url: &Url) -> Option<PathBuf> {
async fn get_file_dict_path(&self, url: &Url) -> anyhow::Result<PathBuf> {
let config = self.config.read().await;

Some(config.file_dict_path.join(Self::file_dict_name(url)?))
Ok(config.file_dict_path.join(Self::file_dict_name(url)?))
}

/// Load a speCific file's dictionary
async fn load_file_dictionary(&self, url: &Url) -> Option<FullDictionary> {
match load_dict(self.get_file_dict_path(url).await?).await {
Ok(dict) => Some(dict),
Err(_err) => Some(FullDictionary::new()),
}
/// Load a specific file's dictionary
async fn load_file_dictionary(&self, url: &Url) -> anyhow::Result<FullDictionary> {
let path = self
.get_file_dict_path(url)
.await
.context("Unable to get the file path.")?;

load_dict(path)
.await
.map_err(|err| info!("{err}"))
.or(Ok(FullDictionary::new()))
}

async fn save_file_dictionary(&self, url: &Url, dict: impl Dictionary) -> Result<()> {
Ok(save_dict(
save_dict(
self.get_file_dict_path(url)
.await
.ok_or(anyhow!("Could not compute dictionary path."))?,
.context("Unable to get the file path.")?,
dict,
)
.await?)
.await
.context("Unable to save the dictionary to path.")
}

async fn load_user_dictionary(&self) -> FullDictionary {
let config = self.config.read().await;

match load_dict(&config.user_dict_path).await {
Ok(dict) => dict,
Err(_err) => FullDictionary::new(),
}
load_dict(&config.user_dict_path)
.await
.map_err(|err| info!("{err}"))
.unwrap_or(FullDictionary::new())
}

async fn save_user_dictionary(&self, dict: impl Dictionary) -> Result<()> {
let config = self.config.read().await;

Ok(save_dict(&config.user_dict_path, dict).await?)
save_dict(&config.user_dict_path, dict)
.await
.map_err(|err| anyhow!("Unable to save the dictionary to file: {err}"))
}

async fn generate_global_dictionary(&self) -> Result<MergedDictionary> {
Expand All @@ -123,29 +135,22 @@ impl Backend {
self.load_file_dictionary(url)
);

let Some(file_dictionary) = file_dictionary else {
return Err(anyhow!("Unable to compute dictionary path for {url}."));
};

let mut global_dictionary = global_dictionary?;
global_dictionary.add_dictionary(Arc::new(file_dictionary));
let mut global_dictionary =
global_dictionary.context("Unable to load the global dictionary.")?;
global_dictionary.add_dictionary(Arc::new(
file_dictionary.context("Unable to load the file dictionary.")?,
));

Ok(global_dictionary)
}

async fn update_document_from_file(&self, url: &Url, language_id: Option<&str>) -> Result<()> {
let content = match tokio::fs::read_to_string(
let content = tokio::fs::read_to_string(
url.to_file_path()
.map_err(|_| anyhow::format_err!("Could not extract file path."))?,
.map_err(|_| anyhow!("Unable to convert URL to file path."))?,
)
.await
{
Ok(content) => content,
Err(err) => {
error!("Error updating document from file: {}", err);
return Ok(());
}
};
.with_context(|| format!("Unable to read from file {:?}", url))?;

self.update_document(url, &content, language_id).await
}
Expand All @@ -161,7 +166,11 @@ impl Backend {
let mut doc_lock = self.doc_state.lock().await;
let config_lock = self.config.read().await;

let dict = Arc::new(self.generate_file_dictionary(url).await?);
let dict = Arc::new(
self.generate_file_dictionary(url)
.await
.context("Unable to generate the file dictionary.")?,
);

let doc_state = doc_lock.entry(url.clone()).or_insert(DocumentState {
linter: LintGroup::new(config_lock.lint_config, dict.clone()),
Expand Down Expand Up @@ -345,15 +354,7 @@ impl Backend {
/// Update the configuration of the server and publish document updates that
/// match it.
async fn update_config_from_obj(&self, json_obj: Value) {
let new_config = match Config::from_lsp_config(json_obj) {
Ok(new_config) => new_config,
Err(err) => {
error!("Unable to change config: {}", err);
return;
}
};

{
if let Ok(new_config) = Config::from_lsp_config(json_obj).map_err(|err| error!("{err}")) {
let mut config = self.config.write().await;
*config = new_config;
}
Expand Down Expand Up @@ -434,21 +435,23 @@ impl LanguageServer for Backend {
}

async fn did_save(&self, params: DidSaveTextDocumentParams) {
let _ = self
.update_document_from_file(&params.text_document.uri, None)
.await;
self.update_document_from_file(&params.text_document.uri, None)
.await
.map_err(|err| error!("{err}"))
.err();

self.publish_diagnostics(&params.text_document.uri).await;
}

async fn did_open(&self, params: DidOpenTextDocumentParams) {
let _ = self
.update_document(
&params.text_document.uri,
&params.text_document.text,
Some(&params.text_document.language_id),
)
.await;
self.update_document(
&params.text_document.uri,
&params.text_document.text,
Some(&params.text_document.language_id),
)
.await
.map_err(|err| error!("{err}"))
.err();

self.publish_diagnostics(&params.text_document.uri).await;
}
Expand Down Expand Up @@ -526,8 +529,14 @@ impl LanguageServer for Backend {

let mut dict = self.load_user_dictionary().await;
dict.append_word(word, WordMetadata::default());
let _ = self.save_user_dictionary(dict).await;
let _ = self.update_document_from_file(&file_url, None).await;
self.save_user_dictionary(dict)
.await
.map_err(|err| error!("{err}"))
.err();
self.update_document_from_file(&file_url, None)
.await
.map_err(|err| error!("{err}"))
.err();
self.publish_diagnostics(&file_url).await;
}
"HarperAddToFileDict" => {
Expand All @@ -539,14 +548,26 @@ impl LanguageServer for Backend {

let file_url = second.parse().unwrap();

let Some(mut dict) = self.load_file_dictionary(&file_url).await else {
error!("Unable resolve dictionary path: {file_url}");
return Ok(None);
let mut dict = match self
.load_file_dictionary(&file_url)
.await
.map_err(|err| error!("{err}"))
{
Ok(dict) => dict,
Err(_) => {
return Ok(None);
}
};
dict.append_word(word, WordMetadata::default());

let _ = self.save_file_dictionary(&file_url, dict).await;
let _ = self.update_document_from_file(&file_url, None).await;
self.save_file_dictionary(&file_url, dict)
.await
.map_err(|err| error!("{err}"))
.err();
self.update_document_from_file(&file_url, None)
.await
.map_err(|err| error!("{err}"))
.err();
self.publish_diagnostics(&file_url).await;
}
"HarperOpen" => match open::that(&first) {
Expand Down Expand Up @@ -585,7 +606,10 @@ impl LanguageServer for Backend {
};

for url in urls {
let _ = self.update_document_from_file(&url, None).await;
self.update_document_from_file(&url, None)
.await
.map_err(|err| error!("{err}"))
.err();
self.publish_diagnostics(&url).await;
}
}
Expand Down
25 changes: 10 additions & 15 deletions harper-ls/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::path::PathBuf;

use anyhow::{bail, Result};
use dirs::{config_dir, data_local_dir};
use harper_core::linting::LintGroupConfig;
use resolve_path::PathResolveExt;
Expand Down Expand Up @@ -42,18 +43,16 @@ pub struct CodeActionConfig {
}

impl CodeActionConfig {
pub fn from_lsp_config(value: Value) -> anyhow::Result<Self> {
pub fn from_lsp_config(value: Value) -> Result<Self> {
let mut base = CodeActionConfig::default();

let Value::Object(value) = value else {
return Err(anyhow::format_err!(
"The code action configuration must be an object."
));
bail!("The code action configuration must be an object.");
};

if let Some(force_stable_val) = value.get("forceStable") {
let Value::Bool(force_stable) = force_stable_val else {
return Err(anyhow::format_err!("forceStable must be a boolean value."));
bail!("forceStable must be a boolean value.");
};
base.force_stable = *force_stable;
};
Expand All @@ -73,32 +72,30 @@ pub struct Config {
}

impl Config {
pub fn from_lsp_config(value: Value) -> anyhow::Result<Self> {
pub fn from_lsp_config(value: Value) -> Result<Self> {
let mut base = Config::default();

let Value::Object(value) = value else {
return Err(anyhow::format_err!("Settings must be an object."));
bail!("Settings must be an object.");
};

let Some(Value::Object(value)) = value.get("harper-ls") else {
return Err(anyhow::format_err!(
"Settings must contain a \"harper-ls\" key."
));
bail!("Settings must contain a \"harper-ls\" key.");
grantlemons marked this conversation as resolved.
Show resolved Hide resolved
};

if let Some(v) = value.get("userDictPath") {
if let Value::String(path) = v {
base.user_dict_path = path.try_resolve()?.to_path_buf();
} else {
return Err(anyhow::format_err!("userDict path must be a string."));
bail!("userDict path must be a string.");
}
}

if let Some(v) = value.get("fileDictPath") {
if let Value::String(path) = v {
base.file_dict_path = path.try_resolve()?.to_path_buf();
} else {
return Err(anyhow::format_err!("fileDict path must be a string."));
bail!("fileDict path must be a string.");
}
}

Expand All @@ -118,9 +115,7 @@ impl Config {
if let Value::Bool(v) = v {
base.isolate_english = *v;
} else {
return Err(anyhow::format_err!(
"isolateEnglish path must be a boolean."
));
bail!("isolateEnglish path must be a boolean.");
}
}

Expand Down
11 changes: 5 additions & 6 deletions harper-ls/src/dictionary_io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ use std::path::Path;

use harper_core::{Dictionary, FullDictionary, WordMetadata};
use tokio::fs::{self, File};
use tokio::io::{self, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, BufReader, BufWriter};
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, BufReader, BufWriter, Result};

/// Save the contents of a dictionary to a file.
/// Ensures that the path to the destination exists.
pub async fn save_dict(path: impl AsRef<Path>, dict: impl Dictionary) -> io::Result<()> {
pub async fn save_dict(path: impl AsRef<Path>, dict: impl Dictionary) -> Result<()> {
if let Some(parent) = path.as_ref().parent() {
fs::create_dir_all(parent).await?;
}
Expand All @@ -15,13 +15,12 @@ pub async fn save_dict(path: impl AsRef<Path>, dict: impl Dictionary) -> io::Res
let mut write = BufWriter::new(file);

write_word_list(dict, &mut write).await?;

write.flush().await?;

Ok(())
}

async fn write_word_list(dict: impl Dictionary, mut w: impl AsyncWrite + Unpin) -> io::Result<()> {
async fn write_word_list(dict: impl Dictionary, mut w: impl AsyncWrite + Unpin) -> Result<()> {
let mut cur_str = String::new();

for word in dict.words_iter() {
Expand All @@ -35,7 +34,7 @@ async fn write_word_list(dict: impl Dictionary, mut w: impl AsyncWrite + Unpin)
Ok(())
}

pub async fn load_dict(path: impl AsRef<Path>) -> io::Result<FullDictionary> {
pub async fn load_dict(path: impl AsRef<Path>) -> Result<FullDictionary> {
grantlemons marked this conversation as resolved.
Show resolved Hide resolved
let file = File::open(path.as_ref()).await?;
let read = BufReader::new(file);

Expand All @@ -44,7 +43,7 @@ pub async fn load_dict(path: impl AsRef<Path>) -> io::Result<FullDictionary> {

/// This function could definitely be optimized to use less memory.
/// Right now it isn't an issue.
async fn dict_from_word_list(mut r: impl AsyncRead + Unpin) -> io::Result<FullDictionary> {
async fn dict_from_word_list(mut r: impl AsyncRead + Unpin) -> Result<FullDictionary> {
let mut str = String::new();

r.read_to_string(&mut str).await?;
Expand Down
Loading