Skip to content

Commit

Permalink
refactor(server): change analysis to be per-rule
Browse files Browse the repository at this point in the history
feat(server): add rule inlining code action (#12)
feat: add a test cases file with instructions for testing the server
 feat(server): made the parsing code more robust
  • Loading branch information
Jamalam360 committed Mar 31, 2023
1 parent b2cb50c commit fa2d7bb
Show file tree
Hide file tree
Showing 6 changed files with 210 additions and 76 deletions.
13 changes: 13 additions & 0 deletions TEST_CASES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Test Cases

These are manual tests that should be performed to ensure the server is working correctly.

- Check error reporting works as expected.
- Check autocompletion works as expected.
- Check formatting works as expected.
- Check that rules with documentation show that documentation on hover.
- Check that builtins show documentation on hover.
- Check that the unused rule diagnostic works, with and without the `pestIdeTools.alwaysUsedRuleNames` configuration.
- Check go to definition and find references works correctly.
- Check that renaming rules works as expected.

86 changes: 59 additions & 27 deletions language-server/src/analysis.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use pest::iterators::Pairs;
use pest::{iterators::Pairs, Span};
use pest_meta::parser::Rule;
use reqwest::Url;
use std::collections::HashMap;
Expand All @@ -9,49 +9,60 @@ use crate::{
helpers::{FindOccurrences, IntoLocation},
};

#[derive(Debug, Clone)]
/// Stores analysis information for a rule.
pub struct RuleAnalysis {
/// The location of the entire definition of the rule (i.e. `rule = { "hello" }`).
pub definition_location: Location,
/// The location of the name definition of the rule.
pub identifier_location: Location,
/// The inner body of the rule (the bit inside the curly braces).
pub expression: String,
/// The occurrences of the rule, including its definition.
pub occurrences: Vec<Location>,
/// The rules documentation, in markdown.
pub doc: String,
}

#[derive(Debug)]
/// Stores analysis information for a document.
pub struct Analysis {
/// The URL of the document that this analysis is for.
pub doc_url: Url,
/// Maps rule names to their locations in the document. If the rule is a builtin, the location
/// will be [None].
pub rule_names: HashMap<String, Option<Location>>,
/// Maps rule names to all of their occurrences in the document, including their definition.
pub rule_occurrences: HashMap<String, Vec<Location>>,
/// Maps rule names to their documentation, in Markdown.
pub rule_docs: HashMap<String, String>,
/// Holds analyses for individual rules.
/// [RuleAnalysis] is [None] for builtins.
pub rules: HashMap<String, Option<RuleAnalysis>>,
}

impl Analysis {
/// Updates this analysis from the given pairs.
pub fn update_from(&mut self, pairs: Pairs<Rule>) {
self.rule_names = HashMap::new();
self.rule_docs = HashMap::new();
self.rule_occurrences = HashMap::new();
self.rules = HashMap::new();

for builtin in BUILTINS.iter() {
self.rule_names.insert(builtin.to_string(), None);
self.rules.insert(builtin.to_string(), None);
}

let mut preceding_docs = Vec::new();
let mut current_span: Option<Span>;

for pair in pairs.clone() {
if pair.as_rule() == Rule::grammar_rule {
let inner = pair.into_inner().next().unwrap();
current_span = Some(pair.as_span());
let mut inner_pairs = pair.into_inner();
let inner = inner_pairs.next().unwrap();

match inner.as_rule() {
Rule::line_doc => {
preceding_docs.push(inner.into_inner().next().unwrap().as_str());
}
Rule::identifier => {
self.rule_names.insert(
inner.as_str().to_owned(),
Some(inner.as_span().into_location(&self.doc_url)),
);
self.rule_occurrences.insert(
inner.as_str().to_owned(),
pairs.find_occurrences(&self.doc_url, inner.as_str()),
);
let expression = inner_pairs.find(|r| r.as_rule() == Rule::expression)
.expect("rule should contain expression")
.as_str()
.to_owned();
let occurrences = pairs.find_occurrences(&self.doc_url, inner.as_str());
let mut docs = None;

if !preceding_docs.is_empty() {
let mut buf = String::new();
Expand All @@ -63,9 +74,22 @@ impl Analysis {
buf.push_str(preceding_docs.join("\n- ").as_str());
}

self.rule_docs.insert(inner.as_str().to_owned(), buf);
docs = Some(buf);
preceding_docs.clear();
}

self.rules.insert(
inner.as_str().to_owned(),
Some(RuleAnalysis {
identifier_location: inner.as_span().into_location(&self.doc_url),
definition_location: current_span
.expect("rule should have a defined span")
.into_location(&self.doc_url),
expression,
occurrences,
doc: docs.unwrap_or_else(|| "".to_owned()),
}),
);
}
_ => {}
}
Expand All @@ -74,14 +98,22 @@ impl Analysis {
}

pub fn get_unused_rules(&self) -> Vec<(&String, &Location)> {
self.rule_occurrences
self.rules
.iter()
.filter(|(_, occurrences)| occurrences.len() == 1)
.filter(|(name, _)| !BUILTINS.iter().filter(|n| n == name).any(|_| true))
.map(|(name, occurrences)| {
.filter(|(_, ra)| {
if let Some(ra) = ra {
ra.occurrences.len() == 1
} else {
false
}
})
.filter(|(name, _) | {
!BUILTINS.contains(&name.as_str())
})
.map(|(name, ra)| {
(
name,
occurrences.first().unwrap_or_else(|| {
ra.as_ref().unwrap().occurrences.first().unwrap_or_else(|| {
panic!("Expected at least one occurrence for rule {}", name)
}),
)
Expand Down
16 changes: 14 additions & 2 deletions language-server/src/capabilities.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use tower_lsp::lsp_types::{
CompletionOptions, FileOperationFilter, FileOperationPattern, FileOperationRegistrationOptions,
CodeActionKind, CodeActionOptions, CodeActionProviderCapability, CompletionOptions,
FileOperationFilter, FileOperationPattern, FileOperationRegistrationOptions,
HoverProviderCapability, InitializeResult, OneOf, ServerCapabilities, ServerInfo,
TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions,
WorkspaceFileOperationsServerCapabilities, WorkspaceServerCapabilities,
WorkDoneProgressOptions, WorkspaceFileOperationsServerCapabilities,
WorkspaceServerCapabilities,
};

/// Returns the capabilities of the language server.
Expand All @@ -21,6 +23,16 @@ pub fn capabilities() -> InitializeResult {
trigger_characters: Some(vec!["{".to_string(), "~".to_string(), "|".to_string()]),
..Default::default()
}),
code_action_provider: Some(CodeActionProviderCapability::Options(CodeActionOptions {
code_action_kinds: Some(vec![
CodeActionKind::REFACTOR_EXTRACT,
CodeActionKind::REFACTOR_INLINE,
]),
work_done_progress_options: WorkDoneProgressOptions::default(),
resolve_provider: None,
//FIXME(Jamalam): Use Default here once https://github.com/gluon-lang/lsp-types/issues/260 is resolved.
// ..Default::default()
})),
definition_provider: Some(OneOf::Left(true)),
references_provider: Some(OneOf::Left(true)),
document_formatting_provider: Some(OneOf::Left(true)),
Expand Down
Loading

0 comments on commit fa2d7bb

Please sign in to comment.