Repository: microsoft/qsharp (rust)

Issue:
Python cells get marked as Q# in VS Code notebooks, causing syntax errors and confusion
The VS Code notebook cell language setting is undiscoverable to new users, but we have to rely on it for the language service to work correctly.

![image](https://github.com/microsoft/qsharp/assets/16928427/557b8092-c7ba-4d25-b7c7-3ed0bff0e7f4)

I've heard several new users wonder "why am I getting syntax errors in Python code?" , while still not realizing that the lower right corner says "Q#" and that it's a language setting that they can change.

**To Reproduce**

Steps to reproduce the behavior:
1. Create a new Python notebook
2. In a code cell, type
```
%%qsharp
operation Foo() : Unit {}
```
3. Notice how the cell language is automatically set to Q# , and the language service is working as expected.
4. Now add a new code cell below the last one. Start typing Python in that cell.
5. Notice the syntax errors, because the new cell is marked as Q# as well.

Step 3 above is done by us, but step 5 is performed automatically by VS Code - it just sets the cell language to be the same as the last one by default. We may be able to override this for a less confusing user experience.

(Seen in commit cb8e3805e5c3978ec9e0fba87c6eb65d944b3840)

Pull request:
Set notebook cell language back to Python if `%%qsharp` magic isn't there
Fixes #1117

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2fd97ef9..dc61fcae 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -150,6 +150,9 @@ jobs:
   runBenchmark:
     name: run benchmark
     runs-on: ubuntu-latest
+    permissions:
+      contents: read
+      pull-requests: write
     steps:
       - uses: actions/checkout@v3
       - uses: boa-dev/criterion-compare-action@v3
@@ -161,6 +164,9 @@ jobs:
   runMemoryProfile:
     name: run memory profile
     runs-on: ubuntu-latest
+    permissions:
+      contents: read
+      pull-requests: write
     steps:
       - uses: actions/checkout@v2
         with:
diff --git a/compiler/qsc/src/lib.rs b/compiler/qsc/src/lib.rs
index ef7b8e80..d11b577e 100644
--- a/compiler/qsc/src/lib.rs
+++ b/compiler/qsc/src/lib.rs
@@ -8,6 +8,7 @@ pub mod compile;
 pub mod error;
 pub mod incremental;
 pub mod interpret;
+pub mod location;
 pub mod target;
 
 pub use qsc_frontend::compile::{
diff --git a/compiler/qsc/src/location.rs b/compiler/qsc/src/location.rs
new file mode 100644
index 00000000..1c4a3b3a
--- /dev/null
+++ b/compiler/qsc/src/location.rs
@@ -0,0 +1,256 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+use std::sync::Arc;
+
+use qsc_data_structures::{
+    line_column::{Encoding, Range},
+    span::Span,
+};
+use qsc_frontend::compile::PackageStore;
+use qsc_hir::hir::PackageId;
+
+pub const QSHARP_LIBRARY_URI_SCHEME: &str = "qsharp-library-source";
+
+/// Describes a location in source code in terms of a source name and [`Range`].
+#[derive(Debug, PartialEq, Clone)]
+pub struct Location {
+    pub source: Arc<str>,
+    pub range: Range,
+}
+
+impl Location {
+    /// Creates a [`Location`] from a package ID and a SourceMap-relative span.
+    ///
+    /// To differentiate user sources from library sources, this function takes
+    /// a `user_package_id` parameter which denotes the user package.
+    /// All other packages in the package store are assumed to be library packages.
+    /// Source names from library packages are prepended with a unique URI scheme.
+    #[must_use]
+    pub fn from(
+        span: Span,
+        package_id: PackageId,
+        package_store: &PackageStore,
+        user_package_id: PackageId,
+        position_encoding: Encoding,
+    ) -> Self {
+        let source = package_store
+            .get(package_id)
+            .expect("package id must exist in store")
+            .sources
+            .find_by_offset(span.lo)
+            .expect("source should exist for offset");
+
+        let source_name = if package_id == user_package_id {
+            source.name.clone()
+        } else {
+            // Currently the only supported external packages are our library packages,
+            // URI's to which need to include our custom library scheme.
+            format!("{}:{}", QSHARP_LIBRARY_URI_SCHEME, source.name).into()
+        };
+
+        Location {
+            source: source_name,
+            range: Range::from_span(position_encoding, &source.contents, &(span - source.offset)),
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::compile;
+    use expect_test::expect;
+    use qsc_data_structures::{line_column::Encoding, span::Span};
+    use qsc_frontend::compile::{PackageStore, RuntimeCapabilityFlags, SourceMap};
+    use qsc_hir::hir::PackageId;
+    use qsc_passes::PackageType;
+
+    use super::Location;
+
+    #[test]
+    fn from_std_span() {
+        let (store, std_package_id, user_package_id) = compile_package();
+
+        let location = Location::from(
+            Span { lo: 0, hi: 1 },
+            std_package_id,
+            &store,
+            user_package_id,
+            Encoding::Utf8,
+        );
+
+        expect![[r#"
+            Location {
+                source: "qsharp-library-source:arrays.qs",
+                range: Range {
+                    start: Position {
+                        line: 0,
+                        column: 0,
+                    },
+                    end: Position {
+                        line: 0,
+                        column: 1,
+                    },
+                },
+            }
+        "#]]
+        .assert_debug_eq(&location);
+    }
+
+    #[test]
+    fn from_core_span() {
+        let (store, _, user_package_id) = compile_package();
+
+        let location = Location::from(
+            Span { lo: 0, hi: 1 },
+            PackageId::CORE,
+            &store,
+            user_package_id,
+            Encoding::Utf8,
+        );
+
+        expect![[r#"
+            Location {
+                source: "qsharp-library-source:core/core.qs",
+                range: Range {
+                    start: Position {
+                        line: 0,
+                        column: 0,
+                    },
+                    end: Position {
+                        line: 0,
+                        column: 1,
+                    },
+                },
+            }
+        "#]]
+        .assert_debug_eq(&location);
+    }
+
+    #[test]
+    fn from_user_span() {
+        let (store, _, user_package_id) = compile_package();
+
+        let bar_start_offset = store
+            .get(user_package_id)
+            .expect("expected to find user package")
+            .sources
+            .find_by_name("bar.qs")
+            .expect("expected to find source")
+            .offset;
+
+        let location = Location::from(
+            Span {
+                lo: bar_start_offset,
+                hi: bar_start_offset + 1,
+            },
+            user_package_id,
+            &store,
+            user_package_id,
+            Encoding::Utf8,
+        );
+
+        expect![[r#"
+            Location {
+                source: "bar.qs",
+                range: Range {
+                    start: Position {
+                        line: 0,
+                        column: 0,
+                    },
+                    end: Position {
+                        line: 0,
+                        column: 1,
+                    },
+                },
+            }
+        "#]]
+        .assert_debug_eq(&location);
+    }
+
+    #[test]
+    fn from_out_of_bounds_lo() {
+        let (store, _, user_package_id) = compile_package();
+
+        let location = Location::from(
+            Span { lo: 1000, hi: 2000 },
+            user_package_id,
+            &store,
+            user_package_id,
+            Encoding::Utf8,
+        );
+
+        // Per [`Range`] spec, out of bounds positions map to EOF
+        expect![[r#"
+            Location {
+                source: "bar.qs",
+                range: Range {
+                    start: Position {
+                        line: 0,
+                        column: 17,
+                    },
+                    end: Position {
+                        line: 0,
+                        column: 17,
+                    },
+                },
+            }
+        "#]]
+        .assert_debug_eq(&location);
+    }
+
+    #[test]
+    fn from_out_of_bounds_hi() {
+        let (store, _, user_package_id) = compile_package();
+
+        let location = Location::from(
+            Span { lo: 0, hi: 2000 },
+            user_package_id,
+            &store,
+            user_package_id,
+            Encoding::Utf8,
+        );
+
+        // Per [`Range`] spec, out of bounds positions map to EOF
+        expect![[r#"
+            Location {
+                source: "foo.qs",
+                range: Range {
+                    start: Position {
+                        line: 0,
+                        column: 0,
+                    },
+                    end: Position {
+                        line: 0,
+                        column: 17,
+                    },
+                },
+            }
+        "#]]
+        .assert_debug_eq(&location);
+    }
+
+    fn compile_package() -> (PackageStore, PackageId, PackageId) {
+        let mut store = PackageStore::new(compile::core());
+        let mut dependencies = Vec::new();
+
+        let (package_type, capabilities) = (PackageType::Lib, RuntimeCapabilityFlags::all());
+
+        let std = compile::std(&store, capabilities);
+        let std_package_id = store.insert(std);
+
+        dependencies.push(std_package_id);
+        let sources = SourceMap::new(
+            [
+                ("foo.qs".into(), "namespace Foo { }".into()),
+                ("bar.qs".into(), "namespace Bar { }".into()),
+            ],
+            None,
+        );
+        let (unit, _) =
+            compile::compile(&store, &dependencies, sources, package_type, capabilities);
+        let user_package_id = store.insert(unit);
+
+        (store, std_package_id, user_package_id)
+    }
+}
diff --git a/language_service/src/definition.rs b/language_service/src/definition.rs
index ddc16213..61cf021c 100644
--- a/language_service/src/definition.rs
+++ b/language_service/src/definition.rs
@@ -6,11 +6,11 @@ mod tests;
 
 use crate::compilation::Compilation;
 use crate::name_locator::{Handler, Locator, LocatorContext};
-use crate::protocol::Location;
 use crate::qsc_utils::into_location;
 use qsc::ast::visit::Visitor;
 use qsc::hir::PackageId;
 use qsc::line_column::{Encoding, Position};
+use qsc::location::Location;
 use qsc::{ast, hir, Span};
 
 pub(crate) fn get_definition(
diff --git a/language_service/src/lib.rs b/language_service/src/lib.rs
index 03b63552..98bf10b5 100644
--- a/language_service/src/lib.rs
+++ b/language_service/src/lib.rs
@@ -27,10 +27,13 @@ use futures::channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender};
 use futures_util::StreamExt;
 use log::{trace, warn};
 use protocol::{
-    CompletionList, DiagnosticUpdate, Hover, Location, NotebookMetadata, SignatureHelp,
+    CompletionList, DiagnosticUpdate, Hover, NotebookMetadata, SignatureHelp,
     WorkspaceConfigurationUpdate,
 };
-use qsc::line_column::{Encoding, Position, Range};
+use qsc::{
+    line_column::{Encoding, Position, Range},
+    location::Location,
+};
 use qsc_project::JSFileEntry;
 use state::{CompilationState, CompilationStateUpdater};
 use std::{cell::RefCell, fmt::Debug, future::Future, pin::Pin, rc::Rc, sync::Arc};
diff --git a/language_service/src/protocol.rs b/language_service/src/protocol.rs
index 190b5f2d..78143267 100644
--- a/language_service/src/protocol.rs
+++ b/language_service/src/protocol.rs
@@ -86,12 +86,6 @@ impl Hash for CompletionItem {
     }
 }
 
-#[derive(Debug, PartialEq)]
-pub struct Location {
-    pub source: String,
-    pub span: Range,
-}
-
 #[derive(Debug, PartialEq)]
 pub struct Hover {
     pub contents: String,
diff --git a/language_service/src/qsc_utils.rs b/language_service/src/qsc_utils.rs
index 384b6513..940addc9 100644
--- a/language_service/src/qsc_utils.rs
+++ b/language_service/src/qsc_utils.rs
@@ -1,15 +1,11 @@
 // Copyright (c) Microsoft Corporation.
 // Licensed under the MIT License.
 
-use crate::{
-    compilation::Compilation,
-    protocol::{self},
-};
+use crate::compilation::Compilation;
 use qsc::line_column::{Encoding, Range};
+use qsc::location::Location;
 use qsc::{ast, hir::PackageId, SourceMap, Span};
 
-pub(crate) const QSHARP_LIBRARY_URI_SCHEME: &str = "qsharp-library-source";
-
 pub(crate) fn span_contains(span: Span, offset: u32) -> bool {
     offset >= span.lo && offset < span.hi
 }
@@ -39,33 +35,16 @@ pub(crate) fn into_range(encoding: Encoding, span: Span, source_map: &SourceMap)
 pub(crate) fn into_location(
     position_encoding: Encoding,
     compilation: &Compilation,
-    location: Span,
+    span: Span,
     package_id: PackageId,
-) -> protocol::Location {
-    let source_map = &compilation
-        .package_store
-        .get(package_id)
-        .expect("package id must exist in store")
-        .sources;
-    let source = source_map
-        .find_by_offset(location.lo)
-        .expect("source should exist for offset");
-    let source_name = if package_id == compilation.user_package_id {
-        source.name.to_string()
-    } else {
-        // Currently the only supported external packages are our library packages,
-        // URI's to which need to include our custom library scheme.
-        format!("{}:{}", QSHARP_LIBRARY_URI_SCHEME, source.name)
-    };
-
-    protocol::Location {
-        source: source_name,
-        span: Range::from_span(
-            position_encoding,
-            &source.contents,
-            &(location - source.offset),
-        ),
-    }
+) -> Location {
+    Location::from(
+        span,
+        package_id,
+        &compilation.package_store,
+        compilation.user_package_id,
+        position_encoding,
+    )
 }
 
 pub(crate) fn find_ident<'a>(
diff --git a/language_service/src/references.rs b/language_service/src/references.rs
index 0a265e64..44931bc7 100644
--- a/language_service/src/references.rs
+++ b/language_service/src/references.rs
@@ -8,12 +8,12 @@ use std::rc::Rc;
 
 use crate::compilation::{Compilation, Lookup};
 use crate::name_locator::{Handler, Locator, LocatorContext};
-use crate::protocol::Location;
 use crate::qsc_utils::into_location;
 use qsc::ast::visit::{walk_callable_decl, walk_expr, walk_ty, Visitor};
 use qsc::hir::ty::Ty;
 use qsc::hir::{PackageId, Res};
 use qsc::line_column::{Encoding, Position};
+use qsc::location::Location;
 use qsc::{ast, hir, resolve, Span};
 
 pub(crate) fn get_references(
diff --git a/language_service/src/rename.rs b/language_service/src/rename.rs
index c2934dd0..b0bef3e7 100644
--- a/language_service/src/rename.rs
+++ b/language_service/src/rename.rs
@@ -6,11 +6,11 @@ mod tests;
 
 use crate::compilation::{Compilation, Lookup};
 use crate::name_locator::{Handler, Locator, LocatorContext};
-use crate::protocol::Location;
 use crate::qsc_utils::into_range;
 use crate::references::ReferenceFinder;
 use qsc::ast::visit::Visitor;
 use qsc::line_column::{Encoding, Position, Range};
+use qsc::location::Location;
 use qsc::{ast, hir, resolve, Span};
 
 pub(crate) fn prepare_rename(
@@ -126,9 +126,9 @@ impl<'a> Rename<'a> {
                 .for_ty_param(param_id, current_callable)
                 .into_iter()
                 .map(|l| {
-                    assert!(!l.span.empty(), "Type parameter name is empty");
+                    assert!(!l.range.empty(), "Type parameter name is empty");
                     Location {
-                        span: type_param_ident_range(l.span),
+                        range: type_param_ident_range(l.range),
                         ..l
                     }
                 })
diff --git a/npm/ux/estimatesOverview.tsx b/npm/ux/estimatesOverview.tsx
index dcf4820b..c352c69d 100644
--- a/npm/ux/estimatesOverview.tsx
+++ b/npm/ux/estimatesOverview.tsx
@@ -112,7 +112,13 @@ function reDataToRowScatter(data: ReData, color: string): ScatterSeries {
 function createRunNames(estimatesData: ReData[]): string[] {
   // If there's only 1 entry, use the shared run name
   if (estimatesData.length === 1) {
-    return [estimatesData[0].jobParams.sharedRunName];
+    const name =
+      estimatesData[0].jobParams.sharedRunName ??
+      estimatesData[0].jobParams.qubitParams.name ??
+      estimatesData[0].jobParams.qecScheme.name ??
+      "estimate";
+
+    return [name];
   }
 
   const fields: string[][] = [];
diff --git a/pip/src/displayable_output.rs b/pip/src/displayable_output.rs
index fceb8f54..9d5fa103 100644
--- a/pip/src/displayable_output.rs
+++ b/pip/src/displayable_output.rs
@@ -7,11 +7,10 @@ mod tests;
 use num_bigint::BigUint;
 use num_complex::{Complex64, ComplexFloat};
 use qsc::{fmt_basis_state_label, fmt_complex, format_state_id, get_phase};
-use rustc_hash::FxHashMap;
 use std::fmt::Write;
 
 #[derive(Clone)]
-pub struct DisplayableState(pub FxHashMap<BigUint, Complex64>, pub usize);
+pub struct DisplayableState(pub Vec<(BigUint, Complex64)>, pub usize);
 
 impl DisplayableState {
     pub fn to_plain(&self) -> String {
diff --git a/pip/src/interpreter.rs b/pip/src/interpreter.rs
index 360b6cf5..bd34b469 100644
--- a/pip/src/interpreter.rs
+++ b/pip/src/interpreter.rs
@@ -28,7 +28,6 @@ use qsc::{
     PackageType, SourceMap,
 };
 use resource_estimator::{self as re, estimate_expr};
-use rustc_hash::FxHashMap;
 use std::fmt::Write;
 
 #[pymodule]
@@ -168,10 +167,7 @@ impl Interpreter {
     /// pairs of real and imaginary amplitudes.
     fn dump_machine(&mut self) -> StateDump {
         let (state, qubit_count) = self.interpreter.get_quantum_state();
-        StateDump(DisplayableState(
-            state.into_iter().collect::<FxHashMap<_, _>>(),
-            qubit_count,
-        ))
+        StateDump(DisplayableState(state, qubit_count))
     }
 
     fn run(
@@ -336,7 +332,13 @@ impl StateDump {
     // Pass by value is needed for compatiblity with the pyo3 API.
     #[allow(clippy::needless_pass_by_value)]
     fn __getitem__(&self, key: BigUint) -> Option<(f64, f64)> {
-        self.0 .0.get(&key).map(|state| (state.re, state.im))
+        self.0 .0.iter().find_map(|state| {
+            if state.0 == key {
+                Some((state.1.re, state.1.im))
+            } else {
+                None
+            }
+        })
     }
 
     fn __len__(&self) -> usize {
@@ -459,7 +461,6 @@ impl Receiver for OptionalCallbackReceiver<'_> {
         qubit_count: usize,
     ) -> core::result::Result<(), Error> {
         if let Some(callback) = &self.callback {
-            let state = state.into_iter().collect::<FxHashMap<_, _>>();
             let out = DisplayableOutput::State(DisplayableState(state, qubit_count));
             callback
                 .call1(
diff --git a/samples/estimation/estimation-frontier-widgets.ipynb b/samples/estimation/estimation-frontier-widgets.ipynb
index 6cda95f1..dd1f40c9 100644
--- a/samples/estimation/estimation-frontier-widgets.ipynb
+++ b/samples/estimation/estimation-frontier-widgets.ipynb
@@ -2,19 +2,14 @@
  "cells": [
   {
    "cell_type": "code",
-   "execution_count": 1,
+   "execution_count": null,
    "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "application/javascript": "// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT License.\n\n// This file provides CodeMirror syntax highlighting for Q# magic cells\n// in classic Jupyter Notebooks. It does nothing in other (Jupyter Notebook 7,\n// VS Code, Azure Notebooks, etc.) environments.\n\n// Detect the prerequisites and do nothing if they don't exist.\nif (window.require && window.CodeMirror && window.Jupyter) {\n  // The simple mode plugin for CodeMirror is not loaded by default, so require it.\n  window.require([\"codemirror/addon/mode/simple\"], function defineMode() {\n    let rules = [\n      {\n        token: \"comment\",\n        regex: /(\\/\\/).*/,\n        beginWord: false,\n      },\n      {\n        token: \"string\",\n        regex: String.raw`^\\\"(?:[^\\\"\\\\]|\\\\[\\s\\S])*(?:\\\"|$)`,\n        beginWord: false,\n      },\n      {\n        token: \"keyword\",\n        regex: String.raw`(namespace|open|as|operation|function|body|adjoint|newtype|controlled|internal)\\b`,\n        beginWord: true,\n      },\n      {\n        token: \"keyword\",\n        regex: String.raw`(if|elif|else|repeat|until|fixup|for|in|return|fail|within|apply)\\b`,\n        beginWord: true,\n      },\n      {\n        token: \"keyword\",\n        regex: String.raw`(Adjoint|Controlled|Adj|Ctl|is|self|auto|distribute|invert|intrinsic)\\b`,\n        beginWord: true,\n      },\n      {\n        token: \"keyword\",\n        regex: String.raw`(let|set|use|borrow|mutable)\\b`,\n        beginWord: true,\n      },\n      {\n        token: \"operatorKeyword\",\n        regex: String.raw`(not|and|or)\\b|(w/)`,\n        beginWord: true,\n      },\n      {\n        token: \"operatorKeyword\",\n        regex: String.raw`(=)|(!)|(<)|(>)|(\\+)|(-)|(\\*)|(/)|(\\^)|(%)|(\\|)|(&&&)|(~~~)|(\\.\\.\\.)|(\\.\\.)|(\\?)`,\n        beginWord: false,\n      },\n      {\n        token: \"meta\",\n        regex: String.raw`(Int|BigInt|Double|Bool|Qubit|Pauli|Result|Range|String|Unit)\\b`,\n        beginWord: true,\n      },\n      {\n        token: \"atom\",\n        regex: String.raw`(true|false|Pauli(I|X|Y|Z)|One|Zero)\\b`,\n        beginWord: true,\n      },\n    ];\n    let simpleRules = [];\n    for (let rule of rules) {\n      simpleRules.push({\n        token: rule.token,\n        regex: new RegExp(rule.regex, \"g\"),\n        sol: rule.beginWord,\n      });\n      if (rule.beginWord) {\n        // Need an additional rule due to the fact that CodeMirror simple mode doesn't work with ^ token\n        simpleRules.push({\n          token: rule.token,\n          regex: new RegExp(String.raw`\\W` + rule.regex, \"g\"),\n          sol: false,\n        });\n      }\n    }\n\n    // Register the mode defined above with CodeMirror\n    window.CodeMirror.defineSimpleMode(\"qsharp\", { start: simpleRules });\n    window.CodeMirror.defineMIME(\"text/x-qsharp\", \"qsharp\");\n\n    // Tell Jupyter to associate %%qsharp magic cells with the qsharp mode\n    window.Jupyter.CodeCell.options_default.highlight_modes[\"qsharp\"] = {\n      reg: [/^%%qsharp/],\n    };\n\n    // Force re-highlighting of all cells the first time this code runs\n    for (const cell of window.Jupyter.notebook.get_cells()) {\n      cell.auto_highlight();\n    }\n  });\n}\n",
-      "text/plain": []
-     },
-     "metadata": {},
-     "output_type": "display_data"
-    }
-   ],
+   "outputs": [],
    "source": [
+    "# If necessary, install the qsharp package and the qsharp-widgets\n",
+    "# by running the following command in a terminal:\n",
+    "# pip install qsharp qsharp-widgets -U\n",
+    "\n",
     "import qsharp\n",
     "from qsharp_widgets import SpaceChart, EstimateDetails, EstimatesOverview, EstimatesPanel\n",
     "\n",
@@ -25,7 +20,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 34,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -64,25 +59,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 40,
+   "execution_count": null,
    "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "application/vnd.jupyter.widget-view+json": {
-       "model_id": "ad98b6f2e9dc4b748f0845751f903e1d",
-       "version_major": 2,
-       "version_minor": 0
-      },
-      "text/plain": [
-       "EstimatesPanel(estimates={0: {'status': 'success', 'jobParams': {'qecScheme': {'name': 'surface_code', 'errorC…"
-      ]
-     },
-     "execution_count": 40,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
+   "outputs": [],
    "source": [
     "# The full experience with the summary table, the space-time diagrams, the space diagrams, and the estimate details\n",
     "EstimatesPanel(result)"
@@ -90,25 +69,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 39,
+   "execution_count": null,
    "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "application/vnd.jupyter.widget-view+json": {
-       "model_id": "7b082ec4006b4134afe54c7e7068acb3",
-       "version_major": 2,
-       "version_minor": 0
-      },
-      "text/plain": [
-       "SpaceChart(estimates={'status': 'success', 'jobParams': {'qecScheme': {'name': 'surface_code', 'errorCorrectio…"
-      ]
-     },
-     "execution_count": 39,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
+   "outputs": [],
    "source": [
     "# Qubit usage diagram for the first (estimate index=0) run and the third (point index=2) shortest runtime\n",
     "SpaceChart(result[0],8)"
@@ -116,25 +79,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 37,
+   "execution_count": null,
    "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "application/vnd.jupyter.widget-view+json": {
-       "model_id": "d3c556b135f84bf19770a6fe2c17b297",
-       "version_major": 2,
-       "version_minor": 0
-      },
-      "text/plain": [
-       "EstimateDetails(estimates={'status': 'success', 'jobParams': {'qecScheme': {'name': 'surface_code', 'errorCorr…"
-      ]
-     },
-     "execution_count": 37,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
+   "outputs": [],
    "source": [
     "# Estimate details usage for the second (estimate index=0) run and the fourth (point index=3) shortest runtime\n",
     "EstimateDetails(result[1], 4)"
@@ -142,25 +89,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 36,
+   "execution_count": null,
    "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "application/vnd.jupyter.widget-view+json": {
-       "model_id": "88d4400236ed4f8ea65b32f803a19971",
-       "version_major": 2,
-       "version_minor": 0
-      },
-      "text/plain": [
-       "EstimatesOverview(colors=['#1f77b4', '#ff7f0e', 'blue', 'red', 'green', 'yellow'], estimates={0: {'status': 's…"
-      ]
-     },
-     "execution_count": 36,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
+   "outputs": [],
    "source": [
     "# Estimate overview with the summary table and the space-time diagram\n",
     "EstimatesOverview(result, colors=[\"#1f77b4\", \"#ff7f0e\", \"blue\", \"red\", \"green\", \"yellow\"], runNames=[\"Gate ns e3, surface\", \"Gate ns e4, surface\", \"Gate us e3, surface\", \"Gate us e4, surface\", \"Majorana ns e4, floquet\", \"Majorana ns e6, floquet\"])"
diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts
index 97ba38ee..d326f009 100644
--- a/vscode/src/extension.ts
+++ b/vscode/src/extension.ts
@@ -105,10 +105,10 @@ function registerDocumentUpdateHandlers(languageService: ILanguageService) {
   const subscriptions = [];
   subscriptions.push(
     vscode.workspace.onDidOpenTextDocument((document) => {
-      const documentType = isQsharpDocument(document)
-        ? QsharpDocumentType.Qsharp
-        : isQsharpNotebookCell(document)
+      const documentType = isQsharpNotebookCell(document)
         ? QsharpDocumentType.JupyterCell
+        : isQsharpDocument(document)
+        ? QsharpDocumentType.Qsharp
         : QsharpDocumentType.Other;
       if (documentType !== QsharpDocumentType.Other) {
         sendTelemetryEvent(
diff --git a/vscode/src/notebook.ts b/vscode/src/notebook.ts
index 5ff07368..7adf7e43 100644
--- a/vscode/src/notebook.ts
+++ b/vscode/src/notebook.ts
@@ -47,24 +47,49 @@ export function registerQSharpNotebookHandlers() {
         const addedCells = event.contentChanges
           .map((change) => change.addedCells)
           .flat();
+
         updateQSharpCellLanguages(changedCells.concat(addedCells));
       }
     }),
   );
 
+  let defaultLanguageId: string | undefined;
+
   function updateQSharpCellLanguages(cells: vscode.NotebookCell[]) {
     for (const cell of cells) {
-      // If this is a code cell that starts with %%qsharp, and language isn't already set to Q#, set it.
+      // If this is a code cell that starts with %%qsharp, and language wasn't already set to Q#, set it.
       if (cell.kind === vscode.NotebookCellKind.Code) {
         const document = cell.document;
-        if (
-          document.languageId !== qsharpLanguageId &&
-          findQSharpCellMagic(document)
-        ) {
-          vscode.languages.setTextDocumentLanguage(
-            cell.document,
-            qsharpLanguageId,
-          );
+        const currentLanguageId = document.languageId;
+        if (findQSharpCellMagic(document)) {
+          if (currentLanguageId !== qsharpLanguageId) {
+            // Remember the "default" language of the notebook (this will normally be Python)
+            defaultLanguageId = currentLanguageId;
+            vscode.languages.setTextDocumentLanguage(
+              cell.document,
+              qsharpLanguageId,
+            );
+            log.trace(
+              `setting cell ${cell.index} language to ${qsharpLanguageId}`,
+            );
+          }
+        } else {
+          // This is not a %%qsharp cell. If the language was set to Q#,
+          // change it back to the default language.
+          //
+          // If the cell language was not set to Q#, it's out of our purview and we don't
+          // want to automatically change the language settings. For example, this could
+          // be a %%bash cell magic and the user may have intentionally set the language
+          // to "shell".
+          if (currentLanguageId === qsharpLanguageId && defaultLanguageId) {
+            vscode.languages.setTextDocumentLanguage(
+              cell.document,
+              defaultLanguageId,
+            );
+            log.trace(
+              `setting cell ${cell.index} language to ${defaultLanguageId}`,
+            );
+          }
         }
       }
     }
diff --git a/wasm/src/language_service.rs b/wasm/src/language_service.rs
index 6313556d..2e4b8e81 100644
--- a/wasm/src/language_service.rs
+++ b/wasm/src/language_service.rs
@@ -239,10 +239,13 @@ impl LanguageService {
 
         let mut renames: FxHashMap<String, Vec<TextEdit>> = FxHashMap::default();
         locations.into_iter().for_each(|l| {
-            renames.entry(l.source).or_default().push(TextEdit {
-                range: l.span.into(),
-                newText: new_name.to_string(),
-            })
+            renames
+                .entry(l.source.to_string())
+                .or_default()
+                .push(TextEdit {
+                    range: l.range.into(),
+                    newText: new_name.to_string(),
+                })
         });
 
         let workspace_edit = WorkspaceEdit {
@@ -265,15 +268,6 @@ impl LanguageService {
     }
 }
 
-impl From<qsls::protocol::Location> for Location {
-    fn from(location: qsls::protocol::Location) -> Self {
-        Location {
-            source: location.source,
-            span: location.span.into(),
-        }
-    }
-}
-
 serializable_type! {
     WorkspaceConfiguration,
     {
diff --git a/wasm/src/line_column.rs b/wasm/src/line_column.rs
index d1a90507..7c7a771d 100644
--- a/wasm/src/line_column.rs
+++ b/wasm/src/line_column.rs
@@ -70,3 +70,12 @@ impl From<qsc::line_column::Range> for Range {
         }
     }
 }
+
+impl From<qsc::location::Location> for Location {
+    fn from(location: qsc::location::Location) -> Self {
+        Location {
+            source: location.source.to_string(),
+            span: location.range.into(),
+        }
+    }
+}


Test patch:
diff --git a/language_service/src/definition/tests.rs b/language_service/src/definition/tests.rs
index a364ac5e..8ccee134 100644
--- a/language_service/src/definition/tests.rs
+++ b/language_service/src/definition/tests.rs
@@ -4,10 +4,10 @@
 #![allow(clippy::needless_raw_string_hashes)]
 
 use expect_test::{expect, Expect};
+use qsc::location::Location;
 
 use super::get_definition;
 use crate::{
-    protocol::Location,
     test_utils::{
         compile_notebook_with_fake_stdlib_and_markers, compile_with_fake_stdlib_and_markers,
     },
@@ -26,8 +26,8 @@ fn assert_definition(source_with_markers: &str) {
         None
     } else {
         Some(Location {
-            source: "<source>".to_string(),
-            span: target_spans[0],
+            source: "<source>".into(),
+            range: target_spans[0],
         })
     };
     assert_eq!(&expected_definition, &actual_definition);
@@ -40,10 +40,7 @@ fn assert_definition_notebook(cells_with_markers: &[(&str, &str)]) {
     let expected_definition = if target_spans.is_empty() {
         None
     } else {
-        Some(Location {
-            source: target_spans[0].0.clone(),
-            span: target_spans[0].1,
-        })
+        Some(target_spans[0].clone())
     };
     assert_eq!(&expected_definition, &actual_definition);
 }
@@ -302,7 +299,7 @@ fn std_call() {
             Some(
                 Location {
                     source: "qsharp-library-source:<std>",
-                    span: Range {
+                    range: Range {
                         start: Position {
                             line: 1,
                             column: 26,
@@ -410,7 +407,7 @@ fn std_udt() {
             Some(
                 Location {
                     source: "qsharp-library-source:<std>",
-                    span: Range {
+                    range: Range {
                         start: Position {
                             line: 4,
                             column: 24,
@@ -442,7 +439,7 @@ fn std_udt_udt_field() {
             Some(
                 Location {
                     source: "qsharp-library-source:<std>",
-                    span: Range {
+                    range: Range {
                         start: Position {
                             line: 4,
                             column: 31,
diff --git a/language_service/src/hover/tests.rs b/language_service/src/hover/tests.rs
index 0f4ec359..fef0da84 100644
--- a/language_service/src/hover/tests.rs
+++ b/language_service/src/hover/tests.rs
@@ -37,7 +37,7 @@ fn check_notebook(cells_with_markers: &[(&str, &str)], expect: &Expect) {
 
     let actual =
         get_hover(&compilation, &cell_uri, position, Encoding::Utf8).expect("Expected a hover.");
-    assert_eq!(&actual.span, &target_spans[0].1);
+    assert_eq!(&actual.span, &target_spans[0].range);
     expect.assert_eq(&actual.contents);
 }
 
diff --git a/language_service/src/references/tests.rs b/language_service/src/references/tests.rs
index 4ebf3a1e..531a8064 100644
--- a/language_service/src/references/tests.rs
+++ b/language_service/src/references/tests.rs
@@ -5,7 +5,6 @@
 
 use super::get_references;
 use crate::{
-    protocol,
     test_utils::{
         compile_notebook_with_fake_stdlib_and_markers, compile_with_fake_stdlib_and_markers,
     },
@@ -43,7 +42,7 @@ fn check(source_with_markers: &str, include_declaration: bool) {
         include_declaration,
     )
     .into_iter()
-    .map(|l| l.span)
+    .map(|l| l.range)
     .collect::<Vec<_>>();
     for target in &target_spans {
         assert!(
@@ -71,10 +70,7 @@ fn check_notebook_exclude_decl(cells_with_markers: &[(&str, &str)]) {
         .collect::<Vec<_>>();
     for target in &target_spans {
         assert!(
-            actual.contains(&protocol::Location {
-                source: target.0.clone(),
-                span: target.1
-            }),
+            actual.contains(target),
             "expected {actual:?} to contain {target:?}"
         );
     }
@@ -98,7 +94,7 @@ fn std_callable_ref() {
             [
                 Location {
                     source: "qsharp-library-source:<std>",
-                    span: Range {
+                    range: Range {
                         start: Position {
                             line: 1,
                             column: 26,
@@ -111,7 +107,7 @@ fn std_callable_ref() {
                 },
                 Location {
                     source: "<source>",
-                    span: Range {
+                    range: Range {
                         start: Position {
                             line: 3,
                             column: 8,
@@ -124,7 +120,7 @@ fn std_callable_ref() {
                 },
                 Location {
                     source: "<source>",
-                    span: Range {
+                    range: Range {
                         start: Position {
                             line: 5,
                             column: 8,
@@ -255,7 +251,7 @@ fn std_udt_ref() {
             [
                 Location {
                     source: "qsharp-library-source:<std>",
-                    span: Range {
+                    range: Range {
                         start: Position {
                             line: 4,
                             column: 24,
@@ -268,7 +264,7 @@ fn std_udt_ref() {
                 },
                 Location {
                     source: "<source>",
-                    span: Range {
+                    range: Range {
                         start: Position {
                             line: 2,
                             column: 22,
@@ -345,7 +341,7 @@ fn std_field_ref() {
             [
                 Location {
                     source: "qsharp-library-source:<std>",
-                    span: Range {
+                    range: Range {
                         start: Position {
                             line: 4,
                             column: 31,
@@ -358,7 +354,7 @@ fn std_field_ref() {
                 },
                 Location {
                     source: "<source>",
-                    span: Range {
+                    range: Range {
                         start: Position {
                             line: 4,
                             column: 23,
diff --git a/language_service/src/rename/tests.rs b/language_service/src/rename/tests.rs
index 85f06b99..0e730b6e 100644
--- a/language_service/src/rename/tests.rs
+++ b/language_service/src/rename/tests.rs
@@ -20,7 +20,7 @@ fn check(source_with_markers: &str) {
         compile_with_fake_stdlib_and_markers(source_with_markers);
     let actual = get_rename(&compilation, "<source>", cursor_position, Encoding::Utf8)
         .into_iter()
-        .map(|l| l.span)
+        .map(|l| l.range)
         .collect::<Vec<_>>();
     for target in &target_spans {
         assert!(actual.contains(target));
@@ -391,7 +391,7 @@ fn notebook_rename_across_cells() {
             [
                 Location {
                     source: "cell1",
-                    span: Range {
+                    range: Range {
                         start: Position {
                             line: 0,
                             column: 10,
@@ -404,7 +404,7 @@ fn notebook_rename_across_cells() {
                 },
                 Location {
                     source: "cell2",
-                    span: Range {
+                    range: Range {
                         start: Position {
                             line: 0,
                             column: 0,
diff --git a/language_service/src/test_utils.rs b/language_service/src/test_utils.rs
index 115a3fe0..611c255a 100644
--- a/language_service/src/test_utils.rs
+++ b/language_service/src/test_utils.rs
@@ -9,6 +9,7 @@ use qsc::{
     hir::PackageId,
     incremental::Compiler,
     line_column::{Encoding, Position, Range},
+    location::Location,
     target::Profile,
     PackageStore, PackageType, SourceMap, Span,
 };
@@ -21,13 +22,13 @@ pub(crate) fn compile_with_fake_stdlib_and_markers(
     (
         compilation,
         cursor_offset,
-        target_spans.iter().map(|(_, s)| *s).collect(),
+        target_spans.iter().map(|l| l.range).collect(),
     )
 }
 
 pub(crate) fn compile_project_with_fake_stdlib_and_markers(
     sources_with_markers: &[(&str, &str)],
-) -> (Compilation, String, Position, Vec<(String, Range)>) {
+) -> (Compilation, String, Position, Vec<Location>) {
     let (sources, cursor_uri, cursor_offset, target_spans) =
         get_sources_and_markers(sources_with_markers);
 
@@ -58,7 +59,7 @@ pub(crate) fn compile_project_with_fake_stdlib_and_markers(
 
 pub(crate) fn compile_notebook_with_fake_stdlib_and_markers(
     cells_with_markers: &[(&str, &str)],
-) -> (Compilation, String, Position, Vec<(String, Range)>) {
+) -> (Compilation, String, Position, Vec<Location>) {
     let (cells, cell_uri, offset, target_spans) = get_sources_and_markers(cells_with_markers);
 
     let compilation =
@@ -160,12 +161,7 @@ fn compile_fake_stdlib() -> (PackageStore, PackageId) {
 #[allow(clippy::type_complexity)]
 fn get_sources_and_markers(
     sources: &[(&str, &str)],
-) -> (
-    Vec<(Arc<str>, Arc<str>)>,
-    String,
-    Position,
-    Vec<(String, Range)>,
-) {
+) -> (Vec<(Arc<str>, Arc<str>)>, String, Position, Vec<Location>) {
     let (mut cursor_uri, mut cursor_offset, mut target_spans) = (None, None, Vec::new());
     let sources = sources
         .iter()
@@ -189,10 +185,10 @@ fn get_sources_and_markers(
             }
             if !targets.is_empty() {
                 for span in target_offsets_to_spans(&targets) {
-                    target_spans.push((
-                        s.0.to_string(),
-                        Range::from_span(Encoding::Utf8, &source, &span),
-                    ));
+                    target_spans.push(Location {
+                        source: s.0.into(),
+                        range: Range::from_span(Encoding::Utf8, &source, &span),
+                    });
                 }
             }
             (Arc::from(s.0), Arc::from(source.as_ref()))
diff --git a/pip/src/displayable_output/tests.rs b/pip/src/displayable_output/tests.rs
index 2aa54cb5..1a9550e7 100644
--- a/pip/src/displayable_output/tests.rs
+++ b/pip/src/displayable_output/tests.rs
@@ -3,18 +3,12 @@
 
 use num_bigint::BigUint;
 use num_complex::Complex;
-use rustc_hash::FxHashMap;
 
 use crate::displayable_output::DisplayableState;
 
 #[test]
 fn display_neg_zero() {
-    let s = DisplayableState(
-        vec![(BigUint::default(), Complex::new(-0.0, -0.0))]
-            .into_iter()
-            .collect::<FxHashMap<_, _>>(),
-        1,
-    );
+    let s = DisplayableState(vec![(BigUint::default(), Complex::new(-0.0, -0.0))], 1);
     // -0 should be displayed as 0.0000 without a minus sign
     assert_eq!("STATE:\n|0⟩: 0.0000+0.0000𝑖", s.to_plain());
 }
@@ -22,11 +16,26 @@ fn display_neg_zero() {
 #[test]
 fn display_rounds_to_neg_zero() {
     let s = DisplayableState(
-        vec![(BigUint::default(), Complex::new(-0.00001, -0.00001))]
-            .into_iter()
-            .collect::<FxHashMap<_, _>>(),
+        vec![(BigUint::default(), Complex::new(-0.00001, -0.00001))],
         1,
     );
     // -0.00001 should be displayed as 0.0000 without a minus sign
     assert_eq!("STATE:\n|0⟩: 0.0000+0.0000𝑖", s.to_plain());
 }
+
+#[test]
+fn display_preserves_order() {
+    let s = DisplayableState(
+        vec![
+            (BigUint::from(0_u64), Complex::new(0.0, 0.0)),
+            (BigUint::from(1_u64), Complex::new(0.0, 1.0)),
+            (BigUint::from(2_u64), Complex::new(1.0, 0.0)),
+            (BigUint::from(3_u64), Complex::new(1.0, 1.0)),
+        ],
+        2,
+    );
+    assert_eq!(
+        "STATE:\n|00⟩: 0.0000+0.0000𝑖\n|01⟩: 0.0000+1.0000𝑖\n|10⟩: 1.0000+0.0000𝑖\n|11⟩: 1.0000+1.0000𝑖",
+        s.to_plain()
+    );
+}
diff --git a/vscode/test/suites/extensionUtils.ts b/vscode/test/suites/extensionUtils.ts
index 5c8a2b64..6f762a6f 100644
--- a/vscode/test/suites/extensionUtils.ts
+++ b/vscode/test/suites/extensionUtils.ts
@@ -31,3 +31,66 @@ export async function activateExtension() {
     `qsharp-tests: activate() completed in ${performance.now() - start}ms`,
   );
 }
+
+/**
+ * Waits until a condition is met, with a timeout.
+ *
+ * @param condition A function that returns true when the condition is met
+ * @param wakeUpOn The event that we want to wake up to check the condition
+ * @param timeoutMs If the condition is not met by this timeout, this function will throw
+ * @param timeoutErrorMsg The custom error message to throw if the condition is not met
+ */
+export async function waitForCondition(
+  condition: (...args: any[]) => boolean,
+  wakeUpOn: vscode.Event<any>,
+  timeoutMs: number,
+  timeoutErrorMsg: string,
+) {
+  let disposable: vscode.Disposable | undefined;
+  await new Promise<void>((resolve, reject) => {
+    let done = false;
+    setTimeout(() => {
+      if (!done) {
+        reject(new Error(timeoutErrorMsg));
+      }
+    }, timeoutMs);
+
+    disposable = wakeUpOn(() => {
+      if (!done && condition()) {
+        done = true;
+        resolve();
+      }
+    });
+
+    // Resolve immediately if condition is already met
+    if (condition()) {
+      done = true;
+      resolve();
+    }
+  });
+  disposable?.dispose();
+}
+
+/**
+ * Convenience method to add a time delay.
+ *
+ * @deprecated While this function is convenient for local development,
+ * please do NOT use it in actual tests. Instead, use `waitForCondition`
+ * with an appropriate condition and wake up event.
+ *
+ * Adding an unconditional delay to tests is guaranteed to slow tests down
+ * unnecessarily. It also reduces reliability when we're not clear about
+ * exactly what we're waiting on.
+ */
+export async function delay(timeoutMs: number) {
+  try {
+    await waitForCondition(
+      () => false,
+      () => ({ dispose() {} }),
+      timeoutMs,
+      "hit the expected timeout",
+    );
+  } catch (e) {
+    // expected
+  }
+}
diff --git a/vscode/test/suites/language-service/notebook.test.ts b/vscode/test/suites/language-service/notebook.test.ts
index 039b61f4..014c8d55 100644
--- a/vscode/test/suites/language-service/notebook.test.ts
+++ b/vscode/test/suites/language-service/notebook.test.ts
@@ -3,7 +3,7 @@
 
 import * as vscode from "vscode";
 import { assert } from "chai";
-import { activateExtension } from "../extensionUtils";
+import { activateExtension, waitForCondition } from "../extensionUtils";
 
 suite("Q# Notebook Tests", function suite() {
   const workspaceFolder =
@@ -23,40 +23,56 @@ suite("Q# Notebook Tests", function suite() {
 
     // Test if the cell with the %%qsharp magic has been detected
     // and its language switched to qsharp. We can only expect this to happen
-    // after the notebook has happened, and the document handlers
+    // after the notebook has been opened, and the document handlers
     // have been invoked, but of course there is no callback for that.
     // So we just verify the notebook has been updated with the qsharp
     // language id within 50ms (If we start exceeding this timeout for some
     // reason, it's enough of a user-perceptible delay that we're probably
     // better off disabling this behavior, rather than suddenly change the
     // cell language from under the user after a delay).
-    await new Promise<void>((resolve, reject) => {
-      let done = false;
-      setTimeout(() => {
-        if (!done) {
-          reject(new Error("timed out waiting for a Q# code cell"));
-        }
-      }, 50);
-
-      vscode.workspace.onDidChangeNotebookDocument((event) => {
-        if (!done && hasQSharpCell(event.notebook)) {
-          done = true;
-          resolve();
-        }
-      });
-
-      // in case the notebook updates have already occurred by the time we get here
-      if (hasQSharpCell(notebook)) {
-        done = true;
-        resolve();
-      }
-
-      function hasQSharpCell(notebook) {
-        return notebook
+    await waitForCondition(
+      () =>
+        !!notebook
           .getCells()
-          .find((cell) => cell.document.languageId === "qsharp");
-      }
-    });
+          .find((cell) => cell.document.languageId === "qsharp"),
+      vscode.workspace.onDidChangeNotebookDocument,
+      50,
+      "timed out waiting for a Q# code cell",
+    );
+  });
+
+  test("Cell language is set back to Python", async () => {
+    const notebook = await vscode.workspace.openNotebookDocument(
+      vscode.Uri.joinPath(workspaceFolderUri, "test.ipynb"),
+    );
+
+    await vscode.window.showNotebookDocument(notebook);
+
+    assert.equal(
+      vscode.window.activeNotebookEditor?.notebook.uri.toString(),
+      notebook.uri.toString(),
+    );
+
+    const oldLength = notebook.getCells().length;
+
+    // Add a new cell at the bottom of the notebook
+    await vscode.commands.executeCommand("notebook.focusBottom");
+    await vscode.commands.executeCommand("notebook.cell.insertCodeCellBelow");
+
+    // There should be an additional cell in the notebook and it should be Python
+    await waitForCondition(
+      () => {
+        const cellsAfter = notebook.getCells();
+        return (
+          cellsAfter.length === oldLength + 1 &&
+          notebook.getCells()[cellsAfter.length - 1].document.languageId ===
+            "python"
+        );
+      },
+      vscode.workspace.onDidChangeNotebookDocument,
+      50,
+      "timed out waiting for a Python code cell",
+    );
   });
 
   test("Diagnostics", async () => {