-
Notifications
You must be signed in to change notification settings - Fork 86
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
More precise completions and namespace member completions #1947
base: main
Are you sure you want to change the base?
Conversation
Change in memory usage detected by benchmark. Memory Report for 6fbbec5
|
} | ||
} | ||
|
||
impl ImportOrExportItem { | ||
/// Returns the span of the export item. This includes the path and , if any exists, the alias. | ||
/// Returns the alias ident, if any, or the name from the path if no alias is present. | ||
/// Returns `None` if this the path has an error. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick: extra word
/// Returns `None` if this the path has an error. | |
/// Returns `None` if the path has an error. |
I ran some tests to narrow it down. This increase is attributable entirely to the changes in the AST. A bunch of I can't think of a way to shrink the AST any further, we are storing more information in there after all. So I think the 0.04% increase in memory usage is a worthwhile tradeoff for the feature. |
language_service/src/tests.rs
Outdated
expect![[r#" | ||
334 | ||
"#]] | ||
.assert_debug_eq( | ||
&ls.get_completions( | ||
"foo.qs", | ||
Position { | ||
line: 0, | ||
column: 76 | ||
} | ||
column: 92, | ||
}, | ||
) | ||
.items | ||
.len(), | ||
13 | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it aid debugging test failures to compare the lists instead of the number of items in the lists?
@@ -310,7 +304,7 @@ function registerMonacoLanguageServiceProviders( | |||
}), | |||
}; | |||
}, | |||
triggerCharacters: ["@"], // for attribute completions | |||
triggerCharacters: ["@", "."], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we add a note that we need to keep this list in sync with the one in extension.ts
?
return results.concat(this.samples); | ||
|
||
// Include the samples list for empty documents | ||
return document.lineCount === 0 ? results.concat(this.samples) : results; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it make sense to make the expected case the first, with fallback after?
return document.lineCount > 0 ? results : results.concat(this.samples);
target::Profile, | ||
LanguageFeatures, PackageStore, PackageType, SourceMap, Span, | ||
}; | ||
use qsc_project::{PackageGraphSources, PackageInfo}; | ||
use rustc_hash::FxHashMap; | ||
|
||
const FAKE_STDLIB_CONTENTS: &str = r#" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having std lib in the name is a little consfusing to me. Is this just some lib, or is it supposed to be an alternative std lib?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it's supposed to replace the stdlib in most language service tests, since it's more minimal (faster to build) and I can easily add any edge case I want to cover.
if let ast::PathKind::Ok(path) = self.path { | ||
write!(f, "{}", path.full_name()) | ||
} else { | ||
write!(f, "?") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we delegate this Display
impl to IncompletePath
which can render the segments with a ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this would work in this case. The AST structs' Display
impls all print in that "AST-ish" format, like:
Err IncompletePath [157-166]:
Ident 13 [157-160] "Foo"
Ident 14 [161-164] "Bar"
like what you see in the "AST" tab in the playground. These are not meant to be used when pretty printing to the user. (This line actually had a bug where it was exposing the AST-ish format to the user, which I fixed.
We've had to differentiate these impls from the user visible "pretty printing" impls, which live here. That's why there are a bunch of structs in this file that wrap the AST/HIR structs only to implement Display
.
line: 2, | ||
column: 18, | ||
}, | ||
end: Position { | ||
line: 1, | ||
column: 30, | ||
line: 2, | ||
column: 22, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are the lines and columns changing in these tests?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because of my changes in test_utils.rs
- I think I changed some whitespace in the "Fake stdlib" while moving strings around.
fn visit_path_kind(&mut self, path: &'a qsc::ast::PathKind) { | ||
let span = match path { | ||
qsc::ast::PathKind::Ok(path) => &path.span, | ||
qsc::ast::PathKind::Err(Some(incomplete_path)) => &incomplete_path.span, | ||
qsc::ast::PathKind::Err(None) => return, | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: clean up with use to match rest of file
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::Err(None) => None, | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: clean up with use to match rest of file
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sad story: there are two different structs named PathKind
in this file, and the qualifiers here are to disambiguate. Maybe I'll use an alias.
pub fn did_advance(&mut self, next_token: &mut Token, scanner_offset: u32) { | ||
match self.state { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be advance
? Being did_advance
I was looking for what bool
it was going to return but was ()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is semantically an "event handler", as in, it's informing the callee that advancing did occur.
I got used to this naming convention in LSP / VS Code APIS (e.g. didDeleteFile
) but I guess it's not coming across :D
Any other suggestions? on_advance()
? on_advanced()
? advanced()
?
let source_name_relative = compilation | ||
.user_unit() | ||
.sources | ||
.find_by_offset(package_offset) | ||
.expect("source should exist in the user source map"); | ||
let source_offset = (package_offset - source.offset) | ||
.try_into() | ||
.expect("offset can't be converted to uszie"); | ||
let before_offset = &source.contents[..source_offset]; | ||
let mut indent = match before_offset.rfind(|c| c == '{' || c == '\n') { | ||
Some(begin) => { | ||
let indent = &before_offset[begin..]; | ||
indent.strip_prefix('{').unwrap_or(indent) | ||
} | ||
None => before_offset, | ||
} | ||
.to_string(); | ||
if !indent.starts_with('\n') { | ||
indent.insert(0, '\n'); | ||
.relative_sources() | ||
.find(|s| s.offset == source.offset) | ||
.expect("source should exist in the user source map") | ||
.name; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it make sense to expose something like as_relative_source(source: Source)
rather than iterating and cloning all of the sources in the map?
let source_name_relative = compilation
.user_unit()
.sources
.as_relative_source(source).name
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I like that idea.
Change in memory usage detected by benchmark. Memory Report for c119d44
|
Change in memory usage detected by benchmark. Memory Report for a9e7e76
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
partially reviewed, will continue to review
pub fn walk_ident(vis: &mut impl MutVisitor, ident: &mut Ident) { | ||
vis.visit_span(&mut ident.span); | ||
} | ||
|
||
pub fn walk_idents(vis: &mut impl MutVisitor, ident: &mut crate::ast::Idents) { | ||
for ref mut ident in &mut *ident.0 { | ||
pub fn walk_idents(vis: &mut impl MutVisitor, idents: &mut [Ident]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we drop this visitor handle if idents
are a proper node type anymore?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great question, I went back and forth on this for a while. Ultimately, I decided to keep visit_idents
to avoid updating the current usages and bloating this PR even further.
This came up in a conversation with Ian as well, and my response was basically:
In the AST, many of choices came down to "how do I avoid touching a billion lines of code", and this was one of them
I think I'll sleep on it and see on Monday if I want to open a separate refactoring PR, or push even more changes to this one...
} | ||
} | ||
|
||
pub fn walk_idents<'a>(vis: &mut impl Visitor<'a>, idents: &'a [Ident]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same here
@@ -3080,15 +3080,15 @@ fn local_open() { | |||
namespace B { function Bar() : () {} } | |||
"}, | |||
"", | |||
&expect![[r#" | |||
&expect![[r##" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: sometimes the macro will erroneously put extra # characters in the expects. Please remove them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OMG, I took so many passes over this PR trying to revert the sneaky hash changes. I agree with you, but in this file, the warning is suppressed (with #![allow(clippy::needless_raw_string_hashes)]
) , and the existing test cases are full of ##
. So I thought I should stick to that convention. Now what?! 😆
"namespace |", | ||
&expect![[r" | ||
WordKinds( | ||
0x0, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe a comment here saying that 0x0
means no valid completion kinds.
@idavis found a bug: Trying to complete import QIR.in; causes a panic since Working on the fix. |
Fixed with my latest commit. |
Change in memory usage detected by benchmark. Memory Report for 96002f0
|
Feature overview
This PR enables:
At the cursor location indicated by
|
, we should only get the keywordapply
.Here, we should only get type names.
The completions here should return only items that exist under
Std.Diagnostics
.Code changes
Parser completions module
Introduce the
completion
module in the parser.possible_words_at_offset_in_source
- a new API to return completions at a particular offset . Uses the parser directly as the best source of knowledge about what tokens to expect.WordKinds
flags to define the different kinds of word tokens that the parser can expect during parsing. (Seecompiler/qsc_parse/src/completion/word_kinds.rs
for longer description)compiler/qsc_parse/src/completion/collector.rs
for a description of how this all works.expect()
s at all possible locations in the parser where we expect a word.Error recovery - AST changes
PathKind::Ok
andPathKind::Err
.open Std.
" where we need:.
to be valid, so we can use them to look up possible completions.open
statement to also be a valid node, so we can tell that the context requires a namespace (and not, say, a type name).Idents
struct and replace it with a trait implemented by various types such asPath
andIdent
slices. This should make the convenience methods easier to use without cloning in/out of the different types we need.Error recovery - parser changes
new Foo.
is still parsed as a valid struct expression node, allowing us to access theFoo
ident from the ASTopen
statement parsing so thatopen Foo.
can be recognized as an open item in the AST.Language service
In
language_service
is essentially a complete rewrite ofcompletions.rs
, so I suggest reviewing the new code only and ditching the diff.Other parser changes
path_field_op
export
,import
)Other notes
import
s from locals (they conflict with globals already added to completion) (fix inresolve.rs
)language-configuration.json
: and removed wonky word pattern in playground and VS Code because we don't need it anymore now that we have proper.
completions! Also update trigger characters.Not in this PR (coming next)