Skip to content

Commit 91afda4

Browse files
authored
Language service crate (microsoft#371)
This adds a `language_service` Rust crate. The language service is simply a struct that maintains a compilation state for each known document. It receives document edits and updates the current compilation state based on those edits. It provides methods (`get_completions`, `get_definition`, `hover`) to get information about the current compilation.
1 parent 7167620 commit 91afda4

File tree

20 files changed

+784
-11
lines changed

20 files changed

+784
-11
lines changed

.github/CODEOWNERS

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
/compiler @idavis @samarsha @swernli
66
/fuzz @billti @kuzminrobin @swernli
77
/katas @billti @cesarzc @swernli
8+
/jupyterlab @billti @idavis @minestarks
9+
/language_service @billti @idavis @minestarks
810
/library @cesarzc @DmitryVasilevsky @swernli
911
/npm @billti @cesarzc @minestarks
1012
/pip @billti @idavis @minestarks

Cargo.lock

+11
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ members = [
1111
"compiler/qsc_passes",
1212
"fuzz",
1313
"katas",
14+
"language_service",
1415
"library/tests",
1516
"pip",
1617
"wasm",
@@ -32,6 +33,7 @@ getrandom = { version = "0.2" }
3233
indoc = "2.0.0"
3334
js-sys = "0.3.61"
3435
libfuzzer-sys = "0.4"
36+
log = "0.4"
3537
miette = "5.6.0"
3638
thiserror = "1.0.39"
3739
num-bigint = "0.4.3"

compiler/qsc/src/bin/qsc.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ fn main() -> miette::Result<ExitCode> {
7979
Ok(ExitCode::SUCCESS)
8080
} else {
8181
for error in errors {
82-
if let Some(source) = unit.sources.find_diagnostic(&error) {
82+
if let Some(source) = unit.sources.find_by_diagnostic(&error) {
8383
eprintln!("{:?}", Report::new(error).with_source_code(source.clone()));
8484
} else {
8585
eprintln!("{:?}", Report::new(error));

compiler/qsc/src/error.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ impl<S, E> WithSource<S, E> {
3535

3636
impl<E: Diagnostic> WithSource<Source, E> {
3737
pub fn from_map(sources: &SourceMap, error: E, stack_trace: Option<String>) -> Self {
38-
let source = sources.find_diagnostic(&error).cloned();
38+
let source = sources.find_by_diagnostic(&error).cloned();
3939
Self {
4040
source,
4141
error,

compiler/qsc/src/interpret/debug.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ fn get_item_parent(store: &PackageStore, id: GlobalId) -> Option<Item> {
6868
fn get_item_file_name(store: &PackageStore, id: GlobalId) -> Option<String> {
6969
store.get(id.package).and_then(|unit| {
7070
let item = unit.package.items.get(id.item)?;
71-
let source = unit.sources.find_offset(item.span.lo);
72-
Some(source.name.to_string())
71+
let source = unit.sources.find_by_offset(item.span.lo);
72+
source.map(|s| s.name.to_string())
7373
})
7474
}
7575

compiler/qsc/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,5 @@ pub use qsc_frontend::compile::{PackageStore, SourceMap};
1212
pub mod hir {
1313
pub use qsc_hir::{hir::*, *};
1414
}
15+
16+
pub use qsc_data_structures::span::Span;

compiler/qsc_frontend/src/compile.rs

+11-6
Original file line numberDiff line numberDiff line change
@@ -67,28 +67,33 @@ impl SourceMap {
6767
}
6868

6969
#[must_use]
70-
pub fn find_offset(&self, offset: u32) -> &Source {
70+
pub fn find_by_offset(&self, offset: u32) -> Option<&Source> {
7171
self.sources
7272
.iter()
7373
.chain(&self.entry)
7474
.rev()
7575
.find(|source| offset >= source.offset)
76-
.expect("offset should match at least one source")
7776
}
7877

79-
pub fn find_diagnostic(&self, diagnostic: &impl Diagnostic) -> Option<&Source> {
78+
#[must_use]
79+
pub fn find_by_diagnostic(&self, diagnostic: &impl Diagnostic) -> Option<&Source> {
8080
diagnostic
8181
.labels()
8282
.and_then(|mut labels| labels.next())
83-
.map(|label| {
84-
self.find_offset(
83+
.and_then(|label| {
84+
self.find_by_offset(
8585
label
8686
.offset()
8787
.try_into()
8888
.expect("offset should fit into u32"),
8989
)
9090
})
9191
}
92+
93+
#[must_use]
94+
pub fn find_by_name(&self, name: &str) -> Option<&Source> {
95+
self.sources.iter().find(|s| s.name.as_ref() == name)
96+
}
9297
}
9398

9499
#[derive(Clone, Debug)]
@@ -443,7 +448,7 @@ fn next_offset(sources: &[Source]) -> u32 {
443448
fn assert_no_errors(sources: &SourceMap, errors: &mut Vec<Error>) {
444449
if !errors.is_empty() {
445450
for error in errors.drain(..) {
446-
if let Some(source) = sources.find_diagnostic(&error) {
451+
if let Some(source) = sources.find_by_diagnostic(&error) {
447452
eprintln!("{:?}", Report::new(error).with_source_code(source.clone()));
448453
} else {
449454
eprintln!("{:?}", Report::new(error));

compiler/qsc_frontend/src/compile/tests.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ fn error_span(error: &Error) -> Span {
3636

3737
fn source_span<'a>(sources: &'a SourceMap, error: &Error) -> (&'a str, Span) {
3838
let span = error_span(error);
39-
let source = sources.find_offset(span.lo);
39+
let source = sources
40+
.find_by_offset(span.lo)
41+
.expect("offset should match at least one source");
4042
(
4143
&source.name,
4244
Span {

language_service/Cargo.toml

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[package]
2+
name = "qsls"
3+
version = "0.0.0"
4+
5+
authors.workspace = true
6+
edition.workspace = true
7+
homepage.workspace = true
8+
license.workspace = true
9+
repository.workspace = true
10+
11+
[dev-dependencies]
12+
indoc = { workspace = true }
13+
14+
[dependencies]
15+
log = { workspace = true }
16+
miette = { workspace = true }
17+
qsc = { path = "../compiler/qsc" }
18+
enum-iterator = { workspace = true }

language_service/README.md

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Q# Language Service
2+
3+
This crate contains the implementation of Q# editor features such as
4+
auto-complete, go-to-definition and hover.
5+
6+
The interface for the language service is based on the
7+
[Language Server Protocol](https://microsoft.github.io/language-server-protocol/specifications/specification-current),
8+
even though a true LSP server implementation is not provided here.
9+
Following the LSP protocol makes it easy to use the implementation in
10+
a variety of editors (Monaco, VS Code, JupyterLab) whose extension APIs
11+
either use LSP or map closely to LSP concepts.

language_service/src/completion.rs

+166
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
#[cfg(test)]
5+
mod tests;
6+
7+
use crate::qsc_utils::{map_offset, span_contains, Compilation};
8+
use qsc::hir::{
9+
visit::{walk_item, Visitor},
10+
ItemKind, {Block, Item, Package},
11+
};
12+
use std::collections::HashSet;
13+
14+
// It would have been nice to match these enum values to the ones used by
15+
// VS Code and Monaco, but unfortunately those two disagree on the values.
16+
// So we define our own unique enum here to reduce confusion.
17+
#[derive(Clone, Debug, PartialEq)]
18+
#[allow(clippy::module_name_repetitions)]
19+
pub enum CompletionItemKind {
20+
Function,
21+
Module,
22+
Keyword,
23+
Issue,
24+
}
25+
26+
#[derive(Debug)]
27+
#[allow(clippy::module_name_repetitions)]
28+
pub struct CompletionList {
29+
pub items: Vec<CompletionItem>,
30+
}
31+
32+
#[derive(Clone, Debug, PartialEq)]
33+
#[allow(clippy::module_name_repetitions)]
34+
pub struct CompletionItem {
35+
pub label: String,
36+
pub kind: CompletionItemKind,
37+
}
38+
39+
pub(crate) fn get_completions(
40+
compilation: &Compilation,
41+
source_name: &str,
42+
offset: u32,
43+
) -> CompletionList {
44+
// Map the file offset into a SourceMap offset
45+
let offset = map_offset(&compilation.source_map, source_name, offset);
46+
let package = &compilation.package;
47+
let std_package = &compilation
48+
.package_store
49+
.get(compilation.std_package_id)
50+
.expect("expected to find std package")
51+
.package;
52+
53+
// Collect namespaces
54+
let mut namespace_collector = NamespaceCollector {
55+
namespaces: HashSet::new(),
56+
};
57+
namespace_collector.visit_package(package);
58+
namespace_collector.visit_package(std_package);
59+
60+
// All namespaces
61+
let mut namespaces = namespace_collector
62+
.namespaces
63+
.drain()
64+
.map(|ns| CompletionItem {
65+
label: ns,
66+
kind: CompletionItemKind::Module,
67+
})
68+
.collect::<Vec<_>>();
69+
70+
// Determine context for the offset
71+
let mut context_builder = ContextFinder {
72+
offset,
73+
context: if compilation.package.items.values().next().is_none() {
74+
Context::NoCompilation
75+
} else {
76+
Context::TopLevel
77+
},
78+
};
79+
context_builder.visit_package(package);
80+
let context = context_builder.context;
81+
82+
let mut items = Vec::new();
83+
match context {
84+
Context::Namespace => {
85+
items.push(CompletionItem {
86+
label: "open".to_string(),
87+
kind: CompletionItemKind::Keyword,
88+
});
89+
items.append(&mut namespaces);
90+
}
91+
Context::Block | Context::NoCompilation => {
92+
// Add everything we know of.
93+
// All callables from std package
94+
items.append(&mut callable_names_from_package(std_package));
95+
// Callables from the current document
96+
items.append(&mut callable_names_from_package(package));
97+
items.append(&mut namespaces);
98+
}
99+
Context::TopLevel | Context::NotSignificant => items.push(CompletionItem {
100+
label: "namespace".to_string(),
101+
kind: CompletionItemKind::Keyword,
102+
}),
103+
}
104+
CompletionList { items }
105+
}
106+
107+
struct NamespaceCollector {
108+
namespaces: HashSet<String>,
109+
}
110+
111+
impl Visitor<'_> for NamespaceCollector {
112+
fn visit_item(&mut self, item: &Item) {
113+
if let ItemKind::Namespace(ident, _) = &item.kind {
114+
// Collect namespaces
115+
self.namespaces.insert(ident.name.to_string());
116+
}
117+
walk_item(self, item);
118+
}
119+
}
120+
121+
struct ContextFinder {
122+
offset: u32,
123+
context: Context,
124+
}
125+
126+
#[derive(Debug, PartialEq)]
127+
enum Context {
128+
NoCompilation,
129+
TopLevel,
130+
Namespace,
131+
Block,
132+
NotSignificant,
133+
}
134+
135+
impl Visitor<'_> for ContextFinder {
136+
fn visit_item(&mut self, item: &Item) {
137+
if span_contains(item.span, self.offset) {
138+
self.context = match &item.kind {
139+
ItemKind::Namespace(..) => Context::Namespace,
140+
_ => Context::NotSignificant,
141+
}
142+
}
143+
144+
walk_item(self, item);
145+
}
146+
147+
fn visit_block(&mut self, block: &Block) {
148+
if span_contains(block.span, self.offset) {
149+
self.context = Context::Block;
150+
}
151+
}
152+
}
153+
154+
fn callable_names_from_package(package: &Package) -> Vec<CompletionItem> {
155+
package
156+
.items
157+
.values()
158+
.filter_map(|i| match &i.kind {
159+
ItemKind::Callable(callable_decl) => Some(CompletionItem {
160+
label: callable_decl.name.name.to_string(),
161+
kind: CompletionItemKind::Function,
162+
}),
163+
_ => None,
164+
})
165+
.collect::<Vec<_>>()
166+
}

0 commit comments

Comments
 (0)