Skip to content

Commit

Permalink
Add Hover Descriptions to Attributes (#2052)
Browse files Browse the repository at this point in the history
Adds hover descriptions to built-in attributes in the Q# language
server.

Closes #1994

---------

Co-authored-by: Stefan J. Wernli <swernli@microsoft.com>
  • Loading branch information
ScottCarda-MS and swernli authored Dec 6, 2024
1 parent 645243b commit 03985be
Show file tree
Hide file tree
Showing 8 changed files with 105 additions and 9 deletions.
4 changes: 3 additions & 1 deletion compiler/qsc_frontend/src/lower.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ use self::convert::TyConversionError;
#[derive(Clone, Debug, Diagnostic, Error)]
pub(super) enum Error {
#[error("unknown attribute {0}")]
#[diagnostic(help("supported attributes are: EntryPoint, Config"))]
#[diagnostic(help(
"supported attributes are: EntryPoint, Config, SimulatableIntrinsic, Measurement, Reset"
))]
#[diagnostic(code("Qsc.LowerAst.UnknownAttr"))]
UnknownAttr(String, #[label] Span),
#[error("invalid attribute arguments: expected {0}")]
Expand Down
31 changes: 25 additions & 6 deletions compiler/qsc_hir/src/hir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1344,23 +1344,42 @@ impl Display for Ident {
/// An attribute.
#[derive(Clone, Debug, PartialEq)]
pub enum Attr {
/// Provide pre-processing information about when an item should be included in compilation.
/// Provides pre-processing information about when an item should be included in compilation.
Config,
/// Indicates that a callable is an entry point to a program.
/// Indicates that the callable is the entry point to a program.
EntryPoint,
/// Indicates that an item does not have an implementation available for use.
/// Indicates that an item is not yet implemented.
Unimplemented,
/// Indicates that an item should be treated as an intrinsic callable for QIR code generation
/// and any implementation should be ignored.
/// and any implementation should only be used during simulation.
SimulatableIntrinsic,
/// Indicates that a callable is a measurement. This means that the operation will be marked as
/// Indicates that an intrinsic callable is a measurement. This means that the operation will be marked as
/// "irreversible" in the generated QIR, and output Result types will be moved to the arguments.
Measurement,
/// Indicates that a callable is a reset. This means that the operation will be marked as
/// Indicates that an intrinsic callable is a reset. This means that the operation will be marked as
/// "irreversible" in the generated QIR.
Reset,
}

impl Attr {
/// Gets the string description of the attribute.
#[must_use]
pub fn description(&self) -> &'static str {
match self {
Attr::Config => "Provides pre-processing information about when an item should be included in compilation.
Valid arguments are `Base`, `Adaptive`, `IntegerComputations`, `FloatingPointComputations`, `BackwardsBranching`, `HigherLevelConstructs`, `QubitReset`, and `Unrestricted`.
The `not` operator is also supported to negate the attribute, e.g. `not Adaptive`.",
Attr::EntryPoint => "Indicates that the callable is the entry point to a program.",
Attr::Unimplemented => "Indicates that an item is not yet implemented.",
Attr::SimulatableIntrinsic => "Indicates that an item should be treated as an intrinsic callable for QIR code generation and any implementation should only be used during simulation.",
Attr::Measurement => "Indicates that an intrinsic callable is a measurement. This means that the operation will be marked as \"irreversible\" in the generated QIR, and output Result types will be moved to the arguments.",
Attr::Reset => "Indicates that an intrinsic callable is a reset. This means that the operation will be marked as \"irreversible\" in the generated QIR.",
}
}
}

impl FromStr for Attr {
type Err = ();

Expand Down
4 changes: 4 additions & 0 deletions language_service/src/definition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ struct DefinitionFinder<'a> {
}

impl<'a> Handler<'a> for DefinitionFinder<'a> {
fn at_attr_ref(&mut self, _: &'a ast::Ident) {
// We don't support goto def for attributes.
}

fn at_callable_def(
&mut self,
_: &LocatorContext<'a>,
Expand Down
15 changes: 15 additions & 0 deletions language_service/src/hover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ use crate::protocol::Hover;
use crate::qsc_utils::into_range;
use qsc::ast::visit::Visitor;
use qsc::display::{parse_doc_for_param, parse_doc_for_summary, CodeDisplay, Lookup};
use qsc::hir::Attr;
use qsc::line_column::{Encoding, Position, Range};
use qsc::{ast, hir, Span};
use std::fmt::Display;
use std::rc::Rc;
use std::str::FromStr;

pub(crate) fn get_hover(
compilation: &Compilation,
Expand Down Expand Up @@ -43,6 +45,7 @@ enum LocalKind {
LambdaParam,
Local,
}

struct HoverGenerator<'a> {
position_encoding: Encoding,
hover: Option<Hover>,
Expand All @@ -51,6 +54,18 @@ struct HoverGenerator<'a> {
}

impl<'a> Handler<'a> for HoverGenerator<'a> {
fn at_attr_ref(&mut self, name: &'a ast::Ident) {
let description = match Attr::from_str(&name.name) {
Ok(attr) => attr.description(),
Err(()) => return, // No hover information for unsupported attributes.
};

self.hover = Some(Hover {
contents: format!("attribute ```{}```\n\n{}", name.name, description),
span: self.range(name.span),
});
}

fn at_callable_def(
&mut self,
context: &LocatorContext<'a>,
Expand Down
36 changes: 36 additions & 0 deletions language_service/src/hover/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,42 @@ fn check_notebook_none(cells_with_markers: &[(&str, &str)]) {
assert!(actual.is_none());
}

#[test]
fn attr() {
check(
indoc! {r#"
namespace Test {
@◉Entr↘yPoint◉()
operation Bar() : Unit {}
}
"#},
&expect![[r#"
attribute ```EntryPoint```
Indicates that the callable is the entry point to a program."#]],
);
}

#[test]
fn attr_with_arg() {
check(
indoc! {r#"
namespace Test {
@◉Con↘fig◉(BackwardsBranching)
operation Bar() : Unit {}
}
"#},
&expect![[r#"
attribute ```Config```
Provides pre-processing information about when an item should be included in compilation.
Valid arguments are `Base`, `Adaptive`, `IntegerComputations`, `FloatingPointComputations`, `BackwardsBranching`, `HigherLevelConstructs`, `QubitReset`, and `Unrestricted`.
The `not` operator is also supported to negate the attribute, e.g. `not Adaptive`."#]],
);
}

#[test]
fn callable_unit_types() {
check(
Expand Down
16 changes: 14 additions & 2 deletions language_service/src/name_locator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,23 @@ use std::mem::replace;
use std::rc::Rc;

use crate::compilation::Compilation;
use qsc::ast::visit::{walk_expr, walk_namespace, walk_pat, walk_ty, walk_ty_def, Visitor};
use qsc::ast::visit::{
walk_attr, walk_expr, walk_namespace, walk_pat, walk_ty, walk_ty_def, Visitor,
};
use qsc::ast::{FieldAccess, Idents, PathKind};
use qsc::display::Lookup;
use qsc::{ast, hir, resolve};

#[allow(unused_variables)]
pub(crate) trait Handler<'package> {
fn at_attr_ref(&mut self, name: &'package ast::Ident);

fn at_callable_def(
&mut self,
context: &LocatorContext<'package>,
name: &'package ast::Ident,
decl: &'package ast::CallableDecl,
);

fn at_callable_ref(
&mut self,
path: &'package ast::Path,
Expand Down Expand Up @@ -166,6 +170,13 @@ impl<'inner, 'package, T: Handler<'package>> Locator<'inner, 'package, T> {
}

impl<'inner, 'package, T: Handler<'package>> Visitor<'package> for Locator<'inner, 'package, T> {
fn visit_attr(&mut self, attr: &'package ast::Attr) {
if attr.name.span.contains(self.offset) {
self.inner.at_attr_ref(&attr.name);
}
walk_attr(self, attr);
}

fn visit_namespace(&mut self, namespace: &'package ast::Namespace) {
if namespace.span.contains(self.offset) {
self.context.current_namespace = namespace.name.full_name();
Expand All @@ -177,6 +188,7 @@ impl<'inner, 'package, T: Handler<'package>> Visitor<'package> for Locator<'inne
fn visit_item(&mut self, item: &'package ast::Item) {
if item.span.contains(self.offset) {
let context = replace(&mut self.context.current_item_doc, item.doc.clone());
item.attrs.iter().for_each(|a| self.visit_attr(a));
match &*item.kind {
ast::ItemKind::Callable(decl) => {
if decl.name.span.touches(self.offset) {
Expand Down
4 changes: 4 additions & 0 deletions language_service/src/references.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ struct NameHandler<'a> {
}

impl<'a> Handler<'a> for NameHandler<'a> {
fn at_attr_ref(&mut self, _: &'a ast::Ident) {
// We don't support find all refs for attributes.
}

fn at_callable_def(
&mut self,
_: &LocatorContext<'a>,
Expand Down
4 changes: 4 additions & 0 deletions language_service/src/rename.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ impl<'a> Rename<'a> {
}

impl<'a> Handler<'a> for Rename<'a> {
fn at_attr_ref(&mut self, _: &'a ast::Ident) {
// We don't support renaming attributes.
}

fn at_callable_def(
&mut self,
_: &LocatorContext<'a>,
Expand Down

0 comments on commit 03985be

Please sign in to comment.