Skip to content

Commit

Permalink
Handle when path ends with a keyword, and multi-character op tokens
Browse files Browse the repository at this point in the history
  • Loading branch information
minestarks committed Oct 11, 2024
1 parent 7c89f2b commit 96d6dce
Show file tree
Hide file tree
Showing 9 changed files with 247 additions and 38 deletions.
6 changes: 5 additions & 1 deletion compiler/qsc_ast/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1423,10 +1423,14 @@ impl Default for PathKind {
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct IncompletePath {
/// The whole span of the incomplete path,
/// including the final `.` and any whitespace that follows it.
/// including the final `.` and any whitespace or keyword
/// that follows it.
pub span: Span,
/// Any segments that were successfully parsed before the final `.`.
pub segments: Box<[Ident]>,
/// Whether a keyword exists after the final `.`.
/// This keyword can be presumed to be a partially typed identifier.
pub keyword: bool,
}

impl Display for PathKind {
Expand Down
12 changes: 12 additions & 0 deletions compiler/qsc_parse/src/completion/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -365,3 +365,15 @@ fn base_type_tuple() {
"]],
);
}

#[test]
fn keyword_after_incomplete_path() {
check_valid_words(
"import Foo.in|",
&expect![[r"
WordKinds(
PathSegment,
)
"]],
);
}
4 changes: 3 additions & 1 deletion compiler/qsc_parse/src/item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -647,7 +647,9 @@ fn path_import(s: &mut ParserContext) -> Result<(PathKind, bool)> {
match path(s, WordKinds::PathImport) {
Ok(path) => Ok((PathKind::Ok(path), false)),
Err((error, Some(incomplete_path))) => {
if token(s, TokenKind::ClosedBinOp(ClosedBinOp::Star)).is_ok() {
if !incomplete_path.keyword
&& token(s, TokenKind::ClosedBinOp(ClosedBinOp::Star)).is_ok()
{
let (name, namespace) = incomplete_path
.segments
.split_last()
Expand Down
58 changes: 46 additions & 12 deletions compiler/qsc_parse/src/item/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2076,6 +2076,44 @@ fn invalid_glob_syntax_with_following_ident() {
);
}

#[test]
fn invalid_glob_syntax_follows_keyword() {
check(
parse_import_or_export,
"import Foo.in*;",
&expect![[r#"
ImportOrExportDecl [0-13]: [Err IncompletePath [7-13]:
Ident _id_ [7-10] "Foo"]
[
Error(
Rule(
"identifier",
Keyword(
In,
),
Span {
lo: 11,
hi: 13,
},
),
),
Error(
Token(
Semi,
ClosedBinOp(
Star,
),
Span {
lo: 13,
hi: 14,
},
),
),
]"#]],
);
}

#[test]
fn disallow_top_level_recursive_glob() {
check(
Expand Down Expand Up @@ -2158,11 +2196,9 @@ fn missing_semi_between_items() {
Namespace _id_ [0-45] (Ident _id_ [10-13] "Foo"):
Item _id_ [16-24]:
Open (Path _id_ [21-24] (Ident _id_ [21-24] "Foo"))
Item _id_ [25-35]:
Open (Err IncompletePath [30-35]:
Item _id_ [25-39]:
Open (Err IncompletePath [30-39]:
Ident _id_ [30-33] "Bar")
Item _id_ [35-43]:
Open (Path _id_ [40-43] (Ident _id_ [40-43] "Baz"))
[
Error(
Expand Down Expand Up @@ -2192,24 +2228,22 @@ fn missing_semi_between_items() {
Error(
Token(
Semi,
Keyword(
Open,
),
Ident,
Span {
lo: 35,
hi: 39,
lo: 40,
hi: 43,
},
),
),
Error(
Token(
Semi,
Close(
Brace,
),
Ident,
Span {
lo: 44,
hi: 45,
lo: 40,
hi: 43,
},
),
),
Expand Down
8 changes: 8 additions & 0 deletions compiler/qsc_parse/src/prim.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,20 @@ pub(super) fn path(
Ok(ident) => parts.push(*ident),
Err(error) => {
let _ = s.skip_trivia();
let peek = s.peek();
let keyword = matches!(peek.kind, TokenKind::Keyword(_));
if keyword {
// Consume any keyword that comes after the final
// dot, assuming it was intended to be part of the path.
s.advance();
}

return Err((
error,
Some(Box::new(IncompletePath {
span: s.span(lo),
segments: parts.into(),
keyword,
})),
));
}
Expand Down
27 changes: 27 additions & 0 deletions compiler/qsc_parse/src/prim/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,33 @@ fn path_trailing_dot() {
);
}

#[test]
fn path_followed_by_keyword() {
check(
path,
"Foo.Bar.in",
&expect![[r#"
Err IncompletePath [0-10]:
Ident _id_ [0-3] "Foo"
Ident _id_ [4-7] "Bar"
[
Error(
Rule(
"identifier",
Keyword(
In,
),
Span {
lo: 8,
hi: 10,
},
),
),
]"#]],
);
}

#[test]
fn pat_bind() {
check(
Expand Down
4 changes: 3 additions & 1 deletion language_service/src/completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,9 @@ fn collect_paths(
/// `let x : Microsoft.Quantum.Math.↘` should include `Complex` (a type) while
/// `let x = Microsoft.Quantum.Math.↘` should include `PI` (a callable).
fn collect_path_segments(globals: &Globals, path_context: &IncompletePath) -> Vec<Vec<Completion>> {
let (path_kind, qualifier) = path_context.context();
let Some((path_kind, qualifier)) = path_context.context() else {
return Vec::new();
};

match path_kind {
PathKind::Namespace => globals.namespaces_in(&qualifier),
Expand Down
60 changes: 37 additions & 23 deletions language_service/src/completion/path_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
use qsc::{
ast::{
visit::{self, Visitor},
Attr, Block, CallableDecl, Expr, ExprKind, FieldAssign, FieldDef, FunctorExpr, Ident, Item,
ItemKind, Namespace, Package, Pat, Path, QubitInit, SpecDecl, Stmt, StructDecl, Ty, TyDef,
TyKind,
Attr, Block, CallableDecl, Expr, ExprKind, FieldAssign, FieldDef, FunctorExpr, Ident,
Idents, Item, ItemKind, Namespace, Package, Pat, Path, QubitInit, SpecDecl, Stmt,
StructDecl, Ty, TyDef, TyKind,
},
parse::completion::PathKind,
};
Expand All @@ -18,7 +18,7 @@ use std::rc::Rc;
/// Methods may panic if the offset does not fall within an incomplete path.
#[derive(Debug)]
pub(super) struct IncompletePath<'a> {
qualifier: Option<&'a [Ident]>,
qualifier: Option<Vec<&'a Ident>>,
context: Option<PathKind>,
offset: u32,
}
Expand All @@ -41,10 +41,28 @@ impl<'a> IncompletePath<'a> {
}

impl<'a> Visitor<'a> for IncompletePath<'a> {
fn visit_item(&mut self, item: &Item) {
match *item.kind {
fn visit_item(&mut self, item: &'a Item) {
match &*item.kind {
ItemKind::Open(..) => self.context = Some(PathKind::Namespace),
ItemKind::ImportOrExport(..) => self.context = Some(PathKind::Import),
ItemKind::ImportOrExport(decl) => {
self.context = Some(PathKind::Import);
for item in &decl.items {
if item.is_glob
&& item.span.touches(self.offset)
&& item
.alias
.as_ref()
.map_or(true, |a| !a.span.touches(self.offset))
{
// Special case when the cursor falls *between* the
// `Path` and the glob asterisk,
// e.g. `foo.bar.|*` . In that case, the visitor
// will not visit the path since the cursor technically
// is not within the path.
self.visit_path_kind(&item.path);
}
}
}
_ => {}
}
}
Expand All @@ -65,35 +83,31 @@ impl<'a> Visitor<'a> for IncompletePath<'a> {

fn visit_path_kind(&mut self, path: &'a qsc::ast::PathKind) {
self.qualifier = match path {
qsc::ast::PathKind::Ok(path) => path.segments.as_ref().map(AsRef::as_ref),
qsc::ast::PathKind::Err(Some(incomplete_path)) => Some(&incomplete_path.segments),
qsc::ast::PathKind::Ok(path) => Some(path.iter().collect()),
qsc::ast::PathKind::Err(Some(incomplete_path)) => {
Some(incomplete_path.segments.iter().collect())
}
qsc::ast::PathKind::Err(None) => None,
};
}
}

impl IncompletePath<'_> {
pub fn context(&self) -> (PathKind, Vec<Rc<str>>) {
pub fn context(&self) -> Option<(PathKind, Vec<Rc<str>>)> {
let context = self.context?;
let qualifier = self.segments_before_offset();

// WARNING: this assumption appears to hold true today, but it's subtle
// enough that parser and AST changes can easily violate it in the future.
assert!(
!qualifier.is_empty(),
"path segment completion should only be invoked for a partially parsed path"
);

let context = self
.context
.expect("context must exist for path segment completion");
if qualifier.is_empty() {
return None;
}

(context, qualifier)
Some((context, qualifier))
}

fn segments_before_offset(&self) -> Vec<Rc<str>> {
self.qualifier
.into_iter()
.flat_map(AsRef::as_ref)
.iter()
.flatten()
.take_while(|i| i.span.hi < self.offset)
.map(|i| i.name.clone())
.collect::<Vec<_>>()
Expand Down
Loading

0 comments on commit 96d6dce

Please sign in to comment.