diff --git a/.ado/publish.yml b/.ado/publish.yml index 50d13d7d4c..440052ab29 100644 --- a/.ado/publish.yml +++ b/.ado/publish.yml @@ -34,13 +34,13 @@ parameters: default: - name: linux_x86_64 poolName: 'Azure-Pipelines-DevTools-EO' - imageName: 'ubuntu-20.04' + imageName: 'ubuntu-22.04' os: linux arch: x86_64 additionalTargets: wasm32-unknown-unknown - name: linux_aarch64 poolName: 'Azure-Pipelines-DevTools-EO' - imageName: 'ubuntu-20.04' + imageName: 'ubuntu-22.04' os: linux arch: aarch64 additionalRustTargets: aarch64-unknown-linux-gnu wasm32-unknown-unknown @@ -299,7 +299,7 @@ extends: python -m pip install auditwheel patchelf ls target/wheels ls target/wheels/*.whl | xargs auditwheel show - ls target/wheels/*.whl | xargs auditwheel repair --wheel-dir ./target/wheels/ --plat manylinux_2_31_x86_64 + ls target/wheels/*.whl | xargs auditwheel repair --wheel-dir ./target/wheels/ --plat manylinux_2_35_x86_64 rm target/wheels/*-linux_x86_64.whl ls target/wheels displayName: Run auditwheel for Linux Wheels @@ -313,10 +313,8 @@ extends: condition: and(eq(variables['Agent.OS'], 'Linux'), eq(variables['arch'], 'aarch64')) - script: | - chmod +x ./docker/linux-aarch64/build.sh chmod +x ./docker/linux-aarch64/run.sh - ./docker/linux-aarch64/build.sh ./docker/linux-aarch64/run.sh displayName: Run auditwheel and python tests for Linux aarch64 Wheels condition: and(eq(variables['Agent.OS'], 'Linux'), eq(variables['arch'], 'aarch64')) diff --git a/docker/linux-aarch64/Dockerfile b/docker/linux-aarch64/Dockerfile deleted file mode 100644 index 9da3e9cf58..0000000000 --- a/docker/linux-aarch64/Dockerfile +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -ARG BASE_IMAGE -FROM --platform=linux/arm64/v8 ${BASE_IMAGE} - -# install python and pip -RUN apt-get update && \ - DEBIAN_FRONTEND=noninteractive apt-get install \ - python3-minimal python3-pip python3-venv \ - --no-install-recommends -y && \ - apt-get clean - -# We don't update pip here as we need to update it -# inside the virtual environment. Otherwise, we get two versions -# of pip installed, and the one outside the virtual environment -# causes problems. - -ENTRYPOINT ["sh", "-c", "$*", "--"] diff --git a/docker/linux-aarch64/build.sh b/docker/linux-aarch64/build.sh deleted file mode 100644 index 5bc33ea207..0000000000 --- a/docker/linux-aarch64/build.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash - -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -set -e - -SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -echo "SCRIPT_DIR: ${SCRIPT_DIR}" - -BASE_IMAGE="${BASE_IMAGE:-ubuntu:20.04}" -echo "BASE_IMAGE: ${BASE_IMAGE}" - -TAG="${TAG:-qsharp-linux-aarch64-runner}" -echo "TAG: ${TAG}" - -docker build -t ${TAG} --build-arg BASE_IMAGE=${BASE_IMAGE} -f ${SCRIPT_DIR}/Dockerfile ${SCRIPT_DIR} diff --git a/docker/linux-aarch64/entrypoint.sh b/docker/linux-aarch64/entrypoint.sh index 0381036638..3a555b1188 100644 --- a/docker/linux-aarch64/entrypoint.sh +++ b/docker/linux-aarch64/entrypoint.sh @@ -11,7 +11,7 @@ echo "SCRIPT_DIR: ${SCRIPT_DIR}" WHEEL_ARCH="${WHEEL_ARCH:-aarch64}" echo "WHEEL_ARCH: ${WHEEL_ARCH}" -WHEEL_PLATFORM="${WHEEL_PLATFORM:-manylinux_2_31_${WHEEL_ARCH}}" +WHEEL_PLATFORM="${WHEEL_PLATFORM:-manylinux_2_35_${WHEEL_ARCH}}" echo "WHEEL_PLATFORM: ${WHEEL_PLATFORM}" PIP_DIR="${PIP_DIR:-${SCRIPT_DIR}/../../pip}" diff --git a/docker/linux-aarch64/run.sh b/docker/linux-aarch64/run.sh index 5387fba6fa..6b6302f9d9 100644 --- a/docker/linux-aarch64/run.sh +++ b/docker/linux-aarch64/run.sh @@ -8,11 +8,11 @@ set -e SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) echo "SCRIPT_DIR: ${SCRIPT_DIR}" -TAG="${TAG:-qsharp-linux-aarch64-runner}" -echo "TAG: ${TAG}" +BASE_IMAGE="${BASE_IMAGE:-mcr.microsoft.com/cbl-mariner/base/python:3.9}" +echo "BASE_IMAGE: ${BASE_IMAGE}" VOLUME_ROOT=$(realpath ${SCRIPT_DIR}/../..) echo "VOLUME_ROOT: ${VOLUME_ROOT}" -echo "docker run --platform linux/arm64/v8 -v ${VOLUME_ROOT}:/qsharp -e WHEEL_DIR='/qsharp/target/wheels' ${TAG} bash /qsharp/docker/linux-aarch64/entrypoint.sh" -docker run --platform linux/arm64/v8 -v ${VOLUME_ROOT}:/qsharp -e WHEEL_DIR='/qsharp/target/wheels' ${TAG} bash /qsharp/docker/linux-aarch64/entrypoint.sh +echo "docker run --platform linux/arm64/v8 -v ${VOLUME_ROOT}:/qsharp -e WHEEL_DIR='/qsharp/target/wheels' ${BASE_IMAGE} bash /qsharp/docker/linux-aarch64/entrypoint.sh" +docker run --platform linux/arm64/v8 -v ${VOLUME_ROOT}:/qsharp -e WHEEL_DIR='/qsharp/target/wheels' ${BASE_IMAGE} bash /qsharp/docker/linux-aarch64/entrypoint.sh diff --git a/fuzz/README.md b/fuzz/README.md index c010652b91..1c58cb80c0 100644 --- a/fuzz/README.md +++ b/fuzz/README.md @@ -4,7 +4,7 @@ Based on [Fuzzing with cargo-fuzz](https://rust-fuzz.github.io/book/cargo-fuzz.h For running locally you need the following steps. -(**On Windows use [WSL](https://learn.microsoft.com/windows/wsl/).** Tested in WSL Ubuntu 20.04) +(**On Windows use [WSL](https://learn.microsoft.com/windows/wsl/).** Tested in WSL Ubuntu 22.04) ## Prerequisites @@ -329,7 +329,7 @@ See more in [LibFuzzer Corpus](https://llvm.org/docs/LibFuzzer.html#corpus). Based on [Code Coverage](https://rust-fuzz.github.io/book/cargo-fuzz/coverage.html#code-coverage). -Tested in WSL Ubuntu 20.04. +Tested in WSL Ubuntu 22.04. ### Code Coverage Prerequisites diff --git a/jupyterlab/pyproject.toml b/jupyterlab/pyproject.toml index ae838c05d7..8ed1e5bc10 100644 --- a/jupyterlab/pyproject.toml +++ b/jupyterlab/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" name = "qsharp-jupyterlab" version = "0.0.0" readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.9" classifiers = [ "Framework :: Jupyter", "Framework :: Jupyter :: JupyterLab", @@ -16,10 +16,10 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ ] diff --git a/pip/Cargo.toml b/pip/Cargo.toml index e032dfcfd5..004ed6d939 100644 --- a/pip/Cargo.toml +++ b/pip/Cargo.toml @@ -27,12 +27,12 @@ workspace = true allocator = { path = "../allocator" } [target.'cfg(not(any(target_os = "windows")))'.dependencies] -pyo3 = { workspace = true, features = ["abi3-py38", "extension-module", "num-bigint", "num-complex"] } +pyo3 = { workspace = true, features = ["abi3-py39", "extension-module", "num-bigint", "num-complex"] } [target.'cfg(any(target_os = "windows"))'.dependencies] # generate-import-lib: skip requiring Python 3 distribution # files to be present on the (cross-)compile host system. -pyo3 = { workspace = true, features = ["abi3-py38", "extension-module", "generate-import-lib", "num-bigint", "num-complex"] } +pyo3 = { workspace = true, features = ["abi3-py39", "extension-module", "generate-import-lib", "num-bigint", "num-complex"] } [lib] crate-type = ["cdylib"] diff --git a/pip/pyproject.toml b/pip/pyproject.toml index 62b0393cab..0ca5d6c485 100644 --- a/pip/pyproject.toml +++ b/pip/pyproject.toml @@ -1,15 +1,16 @@ [project] name = "qsharp" version = "0.0.0" -requires-python = ">= 3.8" +requires-python = ">= 3.9" classifiers = [ "License :: OSI Approved :: MIT License", "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", - "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Programming Language :: Python", "Programming Language :: Rust", "Operating System :: MacOS", diff --git a/pip/qsharp/_native.pyi b/pip/qsharp/_native.pyi index 37a50b7f9b..328ccb0f01 100644 --- a/pip/qsharp/_native.pyi +++ b/pip/qsharp/_native.pyi @@ -287,6 +287,9 @@ class Output: def __str__(self) -> str: ... def _repr_markdown_(self) -> Optional[str]: ... def state_dump(self) -> Optional[StateDumpData]: ... + def is_state_dump(self) -> bool: ... + def is_matrix(self) -> bool: ... + def is_message(self) -> bool: ... class StateDumpData: """ diff --git a/pip/qsharp/_qsharp.py b/pip/qsharp/_qsharp.py index 24403bceff..198b52db37 100644 --- a/pip/qsharp/_qsharp.py +++ b/pip/qsharp/_qsharp.py @@ -242,18 +242,133 @@ def get_interpreter() -> Interpreter: return _interpreter -def eval(source: str) -> Any: +class StateDump: + """ + A state dump returned from the Q# interpreter. + """ + + """ + The number of allocated qubits at the time of the dump. + """ + qubit_count: int + + __inner: dict + __data: StateDumpData + + def __init__(self, data: StateDumpData): + self.__data = data + self.__inner = data.get_dict() + self.qubit_count = data.qubit_count + + def __getitem__(self, index: int) -> complex: + return self.__inner.__getitem__(index) + + def __iter__(self): + return self.__inner.__iter__() + + def __len__(self) -> int: + return len(self.__inner) + + def __repr__(self) -> str: + return self.__data.__repr__() + + def __str__(self) -> str: + return self.__data.__str__() + + def _repr_markdown_(self) -> str: + return self.__data._repr_markdown_() + + def check_eq( + self, state: Union[Dict[int, complex], List[complex]], tolerance: float = 1e-10 + ) -> bool: + """ + Checks if the state dump is equal to the given state. This is not mathematical equality, + as the check ignores global phase. + + :param state: The state to check against, provided either as a dictionary of state indices to complex amplitudes, + or as a list of real amplitudes. + :param tolerance: The tolerance for the check. Defaults to 1e-10. + """ + phase = None + # Convert a dense list of real amplitudes to a dictionary of state indices to complex amplitudes + if isinstance(state, list): + state = {i: val for i, val in enumerate(state)} + # Filter out zero states from the state dump and the given state based on tolerance + state = {k: v for k, v in state.items() if abs(v) > tolerance} + inner_state = {k: v for k, v in self.__inner.items() if abs(v) > tolerance} + if len(state) != len(inner_state): + return False + for key in state: + if key not in inner_state: + return False + if phase is None: + # Calculate the phase based on the first state pair encountered. + # Every pair of states after this must have the same phase for the states to be equivalent. + phase = inner_state[key] / state[key] + elif abs(phase - inner_state[key] / state[key]) > tolerance: + # This pair of states does not have the same phase, + # within tolerance, so the equivalence check fails. + return False + return True + + def as_dense_state(self) -> List[complex]: + """ + Returns the state dump as a dense list of complex amplitudes. This will include zero amplitudes. + """ + return [self.__inner.get(i, complex(0)) for i in range(2**self.qubit_count)] + + +class ShotResult(TypedDict): + """ + A single result of a shot. + """ + + events: List[Output] + result: Any + messages: List[str] + matrices: List[Output] + dumps: List[StateDump] + + +def eval( + source: str, + *, + save_events: bool = False, +) -> Any: """ Evaluates Q# source code. Output is printed to console. :param source: The Q# source code to evaluate. - :returns value: The value returned by the last statement in the source code. + :param save_events: If true, all output will be saved and returned. If false, they will be printed. + :returns value: The value returned by the last statement in the source code or the saved output if `save_events` is true. :raises QSharpError: If there is an error evaluating the source code. """ ipython_helper() + results: ShotResult = { + "events": [], + "result": None, + "messages": [], + "matrices": [], + "dumps": [], + } + + def on_save_events(output: Output) -> None: + # Append the output to the last shot's output list + if output.is_matrix(): + results["events"].append(output) + results["matrices"].append(output) + elif output.is_state_dump(): + state_dump = StateDump(output.state_dump()) + results["events"].append(state_dump) + results["dumps"].append(state_dump) + elif output.is_message(): + stringified = str(output) + results["events"].append(stringified) + results["messages"].append(stringified) + def callback(output: Output) -> None: if _in_jupyter: try: @@ -267,12 +382,17 @@ def callback(output: Output) -> None: telemetry_events.on_eval() start_time = monotonic() - results = get_interpreter().interpret(source, callback) + results["result"] = get_interpreter().interpret( + source, on_save_events if save_events else callback + ) durationMs = (monotonic() - start_time) * 1000 telemetry_events.on_eval_end(durationMs) - return results + if save_events: + return results + else: + return results["result"] # Helper function that knows how to create a function that invokes a callable. This will be @@ -325,15 +445,6 @@ def callback(output: Output) -> None: module.__setattr__(callable_name, _callable) -class ShotResult(TypedDict): - """ - A single result of a shot. - """ - - events: List[Output] - result: Any - - def run( entry_expr: str, shots: int, @@ -388,9 +499,17 @@ def print_output(output: Output) -> None: def on_save_events(output: Output) -> None: # Append the output to the last shot's output list results[-1]["events"].append(output) + if output.is_matrix(): + results[-1]["matrices"].append(output) + elif output.is_state_dump(): + results[-1]["dumps"].append(StateDump(output.state_dump())) + elif output.is_message(): + results[-1]["messages"].append(str(output)) for shot in range(shots): - results.append({"result": None, "events": []}) + results.append( + {"result": None, "events": [], "messages": [], "matrices": [], "dumps": []} + ) run_results = get_interpreter().run( entry_expr, on_save_events if save_events else print_output, @@ -555,82 +674,6 @@ def set_classical_seed(seed: Optional[int]) -> None: get_interpreter().set_classical_seed(seed) -class StateDump: - """ - A state dump returned from the Q# interpreter. - """ - - """ - The number of allocated qubits at the time of the dump. - """ - qubit_count: int - - __inner: dict - __data: StateDumpData - - def __init__(self, data: StateDumpData): - self.__data = data - self.__inner = data.get_dict() - self.qubit_count = data.qubit_count - - def __getitem__(self, index: int) -> complex: - return self.__inner.__getitem__(index) - - def __iter__(self): - return self.__inner.__iter__() - - def __len__(self) -> int: - return len(self.__inner) - - def __repr__(self) -> str: - return self.__data.__repr__() - - def __str__(self) -> str: - return self.__data.__str__() - - def _repr_markdown_(self) -> str: - return self.__data._repr_markdown_() - - def check_eq( - self, state: Union[Dict[int, complex], List[complex]], tolerance: float = 1e-10 - ) -> bool: - """ - Checks if the state dump is equal to the given state. This is not mathematical equality, - as the check ignores global phase. - - :param state: The state to check against, provided either as a dictionary of state indices to complex amplitudes, - or as a list of real amplitudes. - :param tolerance: The tolerance for the check. Defaults to 1e-10. - """ - phase = None - # Convert a dense list of real amplitudes to a dictionary of state indices to complex amplitudes - if isinstance(state, list): - state = {i: state[i] for i in range(len(state))} - # Filter out zero states from the state dump and the given state based on tolerance - state = {k: v for k, v in state.items() if abs(v) > tolerance} - inner_state = {k: v for k, v in self.__inner.items() if abs(v) > tolerance} - if len(state) != len(inner_state): - return False - for key in state: - if key not in inner_state: - return False - if phase is None: - # Calculate the phase based on the first state pair encountered. - # Every pair of states after this must have the same phase for the states to be equivalent. - phase = inner_state[key] / state[key] - elif abs(phase - inner_state[key] / state[key]) > tolerance: - # This pair of states does not have the same phase, - # within tolerance, so the equivalence check fails. - return False - return True - - def as_dense_state(self) -> List[complex]: - """ - Returns the state dump as a dense list of complex amplitudes. This will include zero amplitudes. - """ - return [self.__inner.get(i, complex(0)) for i in range(2**self.qubit_count)] - - def dump_machine() -> StateDump: """ Returns the sparse state vector of the simulator as a StateDump object. diff --git a/pip/src/interpreter.rs b/pip/src/interpreter.rs index e2a3a2f480..6f1c41a255 100644 --- a/pip/src/interpreter.rs +++ b/pip/src/interpreter.rs @@ -746,6 +746,18 @@ impl Output { DisplayableOutput::Matrix(_) | DisplayableOutput::Message(_) => None, } } + + fn is_state_dump(&self) -> bool { + matches!(&self.0, DisplayableOutput::State(_)) + } + + fn is_matrix(&self) -> bool { + matches!(&self.0, DisplayableOutput::Matrix(_)) + } + + fn is_message(&self) -> bool { + matches!(&self.0, DisplayableOutput::Message(_)) + } } #[pyclass] diff --git a/pip/tests/test_qsharp.py b/pip/tests/test_qsharp.py index 880927adfe..f79541b070 100644 --- a/pip/tests/test_qsharp.py +++ b/pip/tests/test_qsharp.py @@ -36,6 +36,35 @@ def test_stdout_multiple_lines() -> None: assert f.getvalue() == "STATE:\n|0⟩: 1.0000+0.0000𝑖\nHello!\n" +def test_captured_stdout() -> None: + qsharp.init(target_profile=qsharp.TargetProfile.Unrestricted) + f = io.StringIO() + with redirect_stdout(f): + result = qsharp.eval( + '{Message("Hello, world!"); Message("Goodbye!")}', save_events=True + ) + assert f.getvalue() == "" + assert len(result["messages"]) == 2 + assert result["messages"][0] == "Hello, world!" + assert result["messages"][1] == "Goodbye!" + + +def test_captured_matrix() -> None: + qsharp.init(target_profile=qsharp.TargetProfile.Unrestricted) + f = io.StringIO() + with redirect_stdout(f): + result = qsharp.eval( + "Std.Diagnostics.DumpOperation(1, qs => H(qs[0]))", + save_events=True, + ) + assert f.getvalue() == "" + assert len(result["matrices"]) == 1 + assert ( + str(result["matrices"][0]) + == "MATRIX:\n 0.7071+0.0000𝑖 0.7071+0.0000𝑖\n 0.7071+0.0000𝑖 −0.7071+0.0000𝑖" + ) + + def test_quantum_seed() -> None: qsharp.init(target_profile=qsharp.TargetProfile.Unrestricted) qsharp.set_quantum_seed(42) @@ -327,7 +356,7 @@ def on_result(result): results = qsharp.run("Foo()", 3, on_result=on_result, save_events=True) assert ( str(results) - == "[{'result': Zero, 'events': [Hello, world!]}, {'result': Zero, 'events': [Hello, world!]}, {'result': Zero, 'events': [Hello, world!]}]" + == "[{'result': Zero, 'events': [Hello, world!], 'messages': ['Hello, world!'], 'matrices': [], 'dumps': []}, {'result': Zero, 'events': [Hello, world!], 'messages': ['Hello, world!'], 'matrices': [], 'dumps': []}, {'result': Zero, 'events': [Hello, world!], 'messages': ['Hello, world!'], 'matrices': [], 'dumps': []}]" ) stdout = capsys.readouterr().out assert stdout == "" diff --git a/vscode/src/azure/providerProperties.ts b/vscode/src/azure/providerProperties.ts index e8b2176279..81f7191e9d 100644 --- a/vscode/src/azure/providerProperties.ts +++ b/vscode/src/azure/providerProperties.ts @@ -13,9 +13,8 @@ export function targetSupportQir(target: string) { // Note: Most of these should be dynamic at some point, with configuration coming // from the service, and able to be overridden by settings. return ( - target.startsWith("ionq") || - target.startsWith("quantinuum") || - target.startsWith("rigetti") + !(target == "microsoft.estimator") && + !(target.startsWith("microsoft") && target.endsWith("cpu")) ); } @@ -28,5 +27,5 @@ export function shouldExcludeProvider(provider: string) { } export function supportsAdaptive(target: string) { - return target.startsWith("quantinuum"); + return !target.startsWith("ionq") && !target.startsWith("rigetti"); } diff --git a/vscode/src/common.ts b/vscode/src/common.ts index ea6da48537..71de7e08b6 100644 --- a/vscode/src/common.ts +++ b/vscode/src/common.ts @@ -3,7 +3,7 @@ import { TextDocument, Uri, Range, Location } from "vscode"; import { Utils } from "vscode-uri"; -import { ILocation, IRange, IWorkspaceEdit } from "qsharp-lang"; +import { ILocation, IRange, IWorkspaceEdit, VSDiagnostic } from "qsharp-lang"; import * as vscode from "vscode"; export const qsharpLanguageId = "qsharp"; @@ -58,3 +58,40 @@ export function toVscodeWorkspaceEdit( } return workspaceEdit; } + +export function toVsCodeDiagnostic(d: VSDiagnostic): vscode.Diagnostic { + let severity; + switch (d.severity) { + case "error": + severity = vscode.DiagnosticSeverity.Error; + break; + case "warning": + severity = vscode.DiagnosticSeverity.Warning; + break; + case "info": + severity = vscode.DiagnosticSeverity.Information; + break; + } + const vscodeDiagnostic = new vscode.Diagnostic( + toVscodeRange(d.range), + d.message, + severity, + ); + if (d.uri && d.code) { + vscodeDiagnostic.code = { + value: d.code, + target: vscode.Uri.parse(d.uri), + }; + } else if (d.code) { + vscodeDiagnostic.code = d.code; + } + if (d.related) { + vscodeDiagnostic.relatedInformation = d.related.map((r) => { + return new vscode.DiagnosticRelatedInformation( + toVscodeLocation(r.location), + r.message, + ); + }); + } + return vscodeDiagnostic; +} diff --git a/vscode/src/diagnostics.ts b/vscode/src/diagnostics.ts index 3a4dd2503d..e0fd154c86 100644 --- a/vscode/src/diagnostics.ts +++ b/vscode/src/diagnostics.ts @@ -1,100 +1,20 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { - ILanguageService, - IQSharpError, - VSDiagnostic, - log, - qsharpLibraryUriScheme, -} from "qsharp-lang"; +import { IQSharpError, log, qsharpLibraryUriScheme } from "qsharp-lang"; import * as vscode from "vscode"; -import { toVscodeLocation, toVscodeRange, qsharpLanguageId } from "./common.js"; +import { qsharpLanguageId, toVsCodeDiagnostic } from "./common.js"; -export function startCheckingQSharp( - languageService: ILanguageService, -): vscode.Disposable[] { - return [ - ...startLanguageServiceDiagnostics(languageService), - ...startQsharpJsonDiagnostics(), - ...startCommandDiagnostics(), - ]; -} - -function startLanguageServiceDiagnostics( - languageService: ILanguageService, -): vscode.Disposable[] { - const diagCollection = - vscode.languages.createDiagnosticCollection(qsharpLanguageId); - - async function onDiagnostics(evt: { - detail: { - uri: string; - version: number; - diagnostics: VSDiagnostic[]; - }; - }) { - const diagnostics = evt.detail; - const uri = vscode.Uri.parse(diagnostics.uri); - - if (uri.scheme === qsharpLibraryUriScheme) { - // Don't report diagnostics for library files. - return; - } - - diagCollection.set( - uri, - diagnostics.diagnostics.map((d) => toVsCodeDiagnostic(d)), - ); - } - - languageService.addEventListener("diagnostics", onDiagnostics); - - return [ - { - dispose: () => { - languageService.removeEventListener("diagnostics", onDiagnostics); - }, - }, - diagCollection, - ]; -} - -export function toVsCodeDiagnostic(d: VSDiagnostic): vscode.Diagnostic { - let severity; - switch (d.severity) { - case "error": - severity = vscode.DiagnosticSeverity.Error; - break; - case "warning": - severity = vscode.DiagnosticSeverity.Warning; - break; - case "info": - severity = vscode.DiagnosticSeverity.Information; - break; - } - const vscodeDiagnostic = new vscode.Diagnostic( - toVscodeRange(d.range), - d.message, - severity, - ); - if (d.uri && d.code) { - vscodeDiagnostic.code = { - value: d.code, - target: vscode.Uri.parse(d.uri), - }; - } else if (d.code) { - vscodeDiagnostic.code = d.code; - } - if (d.related) { - vscodeDiagnostic.relatedInformation = d.related.map((r) => { - return new vscode.DiagnosticRelatedInformation( - toVscodeLocation(r.location), - r.message, - ); - }); - } - return vscodeDiagnostic; +/** + * Initialize diagnostics for `qsharp.json` files and failures + * that get reported from various Q# commands. + * + * These are distinct from the errors reported by the Q# language + * service, (a.k.a. compiler errors that get reported as you type). + * Those are initialized in `language-service/diagnostics.js` + */ +export function startOtherQSharpDiagnostics(): vscode.Disposable[] { + return [...startQsharpJsonDiagnostics(), ...startCommandDiagnostics()]; } // diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts index a0061bbc17..750700f373 100644 --- a/vscode/src/extension.ts +++ b/vscode/src/extension.ts @@ -2,31 +2,17 @@ // Licensed under the MIT License. import { - ILanguageService, - getLanguageService, getLibrarySourceContent, - loadWasmModule, log, qsharpGithubUriScheme, qsharpLibraryUriScheme, } from "qsharp-lang"; import * as vscode from "vscode"; import { initAzureWorkspaces } from "./azure/commands.js"; -import { createCodeActionsProvider } from "./codeActions.js"; -import { createCodeLensProvider } from "./codeLens.js"; -import { - isQsharpDocument, - isQsharpNotebookCell, - qsharpLanguageId, -} from "./common.js"; -import { createCompletionItemProvider } from "./completion"; -import { getTarget } from "./config"; import { initProjectCreator } from "./createProject.js"; -import { activateDebugger } from "./debugger/activate"; -import { createDefinitionProvider } from "./definition"; -import { startCheckingQSharp } from "./diagnostics"; -import { createFormattingProvider } from "./format.js"; -import { createHoverProvider } from "./hover"; +import { activateDebugger } from "./debugger/activate.js"; +import { startOtherQSharpDiagnostics } from "./diagnostics.js"; +import { activateLanguageService } from "./language-service/activate.js"; import { Logging, initLogForwarder, @@ -35,29 +21,12 @@ import { import { initFileSystem } from "./memfs.js"; import { registerCreateNotebookCommand, - registerQSharpNotebookCellUpdateHandlers, registerQSharpNotebookHandlers, } from "./notebook.js"; -import { - fetchGithubRaw, - findManifestDirectory, - getGithubSourceContent, - listDirectory, - readFile, - resolvePath, - setGithubEndpoint, -} from "./projectSystem.js"; +import { getGithubSourceContent, setGithubEndpoint } from "./projectSystem.js"; import { initCodegen } from "./qirGeneration.js"; -import { createReferenceProvider } from "./references.js"; -import { createRenameProvider } from "./rename.js"; -import { createSignatureHelpProvider } from "./signature.js"; import { activateTargetProfileStatusBarItem } from "./statusbar.js"; -import { - EventType, - QsharpDocumentType, - initTelemetry, - sendTelemetryEvent, -} from "./telemetry.js"; +import { initTelemetry } from "./telemetry.js"; import { registerWebViewCommands } from "./webviewPanel.js"; export async function activate( @@ -102,6 +71,8 @@ export async function activate( ...(await activateLanguageService(context.extensionUri)), ); + context.subscriptions.push(...startOtherQSharpDiagnostics()); + context.subscriptions.push(...registerQSharpNotebookHandlers()); initAzureWorkspaces(context); @@ -123,270 +94,6 @@ export interface ExtensionApi { setGithubEndpoint: (endpoint: string) => void; } -function registerDocumentUpdateHandlers(languageService: ILanguageService) { - vscode.workspace.textDocuments.forEach((document) => { - updateIfQsharpDocument(document); - }); - - // we manually send an OpenDocument telemetry event if this is a Q# document, because the - // below subscriptions won't fire for documents that are already open when the extension is activated - vscode.workspace.textDocuments.forEach((document) => { - if (isQsharpDocument(document)) { - const documentType = isQsharpNotebookCell(document) - ? QsharpDocumentType.JupyterCell - : QsharpDocumentType.Qsharp; - sendTelemetryEvent( - EventType.OpenedDocument, - { documentType }, - { linesOfCode: document.lineCount }, - ); - } - }); - - const subscriptions = []; - subscriptions.push( - vscode.workspace.onDidOpenTextDocument((document) => { - const documentType = isQsharpNotebookCell(document) - ? QsharpDocumentType.JupyterCell - : isQsharpDocument(document) - ? QsharpDocumentType.Qsharp - : QsharpDocumentType.Other; - if (documentType !== QsharpDocumentType.Other) { - sendTelemetryEvent( - EventType.OpenedDocument, - { documentType }, - { linesOfCode: document.lineCount }, - ); - } - updateIfQsharpDocument(document); - }), - ); - - subscriptions.push( - vscode.workspace.onDidChangeTextDocument((evt) => { - updateIfQsharpDocument(evt.document); - }), - ); - - subscriptions.push( - vscode.workspace.onDidCloseTextDocument((document) => { - if (isQsharpDocument(document) && !isQsharpNotebookCell(document)) { - languageService.closeDocument(document.uri.toString()); - } - }), - ); - - // Watch manifest changes and update each document in the same project as the manifest. - subscriptions.push( - vscode.workspace.onDidSaveTextDocument((manifest) => { - updateProjectDocuments(manifest.uri); - }), - ); - - // Trigger an update on all .qs child documents when their manifest is deleted, - // so that they can get reparented to single-file-projects. - subscriptions.push( - vscode.workspace.onDidDeleteFiles((event) => { - event.files.forEach((uri) => { - updateProjectDocuments(uri); - }); - }), - ); - - // Checks if the URI belongs to a qsharp manifest, and updates all - // open documents in the same project as the manifest. - function updateProjectDocuments(manifest: vscode.Uri) { - if (manifest.scheme === "file" && manifest.fsPath.endsWith("qsharp.json")) { - const project_folder = manifest.fsPath.slice( - 0, - manifest.fsPath.length - "qsharp.json".length, - ); - vscode.workspace.textDocuments.forEach((document) => { - if ( - !document.isClosed && - // Check that the document is on the same project as the manifest. - document.fileName.startsWith(project_folder) - ) { - updateIfQsharpDocument(document); - } - }); - } - } - - function updateIfQsharpDocument(document: vscode.TextDocument) { - if (isQsharpDocument(document) && !isQsharpNotebookCell(document)) { - // Regular (not notebook) Q# document. - languageService.updateDocument( - document.uri.toString(), - document.version, - document.getText(), - ); - } - } - - return subscriptions; -} - -async function activateLanguageService(extensionUri: vscode.Uri) { - const subscriptions: vscode.Disposable[] = []; - - const languageService = await loadLanguageService(extensionUri); - - // diagnostics - subscriptions.push(...startCheckingQSharp(languageService)); - - // synchronize document contents - subscriptions.push(...registerDocumentUpdateHandlers(languageService)); - - // synchronize notebook cell contents - subscriptions.push( - ...registerQSharpNotebookCellUpdateHandlers(languageService), - ); - - // synchronize configuration - subscriptions.push(registerConfigurationChangeHandlers(languageService)); - - // format document - subscriptions.push( - vscode.languages.registerDocumentFormattingEditProvider( - qsharpLanguageId, - createFormattingProvider(languageService), - ), - ); - - // format range - subscriptions.push( - vscode.languages.registerDocumentRangeFormattingEditProvider( - qsharpLanguageId, - createFormattingProvider(languageService), - ), - ); - - // completions - subscriptions.push( - vscode.languages.registerCompletionItemProvider( - qsharpLanguageId, - createCompletionItemProvider(languageService), - // Trigger characters should be kept in sync with the ones in `playground/src/main.tsx` - "@", - ".", - ), - ); - - // hover - subscriptions.push( - vscode.languages.registerHoverProvider( - qsharpLanguageId, - createHoverProvider(languageService), - ), - ); - - // go to def - subscriptions.push( - vscode.languages.registerDefinitionProvider( - qsharpLanguageId, - createDefinitionProvider(languageService), - ), - ); - - // find references - subscriptions.push( - vscode.languages.registerReferenceProvider( - qsharpLanguageId, - createReferenceProvider(languageService), - ), - ); - - // signature help - subscriptions.push( - vscode.languages.registerSignatureHelpProvider( - qsharpLanguageId, - createSignatureHelpProvider(languageService), - "(", - ",", - ), - ); - - // rename symbol - subscriptions.push( - vscode.languages.registerRenameProvider( - qsharpLanguageId, - createRenameProvider(languageService), - ), - ); - - // code lens - subscriptions.push( - vscode.languages.registerCodeLensProvider( - qsharpLanguageId, - createCodeLensProvider(languageService), - ), - ); - - subscriptions.push( - vscode.languages.registerCodeActionsProvider( - qsharpLanguageId, - createCodeActionsProvider(languageService), - ), - ); - - // add the language service dispose handler as well - subscriptions.push(languageService); - - return subscriptions; -} - -async function updateLanguageServiceProfile(languageService: ILanguageService) { - const targetProfile = getTarget(); - - switch (targetProfile) { - case "base": - case "adaptive_ri": - case "unrestricted": - break; - default: - log.warn(`Invalid value for target profile: ${targetProfile}`); - } - log.debug("Target profile set to: " + targetProfile); - - languageService.updateConfiguration({ - targetProfile: targetProfile, - lints: [{ lint: "needlessOperation", level: "warn" }], - }); -} - -async function loadLanguageService(baseUri: vscode.Uri) { - const start = performance.now(); - const wasmUri = vscode.Uri.joinPath(baseUri, "./wasm/qsc_wasm_bg.wasm"); - const wasmBytes = await vscode.workspace.fs.readFile(wasmUri); - await loadWasmModule(wasmBytes); - const languageService = await getLanguageService({ - findManifestDirectory, - readFile, - listDirectory, - resolvePath: async (a, b) => resolvePath(a, b), - fetchGithub: fetchGithubRaw, - }); - await updateLanguageServiceProfile(languageService); - const end = performance.now(); - sendTelemetryEvent( - EventType.LoadLanguageService, - {}, - { timeToStartMs: end - start }, - ); - return languageService; -} - -function registerConfigurationChangeHandlers( - languageService: ILanguageService, -) { - return vscode.workspace.onDidChangeConfiguration((event) => { - if (event.affectsConfiguration("Q#.qir.targetProfile")) { - updateLanguageServiceProfile(languageService); - } - }); -} - export class QsTextDocumentContentProvider implements vscode.TextDocumentContentProvider { diff --git a/vscode/src/language-service/activate.ts b/vscode/src/language-service/activate.ts new file mode 100644 index 0000000000..d4815d8713 --- /dev/null +++ b/vscode/src/language-service/activate.ts @@ -0,0 +1,302 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { + ILanguageService, + getLanguageService, + loadWasmModule, + log, +} from "qsharp-lang"; +import * as vscode from "vscode"; +import { + isQsharpDocument, + isQsharpNotebookCell, + qsharpLanguageId, +} from "../common.js"; +import { getTarget } from "../config.js"; +import { + fetchGithubRaw, + findManifestDirectory, + listDirectory, + readFile, + resolvePath, +} from "../projectSystem.js"; +import { + EventType, + QsharpDocumentType, + sendTelemetryEvent, +} from "../telemetry.js"; +import { createCodeActionsProvider } from "./codeActions.js"; +import { createCodeLensProvider } from "./codeLens.js"; +import { createCompletionItemProvider } from "./completion.js"; +import { createDefinitionProvider } from "./definition.js"; +import { startLanguageServiceDiagnostics } from "./diagnostics.js"; +import { createFormattingProvider } from "./format.js"; +import { createHoverProvider } from "./hover.js"; +import { registerQSharpNotebookCellUpdateHandlers } from "./notebook.js"; +import { createReferenceProvider } from "./references.js"; +import { createRenameProvider } from "./rename.js"; +import { createSignatureHelpProvider } from "./signature.js"; + +export async function activateLanguageService(extensionUri: vscode.Uri) { + const subscriptions: vscode.Disposable[] = []; + + const languageService = await loadLanguageService(extensionUri); + + // diagnostics + subscriptions.push(...startLanguageServiceDiagnostics(languageService)); + + // synchronize document contents + subscriptions.push(...registerDocumentUpdateHandlers(languageService)); + + // synchronize notebook cell contents + subscriptions.push( + ...registerQSharpNotebookCellUpdateHandlers(languageService), + ); + + // synchronize configuration + subscriptions.push(registerConfigurationChangeHandlers(languageService)); + + // format document + subscriptions.push( + vscode.languages.registerDocumentFormattingEditProvider( + qsharpLanguageId, + createFormattingProvider(languageService), + ), + ); + + // format range + subscriptions.push( + vscode.languages.registerDocumentRangeFormattingEditProvider( + qsharpLanguageId, + createFormattingProvider(languageService), + ), + ); + + // completions + subscriptions.push( + vscode.languages.registerCompletionItemProvider( + qsharpLanguageId, + createCompletionItemProvider(languageService), + // Trigger characters should be kept in sync with the ones in `playground/src/main.tsx` + "@", + ".", + ), + ); + + // hover + subscriptions.push( + vscode.languages.registerHoverProvider( + qsharpLanguageId, + createHoverProvider(languageService), + ), + ); + + // go to def + subscriptions.push( + vscode.languages.registerDefinitionProvider( + qsharpLanguageId, + createDefinitionProvider(languageService), + ), + ); + + // find references + subscriptions.push( + vscode.languages.registerReferenceProvider( + qsharpLanguageId, + createReferenceProvider(languageService), + ), + ); + + // signature help + subscriptions.push( + vscode.languages.registerSignatureHelpProvider( + qsharpLanguageId, + createSignatureHelpProvider(languageService), + "(", + ",", + ), + ); + + // rename symbol + subscriptions.push( + vscode.languages.registerRenameProvider( + qsharpLanguageId, + createRenameProvider(languageService), + ), + ); + + // code lens + subscriptions.push( + vscode.languages.registerCodeLensProvider( + qsharpLanguageId, + createCodeLensProvider(languageService), + ), + ); + + subscriptions.push( + vscode.languages.registerCodeActionsProvider( + qsharpLanguageId, + createCodeActionsProvider(languageService), + ), + ); + + // add the language service dispose handler as well + subscriptions.push(languageService); + + return subscriptions; +} + +async function loadLanguageService(baseUri: vscode.Uri) { + const start = performance.now(); + const wasmUri = vscode.Uri.joinPath(baseUri, "./wasm/qsc_wasm_bg.wasm"); + const wasmBytes = await vscode.workspace.fs.readFile(wasmUri); + await loadWasmModule(wasmBytes); + const languageService = await getLanguageService({ + findManifestDirectory, + readFile, + listDirectory, + resolvePath: async (a, b) => resolvePath(a, b), + fetchGithub: fetchGithubRaw, + }); + await updateLanguageServiceProfile(languageService); + const end = performance.now(); + sendTelemetryEvent( + EventType.LoadLanguageService, + {}, + { timeToStartMs: end - start }, + ); + return languageService; +} +function registerDocumentUpdateHandlers(languageService: ILanguageService) { + vscode.workspace.textDocuments.forEach((document) => { + updateIfQsharpDocument(document); + }); + + // we manually send an OpenDocument telemetry event if this is a Q# document, because the + // below subscriptions won't fire for documents that are already open when the extension is activated + vscode.workspace.textDocuments.forEach((document) => { + if (isQsharpDocument(document)) { + const documentType = isQsharpNotebookCell(document) + ? QsharpDocumentType.JupyterCell + : QsharpDocumentType.Qsharp; + sendTelemetryEvent( + EventType.OpenedDocument, + { documentType }, + { linesOfCode: document.lineCount }, + ); + } + }); + + const subscriptions = []; + subscriptions.push( + vscode.workspace.onDidOpenTextDocument((document) => { + const documentType = isQsharpNotebookCell(document) + ? QsharpDocumentType.JupyterCell + : isQsharpDocument(document) + ? QsharpDocumentType.Qsharp + : QsharpDocumentType.Other; + if (documentType !== QsharpDocumentType.Other) { + sendTelemetryEvent( + EventType.OpenedDocument, + { documentType }, + { linesOfCode: document.lineCount }, + ); + } + updateIfQsharpDocument(document); + }), + ); + + subscriptions.push( + vscode.workspace.onDidChangeTextDocument((evt) => { + updateIfQsharpDocument(evt.document); + }), + ); + + subscriptions.push( + vscode.workspace.onDidCloseTextDocument((document) => { + if (isQsharpDocument(document) && !isQsharpNotebookCell(document)) { + languageService.closeDocument(document.uri.toString()); + } + }), + ); + + // Watch manifest changes and update each document in the same project as the manifest. + subscriptions.push( + vscode.workspace.onDidSaveTextDocument((manifest) => { + updateProjectDocuments(manifest.uri); + }), + ); + + // Trigger an update on all .qs child documents when their manifest is deleted, + // so that they can get reparented to single-file-projects. + subscriptions.push( + vscode.workspace.onDidDeleteFiles((event) => { + event.files.forEach((uri) => { + updateProjectDocuments(uri); + }); + }), + ); + + // Checks if the URI belongs to a qsharp manifest, and updates all + // open documents in the same project as the manifest. + function updateProjectDocuments(manifest: vscode.Uri) { + if (manifest.scheme === "file" && manifest.fsPath.endsWith("qsharp.json")) { + const project_folder = manifest.fsPath.slice( + 0, + manifest.fsPath.length - "qsharp.json".length, + ); + vscode.workspace.textDocuments.forEach((document) => { + if ( + !document.isClosed && + // Check that the document is on the same project as the manifest. + document.fileName.startsWith(project_folder) + ) { + updateIfQsharpDocument(document); + } + }); + } + } + + function updateIfQsharpDocument(document: vscode.TextDocument) { + if (isQsharpDocument(document) && !isQsharpNotebookCell(document)) { + // Regular (not notebook) Q# document. + languageService.updateDocument( + document.uri.toString(), + document.version, + document.getText(), + ); + } + } + + return subscriptions; +} + +function registerConfigurationChangeHandlers( + languageService: ILanguageService, +) { + return vscode.workspace.onDidChangeConfiguration((event) => { + if (event.affectsConfiguration("Q#.qir.targetProfile")) { + updateLanguageServiceProfile(languageService); + } + }); +} + +async function updateLanguageServiceProfile(languageService: ILanguageService) { + const targetProfile = getTarget(); + + switch (targetProfile) { + case "base": + case "adaptive_ri": + case "unrestricted": + break; + default: + log.warn(`Invalid value for target profile: ${targetProfile}`); + } + log.debug("Target profile set to: " + targetProfile); + + languageService.updateConfiguration({ + targetProfile: targetProfile, + lints: [{ lint: "needlessOperation", level: "warn" }], + }); +} diff --git a/vscode/src/codeActions.ts b/vscode/src/language-service/codeActions.ts similarity index 97% rename from vscode/src/codeActions.ts rename to vscode/src/language-service/codeActions.ts index 03fb7869c7..513f28fe88 100644 --- a/vscode/src/codeActions.ts +++ b/vscode/src/language-service/codeActions.ts @@ -3,7 +3,7 @@ import { ILanguageService, ICodeAction } from "qsharp-lang"; import * as vscode from "vscode"; -import { toVscodeWorkspaceEdit } from "./common"; +import { toVscodeWorkspaceEdit } from "../common"; export function createCodeActionsProvider(languageService: ILanguageService) { return new QSharpCodeActionProvider(languageService); diff --git a/vscode/src/codeLens.ts b/vscode/src/language-service/codeLens.ts similarity index 98% rename from vscode/src/codeLens.ts rename to vscode/src/language-service/codeLens.ts index 7790628105..98672811cb 100644 --- a/vscode/src/codeLens.ts +++ b/vscode/src/language-service/codeLens.ts @@ -7,7 +7,7 @@ import { qsharpLibraryUriScheme, } from "qsharp-lang"; import * as vscode from "vscode"; -import { toVscodeRange } from "./common"; +import { toVscodeRange } from "../common"; export function createCodeLensProvider(languageService: ILanguageService) { return new QSharpCodeLensProvider(languageService); diff --git a/vscode/src/completion.ts b/vscode/src/language-service/completion.ts similarity index 96% rename from vscode/src/completion.ts rename to vscode/src/language-service/completion.ts index 6526962c0b..92f2fc8bc8 100644 --- a/vscode/src/completion.ts +++ b/vscode/src/language-service/completion.ts @@ -4,8 +4,8 @@ import { ILanguageService, samples } from "qsharp-lang"; import * as vscode from "vscode"; import { CompletionItem } from "vscode"; -import { EventType, sendTelemetryEvent } from "./telemetry"; -import { toVscodeRange } from "./common"; +import { toVscodeRange } from "../common"; +import { EventType, sendTelemetryEvent } from "../telemetry"; export function createCompletionItemProvider( languageService: ILanguageService, diff --git a/vscode/src/definition.ts b/vscode/src/language-service/definition.ts similarity index 94% rename from vscode/src/definition.ts rename to vscode/src/language-service/definition.ts index 0800224b4a..fb2f6a6a23 100644 --- a/vscode/src/definition.ts +++ b/vscode/src/language-service/definition.ts @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { toVscodeLocation } from "./common"; import { ILanguageService } from "qsharp-lang"; import * as vscode from "vscode"; +import { toVscodeLocation } from "../common"; export function createDefinitionProvider(languageService: ILanguageService) { return new QSharpDefinitionProvider(languageService); diff --git a/vscode/src/language-service/diagnostics.ts b/vscode/src/language-service/diagnostics.ts new file mode 100644 index 0000000000..cbd0e9ac1d --- /dev/null +++ b/vscode/src/language-service/diagnostics.ts @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { + ILanguageService, + VSDiagnostic, + qsharpLibraryUriScheme, +} from "qsharp-lang"; +import * as vscode from "vscode"; +import { qsharpLanguageId, toVsCodeDiagnostic } from "../common"; + +export function startLanguageServiceDiagnostics( + languageService: ILanguageService, +): vscode.Disposable[] { + const diagCollection = + vscode.languages.createDiagnosticCollection(qsharpLanguageId); + + async function onDiagnostics(evt: { + detail: { + uri: string; + version: number; + diagnostics: VSDiagnostic[]; + }; + }) { + const diagnostics = evt.detail; + const uri = vscode.Uri.parse(diagnostics.uri); + + if (uri.scheme === qsharpLibraryUriScheme) { + // Don't report diagnostics for library files. + return; + } + + diagCollection.set( + uri, + diagnostics.diagnostics.map((d) => toVsCodeDiagnostic(d)), + ); + } + + languageService.addEventListener("diagnostics", onDiagnostics); + + return [ + { + dispose: () => { + languageService.removeEventListener("diagnostics", onDiagnostics); + }, + }, + diagCollection, + ]; +} diff --git a/vscode/src/format.ts b/vscode/src/language-service/format.ts similarity index 92% rename from vscode/src/format.ts rename to vscode/src/language-service/format.ts index 71765f7a89..fb9275dfd5 100644 --- a/vscode/src/format.ts +++ b/vscode/src/language-service/format.ts @@ -3,9 +3,9 @@ import { ILanguageService } from "qsharp-lang"; import * as vscode from "vscode"; -import { toVscodeRange } from "./common"; -import { EventType, FormatEvent, sendTelemetryEvent } from "./telemetry"; -import { getRandomGuid } from "./utils"; +import { toVscodeRange } from "../common"; +import { EventType, FormatEvent, sendTelemetryEvent } from "../telemetry"; +import { getRandomGuid } from "../utils"; export function createFormattingProvider(languageService: ILanguageService) { return new QSharpFormattingProvider(languageService); diff --git a/vscode/src/hover.ts b/vscode/src/language-service/hover.ts similarity index 94% rename from vscode/src/hover.ts rename to vscode/src/language-service/hover.ts index 6cab20afad..4307174099 100644 --- a/vscode/src/hover.ts +++ b/vscode/src/language-service/hover.ts @@ -3,7 +3,7 @@ import { ILanguageService } from "qsharp-lang"; import * as vscode from "vscode"; -import { toVscodeRange } from "./common"; +import { toVscodeRange } from "../common"; export function createHoverProvider(languageService: ILanguageService) { return new QSharpHoverProvider(languageService); diff --git a/vscode/src/language-service/notebook.ts b/vscode/src/language-service/notebook.ts new file mode 100644 index 0000000000..c17e542501 --- /dev/null +++ b/vscode/src/language-service/notebook.ts @@ -0,0 +1,135 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { ILanguageService, log } from "qsharp-lang"; +import * as vscode from "vscode"; +import { isQsharpNotebookCell } from "../common.js"; +import { findQSharpCellMagic, jupyterNotebookType } from "../notebook.js"; + +const qsharpConfigMimeType = "application/x.qsharp-config"; + +const openQSharpNotebooks = new Set(); + +/** + * Document update handlers for syncing notebook cell contents with the language service. + */ +export function registerQSharpNotebookCellUpdateHandlers( + languageService: ILanguageService, +) { + vscode.workspace.notebookDocuments.forEach((notebook) => { + updateIfQsharpNotebook(notebook); + }); + + const subscriptions = []; + subscriptions.push( + vscode.workspace.onDidOpenNotebookDocument((notebook) => { + updateIfQsharpNotebook(notebook); + }), + ); + + subscriptions.push( + vscode.workspace.onDidChangeNotebookDocument((event) => { + updateIfQsharpNotebook(event.notebook); + }), + ); + + subscriptions.push( + vscode.workspace.onDidCloseNotebookDocument((notebook) => { + closeIfKnownQsharpNotebook(notebook); + }), + ); + + function updateIfQsharpNotebook(notebook: vscode.NotebookDocument) { + if (notebook.notebookType === jupyterNotebookType) { + const qsharpMetadata = getQSharpConfigMetadata(notebook); + const qsharpCells = getQSharpCells(notebook); + const notebookUri = notebook.uri.toString(); + if (qsharpCells.length > 0) { + openQSharpNotebooks.add(notebookUri); + languageService.updateNotebookDocument( + notebookUri, + notebook.version, + qsharpMetadata, + qsharpCells.map((cell) => { + return { + uri: cell.document.uri.toString(), + version: cell.document.version, + code: getQSharpText(cell.document), + }; + }), + ); + } else { + // All Q# cells could have been deleted, check if we know this doc from previous calls + closeIfKnownQsharpNotebook(notebook); + } + } + } + + function closeIfKnownQsharpNotebook(notebook: vscode.NotebookDocument) { + const notebookUri = notebook.uri.toString(); + if (openQSharpNotebooks.has(notebookUri)) { + languageService.closeNotebookDocument(notebookUri); + openQSharpNotebooks.delete(notebook.uri.toString()); + } + } + + function getQSharpCells(notebook: vscode.NotebookDocument) { + return notebook + .getCells() + .filter((cell) => isQsharpNotebookCell(cell.document)); + } + + function getQSharpText(document: vscode.TextDocument) { + const magicRange = findQSharpCellMagic(document); + if (magicRange) { + const magicStartOffset = document.offsetAt(magicRange.start); + const magicEndOffset = document.offsetAt(magicRange.end); + // Erase the %%qsharp magic line if it's there. + // Replace it with a comment so that document offsets remain the same. + // This will save us from having to map offsets later when + // communicating with the language service. + const text = document.getText(); + return ( + text.substring(0, magicStartOffset) + + "//qsharp" + + text.substring(magicEndOffset) + ); + } else { + // No %%qsharp magic. This can happen if the user manually sets the + // cell language to Q#. Python won't recognize the cell as a Q# cell, + // so this will fail at runtime, but as the language service we respect + // the manually set cell language, so we treat this as any other + // Q# cell. We could consider raising a warning here to help the user. + log.info( + "found Q# cell without %%qsharp magic: " + document.uri.toString(), + ); + return document.getText(); + } + } + + return subscriptions; +} + +/** + * Finds an output cell that contains an item with the Q# config MIME type, + * and returns the data from it. This data and is generated by the execution of a + * `qsharp.init()` call. It's Q# configuration data to be passed + * to the language service as "notebook metadata". + */ +function getQSharpConfigMetadata(notebook: vscode.NotebookDocument): object { + const data = notebook + .getCells() + .flatMap((cell) => cell.outputs) + .flatMap((output) => output.items) + .find((item) => { + return item.mime === qsharpConfigMimeType; + })?.data; + + if (data) { + const dataString = new TextDecoder().decode(data); + log.trace("found Q# config metadata: " + dataString); + return JSON.parse(dataString); + } else { + return {}; + } +} diff --git a/vscode/src/references.ts b/vscode/src/language-service/references.ts similarity index 95% rename from vscode/src/references.ts rename to vscode/src/language-service/references.ts index 7531cff9f4..528038c189 100644 --- a/vscode/src/references.ts +++ b/vscode/src/language-service/references.ts @@ -3,7 +3,7 @@ import { ILanguageService } from "qsharp-lang"; import * as vscode from "vscode"; -import { toVscodeLocation } from "./common"; +import { toVscodeLocation } from "../common"; export function createReferenceProvider(languageService: ILanguageService) { return new QSharpReferenceProvider(languageService); diff --git a/vscode/src/rename.ts b/vscode/src/language-service/rename.ts similarity index 95% rename from vscode/src/rename.ts rename to vscode/src/language-service/rename.ts index 0569a73801..02060ab4f5 100644 --- a/vscode/src/rename.ts +++ b/vscode/src/language-service/rename.ts @@ -3,7 +3,7 @@ import { ILanguageService } from "qsharp-lang"; import * as vscode from "vscode"; -import { toVscodeRange, toVscodeWorkspaceEdit } from "./common"; +import { toVscodeRange, toVscodeWorkspaceEdit } from "../common"; export function createRenameProvider(languageService: ILanguageService) { return new QSharpRenameProvider(languageService); diff --git a/vscode/src/signature.ts b/vscode/src/language-service/signature.ts similarity index 100% rename from vscode/src/signature.ts rename to vscode/src/language-service/signature.ts diff --git a/vscode/src/notebook.ts b/vscode/src/notebook.ts index 2d370b0be9..5e53cb54bd 100644 --- a/vscode/src/notebook.ts +++ b/vscode/src/notebook.ts @@ -1,16 +1,15 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { ILanguageService, log } from "qsharp-lang"; +import { log } from "qsharp-lang"; import * as vscode from "vscode"; -import { isQsharpNotebookCell, qsharpLanguageId } from "./common.js"; import { WorkspaceTreeProvider } from "./azure/treeView.js"; import { getPythonCodeForWorkspace } from "./azure/workspaceActions.js"; +import { qsharpLanguageId } from "./common.js"; import { notebookTemplate } from "./notebookTemplate.js"; const qsharpCellMagic = "%%qsharp"; -const jupyterNotebookType = "jupyter-notebook"; -const qsharpConfigMimeType = "application/x.qsharp-config"; +export const jupyterNotebookType = "jupyter-notebook"; let defaultLanguageId: string | undefined; /** @@ -97,13 +96,11 @@ export function registerQSharpNotebookHandlers() { return subscriptions; } -const openQSharpNotebooks = new Set(); - /** * Returns the range of the `%%qsharp` cell magic, or `undefined` * if it does not exist. */ -function findQSharpCellMagic(document: vscode.TextDocument) { +export function findQSharpCellMagic(document: vscode.TextDocument) { // Ignore whitespace before the cell magic for (let i = 0; i < document.lineCount; i++) { const line = document.lineAt(i); @@ -126,130 +123,6 @@ function findQSharpCellMagic(document: vscode.TextDocument) { return undefined; } -/** - * This one is for syncing with the language service - */ -export function registerQSharpNotebookCellUpdateHandlers( - languageService: ILanguageService, -) { - vscode.workspace.notebookDocuments.forEach((notebook) => { - updateIfQsharpNotebook(notebook); - }); - - const subscriptions = []; - subscriptions.push( - vscode.workspace.onDidOpenNotebookDocument((notebook) => { - updateIfQsharpNotebook(notebook); - }), - ); - - subscriptions.push( - vscode.workspace.onDidChangeNotebookDocument((event) => { - updateIfQsharpNotebook(event.notebook); - }), - ); - - subscriptions.push( - vscode.workspace.onDidCloseNotebookDocument((notebook) => { - closeIfKnownQsharpNotebook(notebook); - }), - ); - - function updateIfQsharpNotebook(notebook: vscode.NotebookDocument) { - if (notebook.notebookType === jupyterNotebookType) { - const qsharpMetadata = getQSharpConfigMetadata(notebook); - const qsharpCells = getQSharpCells(notebook); - const notebookUri = notebook.uri.toString(); - if (qsharpCells.length > 0) { - openQSharpNotebooks.add(notebookUri); - languageService.updateNotebookDocument( - notebookUri, - notebook.version, - qsharpMetadata, - qsharpCells.map((cell) => { - return { - uri: cell.document.uri.toString(), - version: cell.document.version, - code: getQSharpText(cell.document), - }; - }), - ); - } else { - // All Q# cells could have been deleted, check if we know this doc from previous calls - closeIfKnownQsharpNotebook(notebook); - } - } - } - - function closeIfKnownQsharpNotebook(notebook: vscode.NotebookDocument) { - const notebookUri = notebook.uri.toString(); - if (openQSharpNotebooks.has(notebookUri)) { - languageService.closeNotebookDocument(notebookUri); - openQSharpNotebooks.delete(notebook.uri.toString()); - } - } - - function getQSharpCells(notebook: vscode.NotebookDocument) { - return notebook - .getCells() - .filter((cell) => isQsharpNotebookCell(cell.document)); - } - - function getQSharpText(document: vscode.TextDocument) { - const magicRange = findQSharpCellMagic(document); - if (magicRange) { - const magicStartOffset = document.offsetAt(magicRange.start); - const magicEndOffset = document.offsetAt(magicRange.end); - // Erase the %%qsharp magic line if it's there. - // Replace it with a comment so that document offsets remain the same. - // This will save us from having to map offsets later when - // communicating with the language service. - const text = document.getText(); - return ( - text.substring(0, magicStartOffset) + - "//qsharp" + - text.substring(magicEndOffset) - ); - } else { - // No %%qsharp magic. This can happen if the user manually sets the - // cell language to Q#. Python won't recognize the cell as a Q# cell, - // so this will fail at runtime, but as the language service we respect - // the manually set cell language, so we treat this as any other - // Q# cell. We could consider raising a warning here to help the user. - log.info( - "found Q# cell without %%qsharp magic: " + document.uri.toString(), - ); - return document.getText(); - } - } - - return subscriptions; -} - -/** - * Finds an output cell that contains an item with the Q# config MIME type, - * and returns the data from it. This data and is generated by the execution of a - * `qsharp.init()` call. It's Q# configuration data to be passed - * to the language service as "notebook metadata". - */ -function getQSharpConfigMetadata(notebook: vscode.NotebookDocument): object { - const data = notebook - .getCells() - .flatMap((cell) => cell.outputs) - .flatMap((output) => output.items) - .find((item) => { - return item.mime === qsharpConfigMimeType; - })?.data; - - if (data) { - const dataString = new TextDecoder().decode(data); - log.trace("found Q# config metadata: " + dataString); - return JSON.parse(dataString); - } else { - return {}; - } -} - // Yes, this function is long, but mostly to deal with multi-folder VS Code workspace or multi // Azure Quantum workspace connection scenarios. The actual notebook creation is pretty simple. export function registerCreateNotebookCommand(