diff --git a/.vscode/notebooks/api.github-issues b/.vscode/notebooks/api.github-issues index 706cb4d5d38..3c234735491 100644 --- a/.vscode/notebooks/api.github-issues +++ b/.vscode/notebooks/api.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$repo=repo:microsoft/vscode\n$milestone=milestone:\"August 2023\"" + "value": "$repo=repo:microsoft/vscode\n$milestone=milestone:\"September 2023\"" }, { "kind": 1, diff --git a/.vscode/notebooks/my-work.github-issues b/.vscode/notebooks/my-work.github-issues index 06efd345018..e48617f8fde 100644 --- a/.vscode/notebooks/my-work.github-issues +++ b/.vscode/notebooks/my-work.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "// list of repos we work in\n$repos=repo:microsoft/vscode repo:microsoft/vscode-remote-release repo:microsoft/vscode-js-debug repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-dev repo:microsoft/vscode-unpkg repo:microsoft/vscode-references-view repo:microsoft/vscode-anycode repo:microsoft/vscode-hexeditor repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-livepreview repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-remote-repositories-github repo:microsoft/monaco-editor repo:microsoft/vscode-vsce repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-python repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-l10n repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release\n\n// current milestone name\n$milestone=milestone:\"August 2023\"" + "value": "// list of repos we work in\n$repos=repo:microsoft/vscode repo:microsoft/vscode-remote-release repo:microsoft/vscode-js-debug repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-dev repo:microsoft/vscode-unpkg repo:microsoft/vscode-references-view repo:microsoft/vscode-anycode repo:microsoft/vscode-hexeditor repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-livepreview repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-remote-repositories-github repo:microsoft/monaco-editor repo:microsoft/vscode-vsce repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-python repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-l10n repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release\n\n// current milestone name\n$milestone=milestone:\"September 2023\"" }, { "kind": 1, diff --git a/build/azure-pipelines/alpine/cli-build-alpine.yml b/build/azure-pipelines/alpine/cli-build-alpine.yml index 3fb6234cbcf..8b5303f5785 100644 --- a/build/azure-pipelines/alpine/cli-build-alpine.yml +++ b/build/azure-pipelines/alpine/cli-build-alpine.yml @@ -34,7 +34,7 @@ steps: displayName: Download openssl prebuilt inputs: command: custom - customCommand: pack @vscode-internal/openssl-prebuilt@0.0.8 + customCommand: pack @vscode-internal/openssl-prebuilt@0.0.10 customRegistry: useFeed customFeed: "Monaco/openssl-prebuilt" workingDir: $(Build.ArtifactStagingDirectory) @@ -42,7 +42,7 @@ steps: - script: | set -e mkdir $(Build.ArtifactStagingDirectory)/openssl - tar -xvzf $(Build.ArtifactStagingDirectory)/vscode-internal-openssl-prebuilt-0.0.8.tgz --strip-components=1 --directory=$(Build.ArtifactStagingDirectory)/openssl + tar -xvzf $(Build.ArtifactStagingDirectory)/vscode-internal-openssl-prebuilt-0.0.10.tgz --strip-components=1 --directory=$(Build.ArtifactStagingDirectory)/openssl displayName: Extract openssl prebuilt # inspired by: https://github.com/emk/rust-musl-builder/blob/main/Dockerfile diff --git a/build/azure-pipelines/darwin/cli-build-darwin.yml b/build/azure-pipelines/darwin/cli-build-darwin.yml index ae8f0e84652..f090c811a34 100644 --- a/build/azure-pipelines/darwin/cli-build-darwin.yml +++ b/build/azure-pipelines/darwin/cli-build-darwin.yml @@ -23,7 +23,7 @@ steps: displayName: Download openssl prebuilt inputs: command: custom - customCommand: pack @vscode-internal/openssl-prebuilt@0.0.8 + customCommand: pack @vscode-internal/openssl-prebuilt@0.0.10 customRegistry: useFeed customFeed: "Monaco/openssl-prebuilt" workingDir: $(Build.ArtifactStagingDirectory) @@ -31,7 +31,7 @@ steps: - script: | set -e mkdir $(Build.ArtifactStagingDirectory)/openssl - tar -xvzf $(Build.ArtifactStagingDirectory)/vscode-internal-openssl-prebuilt-0.0.8.tgz --strip-components=1 --directory=$(Build.ArtifactStagingDirectory)/openssl + tar -xvzf $(Build.ArtifactStagingDirectory)/vscode-internal-openssl-prebuilt-0.0.10.tgz --strip-components=1 --directory=$(Build.ArtifactStagingDirectory)/openssl displayName: Extract openssl prebuilt - template: ../cli/install-rust-posix.yml diff --git a/build/azure-pipelines/linux/cli-build-linux.yml b/build/azure-pipelines/linux/cli-build-linux.yml index b77b78aa1a8..098829eeb5f 100644 --- a/build/azure-pipelines/linux/cli-build-linux.yml +++ b/build/azure-pipelines/linux/cli-build-linux.yml @@ -26,7 +26,7 @@ steps: displayName: Download openssl prebuilt inputs: command: custom - customCommand: pack @vscode-internal/openssl-prebuilt@0.0.8 + customCommand: pack @vscode-internal/openssl-prebuilt@0.0.10 customRegistry: useFeed customFeed: "Monaco/openssl-prebuilt" workingDir: $(Build.ArtifactStagingDirectory) @@ -34,7 +34,7 @@ steps: - script: | set -e mkdir $(Build.ArtifactStagingDirectory)/openssl - tar -xvzf $(Build.ArtifactStagingDirectory)/vscode-internal-openssl-prebuilt-0.0.8.tgz --strip-components=1 --directory=$(Build.ArtifactStagingDirectory)/openssl + tar -xvzf $(Build.ArtifactStagingDirectory)/vscode-internal-openssl-prebuilt-0.0.10.tgz --strip-components=1 --directory=$(Build.ArtifactStagingDirectory)/openssl displayName: Extract openssl prebuilt - ${{ if eq(parameters.VSCODE_BUILD_LINUX_ARMHF, true) }}: diff --git a/build/azure-pipelines/win32/cli-build-win32.yml b/build/azure-pipelines/win32/cli-build-win32.yml index 6eb5ac41f5c..3d2c08dcf5a 100644 --- a/build/azure-pipelines/win32/cli-build-win32.yml +++ b/build/azure-pipelines/win32/cli-build-win32.yml @@ -26,14 +26,14 @@ steps: displayName: Download openssl prebuilt inputs: command: custom - customCommand: pack @vscode-internal/openssl-prebuilt@0.0.8 + customCommand: pack @vscode-internal/openssl-prebuilt@0.0.10 customRegistry: useFeed customFeed: "Monaco/openssl-prebuilt" workingDir: $(Build.ArtifactStagingDirectory) - powershell: | mkdir $(Build.ArtifactStagingDirectory)/openssl - tar -xvzf $(Build.ArtifactStagingDirectory)/vscode-internal-openssl-prebuilt-0.0.8.tgz --strip-components=1 --directory=$(Build.ArtifactStagingDirectory)/openssl + tar -xvzf $(Build.ArtifactStagingDirectory)/vscode-internal-openssl-prebuilt-0.0.10.tgz --strip-components=1 --directory=$(Build.ArtifactStagingDirectory)/openssl displayName: Extract openssl prebuilt - template: ../cli/install-rust-win32.yml @@ -54,8 +54,8 @@ steps: VSCODE_CLI_ARTIFACT: unsigned_vscode_cli_win32_x64_cli VSCODE_CHECK_ONLY: ${{ parameters.VSCODE_CHECK_ONLY }} VSCODE_CLI_ENV: - OPENSSL_LIB_DIR: $(Build.ArtifactStagingDirectory)/openssl/x64-windows-static-md/lib - OPENSSL_INCLUDE_DIR: $(Build.ArtifactStagingDirectory)/openssl/x64-windows-static-md/include + OPENSSL_LIB_DIR: $(Build.ArtifactStagingDirectory)/openssl/x64-windows-static/lib + OPENSSL_INCLUDE_DIR: $(Build.ArtifactStagingDirectory)/openssl/x64-windows-static/include RUSTFLAGS: "-C target-feature=+crt-static" - ${{ if eq(parameters.VSCODE_BUILD_WIN32_ARM64, true) }}: @@ -66,8 +66,8 @@ steps: VSCODE_CLI_ARTIFACT: unsigned_vscode_cli_win32_arm64_cli VSCODE_CHECK_ONLY: ${{ parameters.VSCODE_CHECK_ONLY }} VSCODE_CLI_ENV: - OPENSSL_LIB_DIR: $(Build.ArtifactStagingDirectory)/openssl/arm64-windows-static-md/lib - OPENSSL_INCLUDE_DIR: $(Build.ArtifactStagingDirectory)/openssl/arm64-windows-static-md/include + OPENSSL_LIB_DIR: $(Build.ArtifactStagingDirectory)/openssl/arm64-windows-static/lib + OPENSSL_INCLUDE_DIR: $(Build.ArtifactStagingDirectory)/openssl/arm64-windows-static/include RUSTFLAGS: "-C target-feature=+crt-static" - ${{ if eq(parameters.VSCODE_BUILD_WIN32_32BIT, true) }}: @@ -78,6 +78,6 @@ steps: VSCODE_CLI_ARTIFACT: unsigned_vscode_cli_win32_ia32_cli VSCODE_CHECK_ONLY: ${{ parameters.VSCODE_CHECK_ONLY }} VSCODE_CLI_ENV: - OPENSSL_LIB_DIR: $(Build.ArtifactStagingDirectory)/openssl/x86-windows-static-md/lib - OPENSSL_INCLUDE_DIR: $(Build.ArtifactStagingDirectory)/openssl/x86-windows-static-md/include + OPENSSL_LIB_DIR: $(Build.ArtifactStagingDirectory)/openssl/x86-windows-static/lib + OPENSSL_INCLUDE_DIR: $(Build.ArtifactStagingDirectory)/openssl/x86-windows-static/include RUSTFLAGS: "-C target-feature=+crt-static" diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index b7d31b812d8..301b01e45c7 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -734,7 +734,7 @@ "--tab-sizing-current-width", "--tab-sizing-fixed-min-width", "--tab-sizing-fixed-max-width", - "--editor-group-title-height", + "--editor-group-tab-height", "--testMessageDecorationFontFamily", "--testMessageDecorationFontSize", "--title-border-bottom-color", diff --git a/cli/src/tunnels/control_server.rs b/cli/src/tunnels/control_server.rs index 45e0c9748ef..af0605bb66d 100644 --- a/cli/src/tunnels/control_server.rs +++ b/cli/src/tunnels/control_server.rs @@ -278,8 +278,8 @@ fn make_socket_rpc( port_forwarding: Option, requires_auth: AuthRequired, platform: Platform, + http_requests: HttpRequestsMap, ) -> RpcDispatcher { - let http_requests = Arc::new(std::sync::Mutex::new(HashMap::new())); let server_bridges = ServerMultiplexer::new(); let mut rpc = RpcBuilder::new(MsgPackSerializer {}).methods(HandlerContext { did_update: Arc::new(AtomicBool::new(false)), @@ -377,7 +377,10 @@ fn make_socket_rpc( ); rpc.register_sync("httpheaders", |p: HttpHeadersParams, c| { if let Some(req) = c.http_requests.lock().unwrap().get(&p.req_id) { + trace!(c.log, "got {} response for req {}", p.status_code, p.req_id); req.initial_response(p.status_code, p.headers); + } else { + warning!(c.log, "got response for unknown req {}", p.req_id); } Ok(EmptyObject {}) }); @@ -388,6 +391,7 @@ fn make_socket_rpc( req.body(p.segment); } if p.complete { + trace!(c.log, "delegated request {} completed", p.req_id); reqs.remove(&p.req_id); } } @@ -441,6 +445,7 @@ async fn process_socket( port_forwarding, requires_auth, platform, + http_requests.clone(), ); { @@ -497,6 +502,7 @@ async fn process_socket( }), }) .unwrap(); + http_requests.lock().unwrap().insert(id, r); tx_counter += serialized.len(); diff --git a/cli/src/tunnels/protocol.rs b/cli/src/tunnels/protocol.rs index 316e3672ba6..5665714fed9 100644 --- a/cli/src/tunnels/protocol.rs +++ b/cli/src/tunnels/protocol.rs @@ -16,6 +16,7 @@ use serde::{Deserialize, Serialize}; #[allow(non_camel_case_types)] pub enum ClientRequestMethod<'a> { servermsg(RefServerMessageParams<'a>), + serverclose(ServerClosedParams), serverlog(ServerLog<'a>), makehttpreq(HttpRequestParams<'a>), version(VersionResponse), @@ -89,6 +90,11 @@ pub struct ServerMessageParams { pub body: Vec, } +#[derive(Serialize, Debug)] +pub struct ServerClosedParams { + pub i: u16, +} + #[derive(Serialize, Debug)] pub struct RefServerMessageParams<'a> { pub i: u16, diff --git a/cli/src/tunnels/server_bridge.rs b/cli/src/tunnels/server_bridge.rs index 50dde8e7303..f1a358279af 100644 --- a/cli/src/tunnels/server_bridge.rs +++ b/cli/src/tunnels/server_bridge.rs @@ -32,6 +32,7 @@ impl ServerBridge { match read.read(&mut read_buf).await { Err(_) => return, Ok(0) => { + let _ = target.server_closed().await; return; // EOF } Ok(s) => { diff --git a/cli/src/tunnels/socket_signal.rs b/cli/src/tunnels/socket_signal.rs index 53e6cd51567..9036c6ae3f9 100644 --- a/cli/src/tunnels/socket_signal.rs +++ b/cli/src/tunnels/socket_signal.rs @@ -9,7 +9,7 @@ use tokio::sync::mpsc; use crate::msgpack_rpc::MsgPackCaller; use super::{ - protocol::{ClientRequestMethod, RefServerMessageParams, ToClientRequest}, + protocol::{ClientRequestMethod, RefServerMessageParams, ServerClosedParams, ToClientRequest}, server_multiplexer::ServerMultiplexer, }; @@ -81,25 +81,43 @@ impl ServerMessageSink { } } + pub async fn server_closed(&mut self) -> Result<(), mpsc::error::SendError> { + self.server_message_or_closed(None).await + } + pub async fn server_message( &mut self, body: &[u8], ) -> Result<(), mpsc::error::SendError> { - let id = self.id; + self.server_message_or_closed(Some(body)).await + } + + async fn server_message_or_closed( + &mut self, + body: Option<&[u8]>, + ) -> Result<(), mpsc::error::SendError> { + let i = self.id; let mut tx = self.tx.take().unwrap(); - let body = self.get_server_msg_content(body); - let msg = RefServerMessageParams { i: id, body }; + let msg = body + .map(|b| self.get_server_msg_content(b)) + .map(|body| RefServerMessageParams { i, body }); let r = match &mut tx { ServerMessageDestination::Channel(tx) => { tx.send(SocketSignal::from_message(&ToClientRequest { id: None, - params: ClientRequestMethod::servermsg(msg), + params: match msg { + Some(msg) => ClientRequestMethod::servermsg(msg), + None => ClientRequestMethod::serverclose(ServerClosedParams { i }), + }, })) .await } ServerMessageDestination::Rpc(caller) => { - caller.notify("servermsg", msg); + match msg { + Some(msg) => caller.notify("servermsg", msg), + None => caller.notify("serverclose", ServerClosedParams { i }), + }; Ok(()) } }; diff --git a/extensions/configuration-editing/package.json b/extensions/configuration-editing/package.json index 520682059e1..b80a187e266 100644 --- a/extensions/configuration-editing/package.json +++ b/extensions/configuration-editing/package.json @@ -23,7 +23,7 @@ "watch": "gulp watch-extension:configuration-editing" }, "dependencies": { - "jsonc-parser": "^2.2.1", + "jsonc-parser": "^3.2.0", "@octokit/rest": "19.0.4", "tunnel": "^0.0.6" }, diff --git a/extensions/configuration-editing/yarn.lock b/extensions/configuration-editing/yarn.lock index 549c578a9ac..7672e88e7a4 100644 --- a/extensions/configuration-editing/yarn.lock +++ b/extensions/configuration-editing/yarn.lock @@ -3,41 +3,39 @@ "@octokit/auth-token@^3.0.0": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-3.0.2.tgz#a0fc8de149fd15876e1ac78f6525c1c5ab48435f" - integrity sha512-pq7CwIMV1kmzkFTimdwjAINCXKTajZErLB4wMLYapR2nuB/Jpr66+05wOTZMSCBXP6n4DdDWT2W19Bm17vU69Q== - dependencies: - "@octokit/types" "^8.0.0" + version "3.0.4" + resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-3.0.4.tgz#70e941ba742bdd2b49bdb7393e821dea8520a3db" + integrity sha512-TWFX7cZF2LXoCvdmJWY7XVPi74aSY0+FfBZNSXEXFkMpjcqsQwDSYVv5FhRFaI0V1ECnwbz4j59T/G+rXNWaIQ== "@octokit/core@^4.0.0": - version "4.1.0" - resolved "https://registry.yarnpkg.com/@octokit/core/-/core-4.1.0.tgz#b6b03a478f1716de92b3f4ec4fd64d05ba5a9251" - integrity sha512-Czz/59VefU+kKDy+ZfDwtOIYIkFjExOKf+HA92aiTZJ6EfWpFzYQWw0l54ji8bVmyhc+mGaLUbSUmXazG7z5OQ== + version "4.2.4" + resolved "https://registry.yarnpkg.com/@octokit/core/-/core-4.2.4.tgz#d8769ec2b43ff37cc3ea89ec4681a20ba58ef907" + integrity sha512-rYKilwgzQ7/imScn3M9/pFfUf4I1AZEH3KhyJmtPdE2zfaXAn2mFfUy4FbKewzc2We5y/LlKLj36fWJLKC2SIQ== dependencies: "@octokit/auth-token" "^3.0.0" "@octokit/graphql" "^5.0.0" "@octokit/request" "^6.0.0" "@octokit/request-error" "^3.0.0" - "@octokit/types" "^8.0.0" + "@octokit/types" "^9.0.0" before-after-hook "^2.2.0" universal-user-agent "^6.0.0" "@octokit/endpoint@^7.0.0": - version "7.0.3" - resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-7.0.3.tgz#0b96035673a9e3bedf8bab8f7335de424a2147ed" - integrity sha512-57gRlb28bwTsdNXq+O3JTQ7ERmBTuik9+LelgcLIVfYwf235VHbN9QNo4kXExtp/h8T423cR5iJThKtFYxC7Lw== + version "7.0.6" + resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-7.0.6.tgz#791f65d3937555141fb6c08f91d618a7d645f1e2" + integrity sha512-5L4fseVRUsDFGR00tMWD/Trdeeihn999rTMGRMC1G/Ldi1uWlWJzI98H4Iak5DB/RVvQuyMYKqSK/R6mbSOQyg== dependencies: - "@octokit/types" "^8.0.0" + "@octokit/types" "^9.0.0" is-plain-object "^5.0.0" universal-user-agent "^6.0.0" "@octokit/graphql@^5.0.0": - version "5.0.4" - resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-5.0.4.tgz#519dd5c05123868276f3ae4e50ad565ed7dff8c8" - integrity sha512-amO1M5QUQgYQo09aStR/XO7KAl13xpigcy/kI8/N1PnZYSS69fgte+xA4+c2DISKqUZfsh0wwjc2FaCt99L41A== + version "5.0.6" + resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-5.0.6.tgz#9eac411ac4353ccc5d3fca7d76736e6888c5d248" + integrity sha512-Fxyxdy/JH0MnIB5h+UQ3yCoh1FG4kWXfFKkpWqjZHw/p+Kc8Y44Hu/kCgNBT6nU1shNumEchmW/sUO1JuQnPcw== dependencies: "@octokit/request" "^6.0.0" - "@octokit/types" "^8.0.0" + "@octokit/types" "^9.0.0" universal-user-agent "^6.0.0" "@octokit/openapi-types@^13.11.0": @@ -50,6 +48,11 @@ resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-14.0.0.tgz#949c5019028c93f189abbc2fb42f333290f7134a" integrity sha512-HNWisMYlR8VCnNurDU6os2ikx0s0VyEjDYHNS/h4cgb8DeOxQ0n72HyinUtdDVxJhFy3FWLGl0DJhfEWk3P5Iw== +"@octokit/openapi-types@^18.0.0": + version "18.0.0" + resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-18.0.0.tgz#f43d765b3c7533fd6fb88f3f25df079c24fccf69" + integrity sha512-V8GImKs3TeQRxRtXFpG2wl19V7444NIOTDF24AWuIbmNaNYOQMWRbjcGDXV5B+0n887fgDcuMNOmlul+k+oJtw== + "@octokit/plugin-paginate-rest@^4.0.0": version "4.3.1" resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-4.3.1.tgz#553e653ee0318605acd23bf3a799c8bfafdedae3" @@ -63,30 +66,30 @@ integrity sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA== "@octokit/plugin-rest-endpoint-methods@^6.0.0": - version "6.7.0" - resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-6.7.0.tgz#2f6f17f25b6babbc8b41d2bb0a95a8839672ce7c" - integrity sha512-orxQ0fAHA7IpYhG2flD2AygztPlGYNAdlzYz8yrD8NDgelPfOYoRPROfEyIe035PlxvbYrgkfUZIhSBKju/Cvw== + version "6.8.1" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-6.8.1.tgz#97391fda88949eb15f68dc291957ccbe1d3e8ad1" + integrity sha512-QrlaTm8Lyc/TbU7BL/8bO49vp+RZ6W3McxxmmQTgYxf2sWkO8ZKuj4dLhPNJD6VCUW1hetCmeIM0m6FTVpDiEg== dependencies: - "@octokit/types" "^8.0.0" + "@octokit/types" "^8.1.1" deprecation "^2.3.1" "@octokit/request-error@^3.0.0": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-3.0.2.tgz#f74c0f163d19463b87528efe877216c41d6deb0a" - integrity sha512-WMNOFYrSaX8zXWoJg9u/pKgWPo94JXilMLb2VManNOby9EZxrQaBe/QSC4a1TzpAlpxofg2X/jMnCyZgL6y7eg== + version "3.0.3" + resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-3.0.3.tgz#ef3dd08b8e964e53e55d471acfe00baa892b9c69" + integrity sha512-crqw3V5Iy2uOU5Np+8M/YexTlT8zxCfI+qu+LxUB7SZpje4Qmx3mub5DfEKSO8Ylyk0aogi6TYdf6kxzh2BguQ== dependencies: - "@octokit/types" "^8.0.0" + "@octokit/types" "^9.0.0" deprecation "^2.0.0" once "^1.4.0" "@octokit/request@^6.0.0": - version "6.2.2" - resolved "https://registry.yarnpkg.com/@octokit/request/-/request-6.2.2.tgz#a2ba5ac22bddd5dcb3f539b618faa05115c5a255" - integrity sha512-6VDqgj0HMc2FUX2awIs+sM6OwLgwHvAi4KCK3mT2H2IKRt6oH9d0fej5LluF5mck1lRR/rFWN0YIDSYXYSylbw== + version "6.2.8" + resolved "https://registry.yarnpkg.com/@octokit/request/-/request-6.2.8.tgz#aaf480b32ab2b210e9dadd8271d187c93171d8eb" + integrity sha512-ow4+pkVQ+6XVVsekSYBzJC0VTVvh/FCTUUgTsboGq+DTeWdyIFV8WSCdo0RIxk6wSkBTHqIK1mYuY7nOBXOchw== dependencies: "@octokit/endpoint" "^7.0.0" "@octokit/request-error" "^3.0.0" - "@octokit/types" "^8.0.0" + "@octokit/types" "^9.0.0" is-plain-object "^5.0.0" node-fetch "^2.6.7" universal-user-agent "^6.0.0" @@ -108,13 +111,20 @@ dependencies: "@octokit/openapi-types" "^13.11.0" -"@octokit/types@^8.0.0": - version "8.1.1" - resolved "https://registry.yarnpkg.com/@octokit/types/-/types-8.1.1.tgz#92e304e0f00d563667dfdbe0ae6b52e70d5149bb" - integrity sha512-7tjk+6DyhYAmei8FOEwPfGKc0VE1x56CKPJ+eE44zhDbOyMT+9yan8apfQFxo8oEFsy+0O7PiBtH8w0Yo0Y9Kw== +"@octokit/types@^8.1.1": + version "8.2.1" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-8.2.1.tgz#a6de091ae68b5541f8d4fcf9a12e32836d4648aa" + integrity sha512-8oWMUji8be66q2B9PmEIUyQm00VPDPun07umUWSaCwxmeaquFBro4Hcc3ruVoDo3zkQyZBlRvhIMEYS3pBhanw== dependencies: "@octokit/openapi-types" "^14.0.0" +"@octokit/types@^9.0.0": + version "9.3.2" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-9.3.2.tgz#3f5f89903b69f6a2d196d78ec35f888c0013cac5" + integrity sha512-D4iHGTdAnEEVsB8fl95m1hiz7D5YiRdQ9b/OEb3BYRVwbLsGHcRVPz+u+BgRLNk0Q0/4iZCBqDN96j2XNxfXrA== + dependencies: + "@octokit/openapi-types" "^18.0.0" + "@types/node@18.x": version "18.15.13" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" @@ -135,15 +145,15 @@ is-plain-object@^5.0.0: resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== -jsonc-parser@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.2.1.tgz#db73cd59d78cce28723199466b2a03d1be1df2bc" - integrity sha512-o6/yDBYccGvTz1+QFevz6l6OBZ2+fMVu2JZ9CIhzsYRX4mjaK5IyX9eldUdCmga16zlgQxyrj5pt9kzuj2C02w== +jsonc-parser@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz#31ff3f4c2b9793f89c67212627c51c6394f88e76" + integrity sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w== node-fetch@^2.6.7: - version "2.6.8" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.8.tgz#a68d30b162bc1d8fd71a367e81b997e1f4d4937e" - integrity sha512-RZ6dBYuj8dRSfxpUSu+NsdF1dpPpluJxwOp+6IoDp/sH2QNDSvurYsAa+F1WxY2RjA1iP93xhcsUoYbF2XBqVg== + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== dependencies: whatwg-url "^5.0.0" diff --git a/extensions/csharp/cgmanifest.json b/extensions/csharp/cgmanifest.json index 962188ea971..dd44d10fb75 100644 --- a/extensions/csharp/cgmanifest.json +++ b/extensions/csharp/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "dotnet/csharp-tmLanguage", "repositoryUrl": "https://github.com/dotnet/csharp-tmLanguage", - "commitHash": "772323937fedd65c6dc1c8ce6ea41d97415ed7d1" + "commitHash": "525e628edad54c0f7aa15b015310df240803ea66" } }, "license": "MIT", diff --git a/extensions/csharp/syntaxes/csharp.tmLanguage.json b/extensions/csharp/syntaxes/csharp.tmLanguage.json index c08bea9ce01..908da2f69cc 100644 --- a/extensions/csharp/syntaxes/csharp.tmLanguage.json +++ b/extensions/csharp/syntaxes/csharp.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/dotnet/csharp-tmLanguage/commit/772323937fedd65c6dc1c8ce6ea41d97415ed7d1", + "version": "https://github.com/dotnet/csharp-tmLanguage/commit/525e628edad54c0f7aa15b015310df240803ea66", "name": "C#", "scopeName": "source.cs", "patterns": [ @@ -210,9 +210,6 @@ { "include": "#else-part" }, - { - "include": "#switch-statement" - }, { "include": "#goto-statement" }, @@ -238,10 +235,10 @@ "include": "#checked-unchecked-statement" }, { - "include": "#lock-statement" + "include": "#context-control-statement" }, { - "include": "#using-statement" + "include": "#context-control-paren-statement" }, { "include": "#labeled-statement" @@ -278,13 +275,10 @@ "include": "#comment" }, { - "include": "#checked-unchecked-expression" + "include": "#expression-operator-expression" }, { - "include": "#typeof-or-default-expression" - }, - { - "include": "#nameof-expression" + "include": "#type-operator-expression" }, { "include": "#default-literal-expression" @@ -305,14 +299,20 @@ "include": "#type-builtin" }, { - "include": "#this-or-base-expression" + "include": "#language-variable" }, { - "include": "#switch-expression" + "include": "#switch-statement-or-expression" + }, + { + "include": "#with-expression" }, { "include": "#conditional-operator" }, + { + "include": "#assignment-expression" + }, { "include": "#expression-operators" }, @@ -370,36 +370,39 @@ ] }, "extern-alias-directive": { - "begin": "\\s*(extern)\\b\\s*(alias)\\b\\s*(@?[_[:alpha:]][_[:alnum:]]*)", + "begin": "\\b(extern)\\s+(alias)\\b", "beginCaptures": { "1": { - "name": "keyword.other.extern.cs" + "name": "keyword.other.directive.extern.cs" }, "2": { - "name": "keyword.other.alias.cs" - }, - "3": { - "name": "variable.other.alias.cs" + "name": "keyword.other.directive.alias.cs" } }, - "end": "(?=;)" + "end": "(?=;)", + "patterns": [ + { + "match": "\\@?[_[:alpha:]][_[:alnum:]]*", + "name": "variable.other.alias.cs" + } + ] }, "using-directive": { "patterns": [ { - "begin": "(\\b(global)\\b\\s+)?\\b(using)\\b\\s+(static)\\b\\s+(\\b(unsafe)\\b\\s+)?", + "begin": "\\b(?:(global)\\s+)?(using)\\s+(static)\\b\\s*(?:(unsafe)\\b\\s*)?", "beginCaptures": { + "1": { + "name": "keyword.other.directive.global.cs" + }, "2": { - "name": "keyword.other.global.cs" + "name": "keyword.other.directive.using.cs" }, "3": { - "name": "keyword.other.using.cs" + "name": "keyword.other.directive.static.cs" }, "4": { - "name": "keyword.other.static.cs" - }, - "6": { - "name": "storage.modifier.cs" + "name": "storage.modifier.unsafe.cs" } }, "end": "(?=;)", @@ -410,19 +413,22 @@ ] }, { - "begin": "(\\b(global)\\b\\s+)?\\b(using)\\b\\s+(\\b(unsafe)\\b\\s+)?(?=(@?[_[:alpha:]][_[:alnum:]]*)\\s*=)", + "begin": "\\b(?:(global)\\s+)?(using)\\b\\s*(?:(unsafe)\\b\\s*)?(@?[_[:alpha:]][_[:alnum:]]*)\\s*(=)", "beginCaptures": { + "1": { + "name": "keyword.other.directive.global.cs" + }, "2": { - "name": "keyword.other.global.cs" + "name": "keyword.other.directive.using.cs" }, "3": { - "name": "keyword.other.using.cs" + "name": "storage.modifier.unsafe.cs" + }, + "4": { + "name": "entity.name.type.alias.cs" }, "5": { - "name": "storage.modifier.cs" - }, - "6": { - "name": "entity.name.type.alias.cs" + "name": "keyword.operator.assignment.cs" } }, "end": "(?=;)", @@ -432,20 +438,17 @@ }, { "include": "#type" - }, - { - "include": "#operator-assignment" } ] }, { - "begin": "(\\b(global)\\b\\s+)?\\b(using)\\s*(?!\\(|\\s|var)", + "begin": "\\b(?:(global)\\s+)?(using)\\b\\s*+(?!\\(|var\\b)", "beginCaptures": { - "2": { - "name": "keyword.other.global.cs" + "1": { + "name": "keyword.other.directive.global.cs" }, - "3": { - "name": "keyword.other.using.cs" + "2": { + "name": "keyword.other.directive.using.cs" } }, "end": "(?=;)", @@ -455,7 +458,10 @@ }, { "name": "entity.name.type.namespace.cs", - "match": "@?[_[:alpha:]][_[:alnum:]]*" + "match": "\\@?[_[:alpha:]][_[:alnum:]]*" + }, + { + "include": "#punctuation-accessor" }, { "include": "#operator-assignment" @@ -551,7 +557,7 @@ "begin": "\\b(namespace)\\s+", "beginCaptures": { "1": { - "name": "keyword.other.namespace.cs" + "name": "storage.type.namespace.cs" } }, "end": "(?<=\\})|(?=;)", @@ -594,7 +600,7 @@ ] }, "storage-modifier": { - "name": "storage.modifier.cs", + "name": "storage.modifier.$1.cs", "match": "(?\n (?:\n (?:ref\\s+(?:readonly\\s+)?)? # ref return\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n)\\s+\n(\\g)\\s*\n(<([^<>]+)>)?\\s*\n(?=\\()", "beginCaptures": { "1": { - "name": "keyword.other.delegate.cs" + "name": "storage.type.delegate.cs" }, "2": { "patterns": [ @@ -712,7 +718,7 @@ "match": "(enum)\\s+(@?[_[:alpha:]][_[:alnum:]]*)", "captures": { "1": { - "name": "keyword.other.enum.cs" + "name": "storage.type.enum.cs" }, "2": { "name": "entity.name.type.enum.cs" @@ -796,7 +802,7 @@ "begin": "(?x)\n(interface)\\b\\s+\n(@?[_[:alpha:]][_[:alnum:]]*)", "beginCaptures": { "1": { - "name": "keyword.other.interface.cs" + "name": "storage.type.interface.cs" }, "2": { "name": "entity.name.type.interface.cs" @@ -853,7 +859,7 @@ "begin": "(?x)\n(record)\\b\\s+\n(@?[_[:alpha:]][_[:alnum:]]*)", "beginCaptures": { "1": { - "name": "keyword.other.record.cs" + "name": "storage.type.record.cs" }, "2": { "name": "entity.name.type.class.cs" @@ -913,10 +919,10 @@ "begin": "(?x)\n(\\b(record)\\b\\s+)?\n(struct)\\b\\s+\n(@?[_[:alpha:]][_[:alnum:]]*)", "beginCaptures": { "2": { - "name": "keyword.other.record.cs" + "name": "storage.type.record.cs" }, "3": { - "name": "keyword.other.struct.cs" + "name": "storage.type.struct.cs" }, "4": { "name": "entity.name.type.struct.cs" @@ -984,19 +990,11 @@ "patterns": [ { "match": "\\b(in|out)\\b", - "captures": { - "1": { - "name": "storage.modifier.cs" - } - } + "name": "storage.modifier.$1.cs" }, { "match": "(@?[_[:alpha:]][_[:alnum:]]*)\\b", - "captures": { - "1": { - "name": "entity.name.type.type-parameter.cs" - } - } + "name": "entity.name.type.type-parameter.cs" }, { "include": "#comment" @@ -1033,7 +1031,7 @@ "begin": "(where)\\s+(@?[_[:alpha:]][_[:alnum:]]*)\\s*(:)", "beginCaptures": { "1": { - "name": "keyword.other.where.cs" + "name": "storage.modifier.where.cs" }, "2": { "name": "entity.name.type.type-parameter.cs" @@ -1045,18 +1043,18 @@ "end": "(?=\\{|where|;|=>)", "patterns": [ { - "name": "keyword.other.class.cs", + "name": "storage.type.class.cs", "match": "\\bclass\\b" }, { - "name": "keyword.other.struct.cs", + "name": "storage.type.struct.cs", "match": "\\bstruct\\b" }, { "match": "(new)\\s*(\\()\\s*(\\))", "captures": { "1": { - "name": "keyword.other.new.cs" + "name": "keyword.operator.expression.new.cs" }, "2": { "name": "punctuation.parenthesis.open.cs" @@ -1112,7 +1110,7 @@ ] }, "property-declaration": { - "begin": "(?x)\n\n# The negative lookahead below ensures that we don't match nested types\n# or other declarations as properties.\n(?![[:word:][:space:]]*\\b(?:class|interface|struct|enum|event)\\b)\n\n(?\n (?\n (?:\n (?:ref\\s+(?:readonly\\s+)?)? # ref return\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n )\\s+\n)\n(?\\g\\s*\\.\\s*)?\n(?\\g)\\s*\n(?=\\{|=>|$)", + "begin": "(?x)\n\n# The negative lookahead below ensures that we don't match nested types\n# or other declarations as properties.\n(?![[:word:][:space:]]*\\b(?:class|interface|struct|enum|event)\\b)\n\n(?\n (?\n (?:\n (?:ref\\s+(?:readonly\\s+)?)? # ref return\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n )\\s+\n)\n(?\\g\\s*\\.\\s*)?\n(?\\g)\\s*\n(?=\\{|=>|//|/\\*|$)", "beginCaptures": { "1": { "patterns": [ @@ -1144,7 +1142,7 @@ "include": "#property-accessors" }, { - "include": "#expression-body" + "include": "#accessor-getter-expression" }, { "include": "#variable-initializer" @@ -1175,7 +1173,7 @@ ] }, "8": { - "name": "keyword.other.this.cs" + "name": "variable.language.this.cs" } }, "end": "(?<=\\})|(?=;)", @@ -1190,7 +1188,7 @@ "include": "#property-accessors" }, { - "include": "#expression-body" + "include": "#accessor-getter-expression" }, { "include": "#variable-initializer" @@ -1198,10 +1196,10 @@ ] }, "event-declaration": { - "begin": "(?x)\n\\b(event)\\b\\s*\n(?\n (?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n )\\s+\n)\n(?\\g\\s*\\.\\s*)?\n(?\\g(?:\\s*,\\s*\\g)*)\\s*\n(?=\\{|;|$)", + "begin": "(?x)\n\\b(event)\\b\\s*\n(?\n (?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n )\\s+\n)\n(?\\g\\s*\\.\\s*)?\n(\\g)\\s* # first event name\n(?=\\{|;|,|=|//|/\\*|$)", "beginCaptures": { "1": { - "name": "keyword.other.event.cs" + "name": "storage.type.event.cs" }, "2": { "patterns": [ @@ -1221,15 +1219,7 @@ ] }, "9": { - "patterns": [ - { - "name": "entity.name.variable.event.cs", - "match": "@?[_[:alpha:]][_[:alnum:]]*" - }, - { - "include": "#punctuation-comma" - } - ] + "name": "entity.name.variable.event.cs" } }, "end": "(?<=\\})|(?=;)", @@ -1240,8 +1230,29 @@ { "include": "#event-accessors" }, + { + "name": "entity.name.variable.event.cs", + "match": "@?[_[:alpha:]][_[:alnum:]]*" + }, { "include": "#punctuation-comma" + }, + { + "begin": "=", + "beginCaptures": { + "0": { + "name": "keyword.operator.assignment.cs" + } + }, + "end": "(?<=,)|(?=;)", + "patterns": [ + { + "include": "#expression" + }, + { + "include": "#punctuation-comma" + } + ] } ] }, @@ -1259,22 +1270,6 @@ } }, "patterns": [ - { - "name": "storage.modifier.cs", - "match": "\\b(private|protected|internal)\\b" - }, - { - "name": "keyword.other.get.cs", - "match": "\\b(get)\\b" - }, - { - "name": "keyword.other.set.cs", - "match": "\\b(set)\\b" - }, - { - "name": "keyword.other.init.cs", - "match": "\\b(init)\\b" - }, { "include": "#comment" }, @@ -1282,13 +1277,36 @@ "include": "#attribute-section" }, { - "include": "#expression-body" + "name": "storage.modifier.$1.cs", + "match": "\\b(private|protected|internal)\\b" }, { - "include": "#block" + "begin": "\\b(get)\\b\\s*(?=\\{|;|=>|//|/\\*|$)", + "beginCaptures": { + "1": { + "name": "storage.type.accessor.$1.cs" + } + }, + "end": "(?<=\\}|;)|(?=\\})", + "patterns": [ + { + "include": "#accessor-getter" + } + ] }, { - "include": "#punctuation-semicolon" + "begin": "\\b(set|init)\\b\\s*(?=\\{|;|=>|//|/\\*|$)", + "beginCaptures": { + "1": { + "name": "storage.type.accessor.$1.cs" + } + }, + "end": "(?<=\\}|;)|(?=\\})", + "patterns": [ + { + "include": "#accessor-setter" + } + ] } ] }, @@ -1306,14 +1324,6 @@ } }, "patterns": [ - { - "name": "keyword.other.add.cs", - "match": "\\b(add)\\b" - }, - { - "name": "keyword.other.remove.cs", - "match": "\\b(remove)\\b" - }, { "include": "#comment" }, @@ -1321,10 +1331,108 @@ "include": "#attribute-section" }, { - "include": "#expression-body" + "begin": "\\b(add|remove)\\b\\s*(?=\\{|;|=>|//|/\\*|$)", + "beginCaptures": { + "1": { + "name": "storage.type.accessor.$1.cs" + } + }, + "end": "(?<=\\}|;)|(?=\\})", + "patterns": [ + { + "include": "#accessor-setter" + } + ] + } + ] + }, + "accessor-getter": { + "patterns": [ + { + "begin": "\\{", + "beginCaptures": { + "0": { + "name": "punctuation.curlybrace.open.cs" + } + }, + "end": "\\}", + "endCaptures": { + "0": { + "name": "punctuation.curlybrace.close.cs" + } + }, + "contentName": "meta.accessor.getter.cs", + "patterns": [ + { + "include": "#statement" + } + ] }, { - "include": "#block" + "include": "#accessor-getter-expression" + }, + { + "include": "#punctuation-semicolon" + } + ] + }, + "accessor-getter-expression": { + "begin": "=>", + "beginCaptures": { + "0": { + "name": "keyword.operator.arrow.cs" + } + }, + "end": "(?=;|\\})", + "contentName": "meta.accessor.getter.cs", + "patterns": [ + { + "include": "#ref-modifier" + }, + { + "include": "#expression" + } + ] + }, + "accessor-setter": { + "patterns": [ + { + "begin": "\\{", + "beginCaptures": { + "0": { + "name": "punctuation.curlybrace.open.cs" + } + }, + "end": "\\}", + "endCaptures": { + "0": { + "name": "punctuation.curlybrace.close.cs" + } + }, + "contentName": "meta.accessor.setter.cs", + "patterns": [ + { + "include": "#statement" + } + ] + }, + { + "begin": "=>", + "beginCaptures": { + "0": { + "name": "keyword.operator.arrow.cs" + } + }, + "end": "(?=;|\\})", + "contentName": "meta.accessor.setter.cs", + "patterns": [ + { + "include": "#ref-modifier" + }, + { + "include": "#expression" + } + ] }, { "include": "#punctuation-semicolon" @@ -1425,13 +1533,10 @@ ] }, "constructor-initializer": { - "begin": "\\b(?:(base)|(this))\\b\\s*(?=\\()", + "begin": "\\b(base|this)\\b\\s*(?=\\()", "beginCaptures": { "1": { - "name": "keyword.other.base.cs" - }, - "2": { - "name": "keyword.other.this.cs" + "name": "variable.language.$1.cs" } }, "end": "(?<=\\))", @@ -1468,7 +1573,7 @@ ] }, "operator-declaration": { - "begin": "(?x)\n(?\n (?:\n (?:ref\\s+(?:readonly\\s+)?)? # ref return\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n)\\s*\n(?(?:\\b(?:operator)))\\s*\n(?(?:\\+|-|\\*|/|%|&|\\||\\^|\\<\\<|\\>\\>|==|!=|\\>|\\<|\\>=|\\<=|!|~|\\+\\+|--|true|false))\\s*\n(?=\\()", + "begin": "(?x)\n(?\n (?:\n (?:ref\\s+(?:readonly\\s+)?)? # ref return\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n)\\s*\n\\b(?operator)\\b\\s*\n(?[+\\-*/%&|\\^!=~<>]+|true|false)\\s*\n(?=\\()", "beginCaptures": { "1": { "patterns": [ @@ -1478,7 +1583,7 @@ ] }, "6": { - "name": "keyword.other.operator-decl.cs" + "name": "storage.type.operator.cs" }, "7": { "name": "entity.name.function.cs" @@ -1509,7 +1614,7 @@ "match": "\\b(explicit)\\b", "captures": { "1": { - "name": "keyword.other.explicit.cs" + "name": "storage.modifier.explicit.cs" } } }, @@ -1517,14 +1622,14 @@ "match": "\\b(implicit)\\b", "captures": { "1": { - "name": "keyword.other.implicit.cs" + "name": "storage.modifier.implicit.cs" } } } ] }, "2": { - "name": "keyword.other.operator-decl.cs" + "name": "storage.type.operator.cs" }, "3": { "patterns": [ @@ -1607,19 +1712,19 @@ "begin": "(?", + "beginCaptures": { + "0": { + "name": "keyword.operator.arrow.cs" + } + }, + "end": "(?=,|})", + "patterns": [ + { + "include": "#expression" + } + ] + }, + { + "begin": "\\b(when)\\b", + "beginCaptures": { + "1": { + "name": "keyword.control.conditional.when.cs" + } + }, + "end": "(?==>|,|})", + "patterns": [ + { + "include": "#case-guard" + } + ] + }, + { + "begin": "(?!\\s)", + "end": "(?=\\bwhen\\b|=>|,|})", + "patterns": [ + { + "include": "#pattern" + } + ] + } + ] + }, + "case-guard": { + "patterns": [ + { + "include": "#parenthesized-expression" + }, + { + "include": "#expression" + } + ] + }, + "is-expression": { + "begin": "(?=?", + "beginCaptures": { + "0": { + "name": "keyword.operator.relational.cs" + } + }, + "end": "(?=[)}\\],;:?=&|^]|!=|\\b(and|or|when)\\b)", + "patterns": [ + { + "include": "#expression" + } + ] + }, + "var-pattern": { + "begin": "\\b(var)\\b", + "beginCaptures": { + "1": { + "name": "storage.type.var.cs" + } + }, + "end": "(?=[)}\\],;:?=&|^]|!=|\\b(and|or|when)\\b)", + "patterns": [ + { + "include": "#designation-pattern" + } + ] + }, + "designation-pattern": { + "patterns": [ + { + "include": "#intrusive" + }, + { + "begin": "\\(", + "beginCaptures": { + "0": { + "name": "punctuation.parenthesis.open.cs" + } + }, + "end": "\\)", + "endCaptures": { + "0": { + "name": "punctuation.parenthesis.close.cs" + } + }, + "patterns": [ + { + "include": "#punctuation-comma" + }, + { + "include": "#designation-pattern" + } + ] + }, + { + "include": "#simple-designation-pattern" + } + ] + }, + "simple-designation-pattern": { + "patterns": [ + { + "include": "#discard-pattern" + }, + { + "match": "@?[_[:alpha:]][_[:alnum:]]*", + "name": "entity.name.variable.local.cs" + } + ] + }, + "type-pattern": { + "begin": "(?=@?[_[:alpha:]][_[:alnum:]]*)", + "end": "(?=[)}\\],;:?=&|^]|!=|\\b(and|or|when)\\b)", + "patterns": [ + { + "begin": "\\G", + "end": "(?!\\G[@_[:alpha:]])(?=[\\({@_[:alpha:])}\\],;:=&|^]|(?:\\s|^)\\?|!=|\\b(and|or|when)\\b)", + "patterns": [ + { + "include": "#intrusive" + }, + { + "include": "#type-subpattern" + } + ] + }, + { + "begin": "(?=[\\({@_[:alpha:]])", + "end": "(?=[)}\\],;:?=&|^]|!=|\\b(and|or|when)\\b)", + "patterns": [ + { + "include": "#intrusive" + }, + { + "include": "#positional-pattern" + }, + { + "include": "#property-pattern" + }, + { + "include": "#simple-designation-pattern" + } + ] + } + ] + }, + "type-subpattern": { + "patterns": [ + { + "include": "#type-builtin" + }, + { + "begin": "(@?[_[:alpha:]][_[:alnum:]]*)\\s*(::)", + "beginCaptures": { + "1": { + "name": "entity.name.type.alias.cs" + }, + "2": { + "name": "punctuation.separator.coloncolon.cs" + } + }, + "end": "(?<=[_[:alnum:]])|(?=[.<\\[\\({)}\\],;:?=&|^]|!=|\\b(and|or|when)\\b)", + "patterns": [ + { + "include": "#intrusive" + }, + { + "match": "\\@?[_[:alpha:]][_[:alnum:]]*", + "name": "entity.name.type.cs" + } + ] + }, + { + "match": "\\@?[_[:alpha:]][_[:alnum:]]*", + "name": "entity.name.type.cs" + }, + { + "begin": "\\.", + "beginCaptures": { + "0": { + "name": "punctuation.accessor.cs" + } + }, + "end": "(?<=[_[:alnum:]])|(?=[<\\[\\({)}\\],;:?=&|^]|!=|\\b(and|or|when)\\b)", + "patterns": [ + { + "include": "#intrusive" + }, + { + "match": "\\@?[_[:alpha:]][_[:alnum:]]*", + "name": "entity.name.type.cs" + } + ] + }, + { + "include": "#type-arguments" + }, + { + "include": "#type-array-suffix" + }, + { + "match": "(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n)\\s+\n(\\g)\\b\\s*", - "beginCaptures": { - "1": { + }, + { + "begin": "(?<=\\})", + "end": "(?=[)}\\],;:?=&|^]|!=|\\b(and|or|when)\\b)", "patterns": [ { - "include": "#type" - } - ] - }, - "2": { - "name": "entity.name.variable.local.cs" - } - }, - "end": "(?==>)", - "patterns": [ - { - "include": "#comment" - }, - { - "include": "#switch-when-clause" - } - ] - }, - "switch-property-expression": { - "begin": "(?x) # e.g. int x OR var x\n(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n)?\\s*\n(\\{)", - "beginCaptures": { - "1": { - "patterns": [ + "include": "#intrusive" + }, { - "include": "#type" - } - ] - }, - "6": { - "name": "punctuation.curlybrace.open.cs" - } - }, - "end": "\\}", - "endCaptures": { - "0": { - "name": "punctuation.curlybrace.close.cs" - } - }, - "patterns": [ - { - "include": "#expression" - }, - { - "include": "#punctuation-comma" - } - ] - }, - "switch-var-pattern": { - "begin": "(?x) # match foreach (var (x, y) in ...)\n(?:\\b(var)\\b\\s*)\n(?\\((?:[^\\(\\)]|\\g)+\\))\\s*", - "beginCaptures": { - "1": { - "name": "keyword.other.var.cs" - }, - "2": { - "patterns": [ - { - "include": "#tuple-declaration-deconstruction-element-list" + "include": "#simple-designation-pattern" } ] } - }, - "end": "(?==>)", - "patterns": [ - { - "include": "#comment" - }, - { - "include": "#switch-when-clause" - } ] }, - "switch-when-clause": { - "begin": "(?)", + "subpattern": { "patterns": [ { - "include": "#comment" - }, - { - "include": "#expression" - }, - { - "include": "#punctuation-comma" - }, - { - "match": "\\(", - "captures": { - "0": { - "name": "punctuation.parenthesis.open.cs" - } - } - }, - { - "match": "\\)", - "captures": { - "0": { - "name": "punctuation.parenthesis.close.cs" - } - } - } - ] - }, - "switch-label": { - "patterns": [ - { - "begin": "(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n )\n)\\s+\n(\\g)\\s+\n\\b(in)\\b", "captures": { "1": { - "name": "keyword.other.var.cs" + "name": "storage.type.var.cs" }, "2": { "patterns": [ @@ -2176,7 +2719,7 @@ "match": "(?x) # match foreach (var (x, y) in ...)\n(?:\\b(var)\\b\\s*)?\n(?\\((?:[^\\(\\)]|\\g)+\\))\\s+\n\\b(in)\\b", "captures": { "1": { - "name": "keyword.other.var.cs" + "name": "storage.type.var.cs" }, "2": { "patterns": [ @@ -2194,9 +2737,6 @@ "include": "#expression" } ] - }, - { - "include": "#statement" } ] }, @@ -2217,7 +2757,7 @@ "begin": "(?\n (?:\n (?:ref\\s+(?:readonly\\s+)?)? # ref local\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n )\n)\\s+\n(\\g)\\s*\n(?!=>)\n(?=,|;|=|\\))", + "begin": "(?x)\n(?:\n (?:(\\bref)\\s+(?:(\\breadonly)\\s+)?)?(\\bvar\\b)| # ref local\n (?\n (?:\n (?:ref\\s+(?:readonly\\s+)?)? # ref local\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*[?*]\\s*)? # nullable or pointer suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n )\n)\\s+\n(\\g)\\s*\n(?!=>)\n(?=,|;|=|\\))", "beginCaptures": { "1": { - "name": "keyword.other.using.cs" + "name": "storage.modifier.ref.cs" }, "2": { - "name": "storage.modifier.cs" + "name": "storage.modifier.readonly.cs" }, "3": { - "name": "storage.modifier.cs" + "name": "storage.type.var.cs" }, "4": { - "name": "keyword.other.var.cs" - }, - "5": { "patterns": [ { "include": "#type" } ] }, - "10": { + "9": { "name": "entity.name.variable.local.cs" } }, - "end": "(?=;|\\))", + "end": "(?=[;)}])", "patterns": [ { "name": "entity.name.variable.local.cs", @@ -2486,7 +3068,7 @@ "begin": "(?x)\n(?\\b(?:const)\\b)\\s*\n(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n)\\s+\n(\\g)\\s*\n(?=,|;|=)", "beginCaptures": { "1": { - "name": "storage.modifier.cs" + "name": "storage.modifier.const.cs" }, "2": { "patterns": [ @@ -2517,9 +3099,49 @@ ] }, "local-function-declaration": { + "begin": "(?x)\n\\b((?:(?:async|unsafe|static|extern)\\s+)*)\n(?\n (?:ref\\s+(?:readonly\\s+)?)? # ref return\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n \\s*(?:,\\s*)* # commata for multi-dimensional arrays\n \\]\n (?:\\s*\\?)? # arrays can be nullable reference types\n )*\n)\\s+\n(\\g)\\s*\n(<[^<>]+>)?\\s*\n(?=\\()", + "beginCaptures": { + "1": { + "patterns": [ + { + "include": "#storage-modifier" + } + ] + }, + "2": { + "patterns": [ + { + "include": "#type" + } + ] + }, + "7": { + "name": "entity.name.function.cs" + }, + "8": { + "patterns": [ + { + "include": "#type-parameter-list" + } + ] + } + }, + "end": "(?<=\\})|(?=;)", "patterns": [ { - "include": "#method-declaration" + "include": "#comment" + }, + { + "include": "#parenthesized-parameter-list" + }, + { + "include": "#generic-constraints" + }, + { + "include": "#expression-body" + }, + { + "include": "#block" } ] }, @@ -2527,7 +3149,7 @@ "begin": "(?x) # e.g. var (x, y) = GetPoint();\n(?:\\b(var)\\b\\s*)\n(?\\((?:[^\\(\\)]|\\g)+\\))\\s*\n(?=;|=|\\))", "beginCaptures": { "1": { - "name": "keyword.other.var.cs" + "name": "storage.type.var.cs" }, "2": { "patterns": [ @@ -2635,7 +3257,7 @@ "match": "(?x) # e.g. int x OR var x\n(?:\n \\b(var)\\b|\n (?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n )\n)\\s+\n(\\g)\\b\\s*\n(?=[,)\\]])", "captures": { "1": { - "name": "keyword.other.var.cs" + "name": "storage.type.var.cs" }, "2": { "patterns": [ @@ -2653,7 +3275,7 @@ "match": "(?x) # e.g. int x OR var x\n(?:\n \\b(var)\\b|\n (?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n )\n)\\s+\n(\\g)\\b\\s*\n(?=[,)])", "captures": { "1": { - "name": "keyword.other.var.cs" + "name": "storage.type.var.cs" }, "2": { "patterns": [ @@ -2667,16 +3289,13 @@ } } }, - "checked-unchecked-expression": { - "begin": "(?>>?|\\|)?=(?!=|>)", + "beginCaptures": { + "0": { + "patterns": [ + { + "include": "#assignment-operators" + } + ] + } + }, + "end": "(?=[,\\)\\];}])", + "patterns": [ + { + "include": "#ref-modifier" + }, + { + "include": "#expression" + } + ] + }, + "assignment-operators": { "patterns": [ { "name": "keyword.operator.assignment.compound.cs", @@ -3391,11 +4006,19 @@ }, { "name": "keyword.operator.assignment.compound.bitwise.cs", - "match": "\\&=|\\^=|<<=|>>=|\\|=" + "match": "\\&=|\\^=|<<=|>>>?=|\\|=" }, + { + "name": "keyword.operator.assignment.cs", + "match": "\\=" + } + ] + }, + "expression-operators": { + "patterns": [ { "name": "keyword.operator.bitwise.shift.cs", - "match": "<<|>>" + "match": "<<|>>>?" }, { "name": "keyword.operator.comparison.cs", @@ -3413,10 +4036,6 @@ "name": "keyword.operator.bitwise.cs", "match": "\\&|~|\\^|\\|" }, - { - "name": "keyword.operator.assignment.cs", - "match": "\\=" - }, { "name": "keyword.operator.decrement.cs", "match": "--" @@ -3427,56 +4046,50 @@ }, { "name": "keyword.operator.arithmetic.cs", - "match": "%|\\*|/|-|\\+" + "match": "\\+|-(?!>)|\\*|/|%" }, { "name": "keyword.operator.null-coalescing.cs", "match": "\\?\\?" + }, + { + "name": "keyword.operator.range.cs", + "match": "\\.\\." } ] }, - "switch-literal": { - "name": "constant.language.null.cs", - "match": "(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n)?", + "match": "(?x)\n(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?(?!\\?))? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n \\s*(?:,\\s*)* # commata for multi-dimensional arrays\n \\]\n (?:\\s*\\?(?!\\?))? # arrays can be nullable reference types\n )*\n )\n)?", "captures": { "1": { - "name": "keyword.other.as.cs" + "name": "keyword.operator.expression.as.cs" }, "2": { "patterns": [ @@ -3556,34 +4169,20 @@ } } }, - "is-expression": { - "match": "(?x)\n(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n)?", - "captures": { - "1": { - "name": "keyword.other.is.cs" + "language-variable": { + "patterns": [ + { + "name": "variable.language.$1.cs", + "match": "\\b(base|this)\\b" }, - "2": { - "patterns": [ - { - "include": "#type" - } - ] + { + "name": "variable.other.$1.cs", + "match": "\\b(value)\\b" } - } - }, - "this-or-base-expression": { - "match": "\\b(?:(base)|(this))\\b", - "captures": { - "1": { - "name": "keyword.other.base.cs" - }, - "2": { - "name": "keyword.other.this.cs" - } - } + ] }, "invocation-expression": { - "begin": "(?x)\n(?:(\\?)\\s*)? # preceding null-conditional operator?\n(?:(\\.)\\s*)? # preceding dot?\n(@?[_[:alpha:]][_[:alnum:]]*)\\s* # method name\n(?\\s*<([^<>]|\\g)+>\\s*)?\\s* # type arguments\n(?=\\() # open paren of argument list", + "begin": "(?x)\n(?:\n (?:(\\?)\\s*)? # preceding null-conditional operator?\n (\\.)\\s*| # preceding dot?\n (->)\\s* # preceding pointer arrow?\n)?\n(@?[_[:alpha:]][_[:alnum:]]*)\\s* # method name\n(\n <\n (?\n [^<>()]+|\n <\\g+>|\n \\(\\g+\\)\n )+\n >\\s*\n)? # type arguments\n(?=\\() # open paren of argument list", "beginCaptures": { "1": { "name": "keyword.operator.null-conditional.cs" @@ -3592,9 +4191,12 @@ "name": "punctuation.accessor.cs" }, "3": { - "name": "entity.name.function.cs" + "name": "punctuation.accessor.pointer.cs" }, "4": { + "name": "entity.name.function.cs" + }, + "5": { "patterns": [ { "include": "#type-arguments" @@ -3610,7 +4212,7 @@ ] }, "element-access-expression": { - "begin": "(?x)\n(?:(\\?)\\s*)? # preceding null-conditional operator?\n(?:(\\.)\\s*)? # preceding dot?\n(?:(@?[_[:alpha:]][_[:alnum:]]*)\\s*)? # property name\n(?:(\\?)\\s*)? # null-conditional operator?\n(?=\\[) # open bracket of argument list", + "begin": "(?x)\n(?:\n (?:(\\?)\\s*)? # preceding null-conditional operator?\n (\\.)\\s*| # preceding dot?\n (->)\\s* # preceding pointer arrow?\n)?\n(?:(@?[_[:alpha:]][_[:alnum:]]*)\\s*)? # property name\n(?:(\\?)\\s*)? # null-conditional operator?\n(?=\\[) # open bracket of argument list", "beginCaptures": { "1": { "name": "keyword.operator.null-conditional.cs" @@ -3619,9 +4221,12 @@ "name": "punctuation.accessor.cs" }, "3": { - "name": "variable.other.object.property.cs" + "name": "punctuation.accessor.pointer.cs" }, "4": { + "name": "variable.other.object.property.cs" + }, + "5": { "name": "keyword.operator.null-conditional.cs" } }, @@ -3635,7 +4240,7 @@ "member-access-expression": { "patterns": [ { - "match": "(?x)\n(?:(\\?)\\s*)? # preceding null-conditional operator?\n(\\.)\\s* # preceding dot\n(@?[_[:alpha:]][_[:alnum:]]*)\\s* # property name\n(?![_[:alnum:]]|\\(|(\\?)?\\[|<) # next character is not alpha-numeric, nor a (, [, or <. Also, test for ?[", + "match": "(?x)\n(?:\n (?:(\\?)\\s*)? # preceding null-conditional operator?\n (\\.)\\s*| # preceding dot?\n (->)\\s* # preceding pointer arrow?\n)\n(@?[_[:alpha:]][_[:alnum:]]*)\\s* # property name\n(?![_[:alnum:]]|\\(|(\\?)?\\[|<) # next character is not alpha-numeric, nor a (, [, or <. Also, test for ?[", "captures": { "1": { "name": "keyword.operator.null-conditional.cs" @@ -3644,6 +4249,9 @@ "name": "punctuation.accessor.cs" }, "3": { + "name": "punctuation.accessor.pointer.cs" + }, + "4": { "name": "variable.other.object.property.cs" } } @@ -3667,7 +4275,7 @@ } }, { - "match": "(?x)\n(@?[_[:alpha:]][_[:alnum:]]*)\n(?=\n (\\s*\\?)?\n \\s*\\.\\s*@?[_[:alpha:]][_[:alnum:]]*\n)", + "match": "(?x)\n(@?[_[:alpha:]][_[:alnum:]]*)\n(?=\n \\s*(?:(?:\\?\\s*)?\\.|->)\n \\s*@?[_[:alpha:]][_[:alnum:]]*\n)", "captures": { "1": { "name": "variable.other.object.cs" @@ -3690,7 +4298,7 @@ "begin": "(?x)\n(new)(?:\\s+\n(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n))?\\s*\n(?=\\()", "beginCaptures": { "1": { - "name": "keyword.other.new.cs" + "name": "keyword.operator.expression.new.cs" }, "2": { "patterns": [ @@ -3708,10 +4316,10 @@ ] }, "object-creation-expression-with-no-parameters": { - "match": "(?x)\n(new)\\s+\n(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n)\\s*\n(?=\\{|$)", + "match": "(?x)\n(new)\\s+\n(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n)\\s*\n(?=\\{|//|/\\*|$)", "captures": { "1": { - "name": "keyword.other.new.cs" + "name": "keyword.operator.expression.new.cs" }, "2": { "patterns": [ @@ -3726,7 +4334,7 @@ "begin": "(?x)\n\\b(new|stackalloc)\\b\\s*\n(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n)?\\s*\n(?=\\[)", "beginCaptures": { "1": { - "name": "keyword.other.new.cs" + "name": "keyword.operator.expression.$1.cs" }, "2": { "patterns": [ @@ -3744,14 +4352,17 @@ ] }, "anonymous-object-creation-expression": { - "begin": "\\b(new)\\b\\s*(?=\\{|$)", + "begin": "\\b(new)\\b\\s*(?=\\{|//|/\\*|$)", "beginCaptures": { "1": { - "name": "keyword.other.new.cs" + "name": "keyword.operator.expression.new.cs" } }, "end": "(?<=\\})", "patterns": [ + { + "include": "#comment" + }, { "include": "#initializer-expression" } @@ -3829,7 +4440,7 @@ "match": "(?x)\n(?:(?:\\b(ref|params|out|in|this)\\b)\\s+)?\n(?\n (?:\n (?:ref\\s+)? # ref return\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n)\\s+\n(\\g)", "captures": { "1": { - "name": "storage.modifier.cs" + "name": "storage.modifier.$1.cs" }, "2": { "patterns": [ @@ -3913,11 +4524,25 @@ "argument": { "patterns": [ { - "name": "storage.modifier.cs", - "match": "\\b(ref|out|in)\\b" + "name": "storage.modifier.$1.cs", + "match": "\\b(ref|in)\\b" }, { - "include": "#declaration-expression-local" + "begin": "\\b(out)\\b", + "beginCaptures": { + "1": { + "name": "storage.modifier.out.cs" + } + }, + "end": "(?=,|\\)|\\])", + "patterns": [ + { + "include": "#declaration-expression-local" + }, + { + "include": "#expression" + } + ] }, { "include": "#expression" @@ -3928,7 +4553,7 @@ "begin": "(?x)\n\\b(from)\\b\\s*\n(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n)?\n\\s+(\\g)\\b\\s*\n\\b(in)\\b\\s*", "beginCaptures": { "1": { - "name": "keyword.query.from.cs" + "name": "keyword.operator.expression.query.from.cs" }, "2": { "patterns": [ @@ -3941,7 +4566,7 @@ "name": "entity.name.variable.range-variable.cs" }, "8": { - "name": "keyword.query.in.cs" + "name": "keyword.operator.expression.query.in.cs" } }, "end": "(?=;|\\))", @@ -3980,7 +4605,7 @@ "begin": "(?x)\n\\b(let)\\b\\s*\n(@?[_[:alpha:]][_[:alnum:]]*)\\b\\s*\n(=)\\s*", "beginCaptures": { "1": { - "name": "keyword.query.let.cs" + "name": "keyword.operator.expression.query.let.cs" }, "2": { "name": "entity.name.variable.range-variable.cs" @@ -4003,7 +4628,7 @@ "begin": "(?x)\n\\b(where)\\b\\s*", "beginCaptures": { "1": { - "name": "keyword.query.where.cs" + "name": "keyword.operator.expression.query.where.cs" } }, "end": "(?=;|\\))", @@ -4020,7 +4645,7 @@ "begin": "(?x)\n\\b(join)\\b\\s*\n(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n)?\n\\s+(\\g)\\b\\s*\n\\b(in)\\b\\s*", "beginCaptures": { "1": { - "name": "keyword.query.join.cs" + "name": "keyword.operator.expression.query.join.cs" }, "2": { "patterns": [ @@ -4033,7 +4658,7 @@ "name": "entity.name.variable.range-variable.cs" }, "8": { - "name": "keyword.query.in.cs" + "name": "keyword.operator.expression.query.in.cs" } }, "end": "(?=;|\\))", @@ -4059,7 +4684,7 @@ "match": "\\b(on)\\b\\s*", "captures": { "1": { - "name": "keyword.query.on.cs" + "name": "keyword.operator.expression.query.on.cs" } } }, @@ -4067,7 +4692,7 @@ "match": "\\b(equals)\\b\\s*", "captures": { "1": { - "name": "keyword.query.equals.cs" + "name": "keyword.operator.expression.query.equals.cs" } } }, @@ -4075,7 +4700,7 @@ "match": "(?x)\n\\b(into)\\b\\s*\n(@?[_[:alpha:]][_[:alnum:]]*)\\b\\s*", "captures": { "1": { - "name": "keyword.query.into.cs" + "name": "keyword.operator.expression.query.into.cs" }, "2": { "name": "entity.name.variable.range-variable.cs" @@ -4086,7 +4711,7 @@ "begin": "\\b(orderby)\\b\\s*", "beginCaptures": { "1": { - "name": "keyword.query.orderby.cs" + "name": "keyword.operator.expression.query.orderby.cs" } }, "end": "(?=;|\\))", @@ -4106,13 +4731,10 @@ ] }, "ordering-direction": { - "match": "\\b(?:(ascending)|(descending))\\b", + "match": "\\b(ascending|descending)\\b", "captures": { "1": { - "name": "keyword.query.ascending.cs" - }, - "2": { - "name": "keyword.query.descending.cs" + "name": "keyword.operator.expression.query.$1.cs" } } }, @@ -4120,7 +4742,7 @@ "begin": "\\b(select)\\b\\s*", "beginCaptures": { "1": { - "name": "keyword.query.select.cs" + "name": "keyword.operator.expression.query.select.cs" } }, "end": "(?=;|\\))", @@ -4137,7 +4759,7 @@ "begin": "\\b(group)\\b\\s*", "beginCaptures": { "1": { - "name": "keyword.query.group.cs" + "name": "keyword.operator.expression.query.group.cs" } }, "end": "(?=;|\\))", @@ -4160,7 +4782,7 @@ "match": "\\b(by)\\b\\s*", "captures": { "1": { - "name": "keyword.query.by.cs" + "name": "keyword.operator.expression.query.by.cs" } } }, @@ -4168,7 +4790,7 @@ "match": "(?x)\n\\b(into)\\b\\s*\n(@?[_[:alpha:]][_[:alnum:]]*)\\b\\s*", "captures": { "1": { - "name": "keyword.query.into.cs" + "name": "keyword.operator.expression.query.into.cs" }, "2": { "name": "entity.name.variable.range-variable.cs" @@ -4178,10 +4800,15 @@ "anonymous-method-expression": { "patterns": [ { - "begin": "(?x)\n(?:\\b(async)\\b\\s*)?\n(@?[_[:alpha:]][_[:alnum:]]*)\\b\\s*\n(=>)", + "begin": "(?x)\n((?:\\b(?:async|static)\\b\\s*)*)\n(@?[_[:alpha:]][_[:alnum:]]*)\\b\\s*\n(=>)", "beginCaptures": { "1": { - "name": "storage.modifier.cs" + "patterns": [ + { + "match": "async|static", + "name": "storage.modifier.$0.cs" + } + ] }, "2": { "name": "entity.name.variable.parameter.cs" @@ -4204,10 +4831,15 @@ ] }, { - "begin": "(?x)\n(?:\\b(async)\\b\\s*)?\n(\\(.*?\\))\\s*\n(=>)", + "begin": "(?x)\n((?:\\b(?:async|static)\\b\\s*)*)\n(?\n \\(\n (?:[^()]|\\g)*\n \\)\n)\\s*\n(=>)", "beginCaptures": { "1": { - "name": "storage.modifier.cs" + "patterns": [ + { + "match": "async|static", + "name": "storage.modifier.$0.cs" + } + ] }, "2": { "patterns": [ @@ -4234,13 +4866,18 @@ ] }, { - "begin": "(?x)\n(?:\\b(async)\\b\\s*)?\n(?:\\b(delegate)\\b\\s*)", + "begin": "(?x)\n((?:\\b(?:async|static)\\b\\s*)*)\n(?:\\b(delegate)\\b\\s*)", "beginCaptures": { "1": { - "name": "storage.modifier.cs" + "patterns": [ + { + "match": "async|static", + "name": "storage.modifier.$0.cs" + } + ] }, "2": { - "name": "keyword.other.delegate.cs" + "name": "storage.type.delegate.cs" } }, "end": "(?=\\)|;|}|,)", @@ -4250,9 +4887,6 @@ }, { "include": "#block" - }, - { - "include": "#expression" } ] } @@ -4290,7 +4924,7 @@ "match": "(?x)\n(?:\\b(ref|out|in)\\b)?\\s*\n(?:(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n)\\s+)?\n(\\g)\\b\\s*\n(?=[,)])", "captures": { "1": { - "name": "storage.modifier.cs" + "name": "storage.modifier.$1.cs" }, "2": { "patterns": [ @@ -4305,7 +4939,6 @@ } }, "type": { - "name": "meta.type.cs", "patterns": [ { "include": "#comment" @@ -4333,16 +4966,19 @@ }, { "include": "#type-nullable-suffix" + }, + { + "include": "#type-pointer-suffix" } ] }, "ref-modifier": { - "name": "storage.modifier.cs", - "match": "\\b(ref)\\b" + "name": "storage.modifier.ref.cs", + "match": "\\bref\\b" }, "readonly-modifier": { - "name": "storage.modifier.cs", - "match": "\\b(readonly)\\b" + "name": "storage.modifier.readonly.cs", + "match": "\\breadonly\\b" }, "tuple-type": { "begin": "\\(", @@ -4382,10 +5018,10 @@ } }, "type-builtin": { - "match": "\\b(bool|byte|char|decimal|double|float|int|long|object|sbyte|short|string|uint|ulong|ushort|void|dynamic)\\b", + "match": "\\b(bool|s?byte|u?short|n?u?int|u?long|float|double|decimal|char|string|object|void|dynamic)\\b", "captures": { "1": { - "name": "keyword.type.cs" + "name": "keyword.type.$1.cs" } } }, @@ -4444,9 +5080,6 @@ } }, "patterns": [ - { - "include": "#comment" - }, { "include": "#type" }, @@ -4469,6 +5102,9 @@ } }, "patterns": [ + { + "include": "#intrusive" + }, { "include": "#punctuation-comma" } @@ -4476,11 +5112,11 @@ }, "type-nullable-suffix": { "match": "\\?", - "captures": { - "0": { - "name": "punctuation.separator.question-mark.cs" - } - } + "name": "punctuation.separator.question-mark.cs" + }, + "type-pointer-suffix": { + "match": "\\*", + "name": "punctuation.separator.asterisk.cs" }, "operator-assignment": { "name": "keyword.operator.assignment.cs", @@ -4498,6 +5134,16 @@ "name": "punctuation.accessor.cs", "match": "\\." }, + "intrusive": { + "patterns": [ + { + "include": "#preprocessor" + }, + { + "include": "#comment" + } + ] + }, "preprocessor": { "name": "meta.preprocessor.cs", "begin": "^\\s*(\\#)\\s*", @@ -4803,38 +5449,47 @@ "comment": { "patterns": [ { - "name": "comment.block.cs", - "begin": "/\\*", - "beginCaptures": { - "0": { - "name": "punctuation.definition.comment.cs" - } - }, - "end": "\\*/", - "endCaptures": { - "0": { - "name": "punctuation.definition.comment.cs" - } - } - }, - { - "begin": "(^\\s+)?(?=//)", - "beginCaptures": { + "name": "comment.block.documentation.cs", + "begin": "(^\\s+)?(///)(?!/)", + "while": "^(\\s*)(///)(?!/)", + "captures": { "1": { "name": "punctuation.whitespace.comment.leading.cs" + }, + "2": { + "name": "punctuation.definition.comment.cs" } }, - "end": "(?=$)", "patterns": [ { - "name": "comment.block.documentation.cs", - "begin": "(?((prev, selection) => + const rangesToUpdate = editor.selections + .reduceRight((prev, selection) => prev.concat(getRangesToUpdate(document, selection, rootNode)), []); if (!rangesToUpdate.length) { return; diff --git a/extensions/extension-editing/package.json b/extensions/extension-editing/package.json index 5d6d342783f..f45105b99d4 100644 --- a/extensions/extension-editing/package.json +++ b/extensions/extension-editing/package.json @@ -26,7 +26,7 @@ "watch": "gulp watch-extension:extension-editing" }, "dependencies": { - "jsonc-parser": "^2.2.1", + "jsonc-parser": "^3.2.0", "markdown-it": "^12.3.2", "parse5": "^3.0.2" }, diff --git a/extensions/extension-editing/src/constants.ts b/extensions/extension-editing/src/constants.ts new file mode 100644 index 00000000000..1be4d0e12e5 --- /dev/null +++ b/extensions/extension-editing/src/constants.ts @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { l10n } from 'vscode'; + +export const implicitActivationEvent = l10n.t("This activation event cannot be explicitly listed by your extension."); +export const redundantImplicitActivationEvent = l10n.t("This activation event can be removed as VS Code generates these automatically from your package.json contribution declarations."); diff --git a/extensions/extension-editing/src/extensionEditingMain.ts b/extensions/extension-editing/src/extensionEditingMain.ts index 9dc71a46896..c056fbfa975 100644 --- a/extensions/extension-editing/src/extensionEditingMain.ts +++ b/extensions/extension-editing/src/extensionEditingMain.ts @@ -12,10 +12,12 @@ export function activate(context: vscode.ExtensionContext) { //package.json suggestions context.subscriptions.push(registerPackageDocumentCompletions()); + //package.json code actions for lint warnings + context.subscriptions.push(registerCodeActionsProvider()); + context.subscriptions.push(new ExtensionLinter()); } - function registerPackageDocumentCompletions(): vscode.Disposable { return vscode.languages.registerCompletionItemProvider({ language: 'json', pattern: '**/package.json' }, { provideCompletionItems(document, position, token) { @@ -23,3 +25,11 @@ function registerPackageDocumentCompletions(): vscode.Disposable { } }); } + +function registerCodeActionsProvider(): vscode.Disposable { + return vscode.languages.registerCodeActionsProvider({ language: 'json', pattern: '**/package.json' }, { + provideCodeActions(document, range, context, token) { + return new PackageDocument(document).provideCodeActions(range, context, token); + } + }); +} diff --git a/extensions/extension-editing/src/extensionLinter.ts b/extensions/extension-editing/src/extensionLinter.ts index 459bd9a8e16..8bb2a4640df 100644 --- a/extensions/extension-editing/src/extensionLinter.ts +++ b/extensions/extension-editing/src/extensionLinter.ts @@ -13,6 +13,7 @@ import * as MarkdownItType from 'markdown-it'; import { commands, languages, workspace, Disposable, TextDocument, Uri, Diagnostic, Range, DiagnosticSeverity, Position, env, l10n } from 'vscode'; import { INormalizedVersion, normalizeVersion, parseVersion } from './extensionEngineValidation'; import { JsonStringScanner } from './jsonReconstruct'; +import { implicitActivationEvent, redundantImplicitActivationEvent } from './constants'; const product = JSON.parse(fs.readFileSync(path.join(env.appRoot, 'product.json'), { encoding: 'utf-8' })); const allowedBadgeProviders: string[] = (product.extensionAllowedBadgeProviders || []).map((s: string) => s.toLowerCase()); @@ -32,8 +33,6 @@ const dataUrlsNotValid = l10n.t("Data URLs are not a valid image source."); const relativeUrlRequiresHttpsRepository = l10n.t("Relative image URLs require a repository with HTTPS protocol to be specified in the package.json."); const relativeBadgeUrlRequiresHttpsRepository = l10n.t("Relative badge URLs require a repository with HTTPS protocol to be specified in this package.json."); const apiProposalNotListed = l10n.t("This proposal cannot be used because for this extension the product defines a fixed set of API proposals. You can test your extension but before publishing you MUST reach out to the VS Code team."); -const implicitActivationEvent = l10n.t("This activation event cannot be explicitly listed by your extension."); -const redundantImplicitActivationEvent = l10n.t("This activation event can be removed as VS Code generates these automatically from your package.json contribution declarations."); const bumpEngineForImplicitActivationEvents = l10n.t("This activation event can be removed for extensions targeting engine version ^1.75 as VS Code will generate these automatically from your package.json contribution declarations."); const starActivation = l10n.t("Using '*' activation is usually a bad idea as it impacts performance."); const parsingErrorHeader = l10n.t("Error parsing the when-clause:"); @@ -128,7 +127,7 @@ export class ExtensionLinter { const tree = parseTree(document.getText()); const info = this.readPackageJsonInfo(this.getUriFolder(document.uri), tree); - if (info.isExtension) { + if (tree && info.isExtension) { const icon = findNodeAtLocation(tree, ['icon']); if (icon && icon.type === 'string') { diff --git a/extensions/extension-editing/src/packageDocumentHelper.ts b/extensions/extension-editing/src/packageDocumentHelper.ts index 67163900b5b..5febab27ea9 100644 --- a/extensions/extension-editing/src/packageDocumentHelper.ts +++ b/extensions/extension-editing/src/packageDocumentHelper.ts @@ -5,6 +5,7 @@ import * as vscode from 'vscode'; import { getLocation, Location } from 'jsonc-parser'; +import { implicitActivationEvent, redundantImplicitActivationEvent } from './constants'; export class PackageDocument { @@ -21,6 +22,24 @@ export class PackageDocument { return undefined; } + public provideCodeActions(_range: vscode.Range, context: vscode.CodeActionContext, _token: vscode.CancellationToken): vscode.ProviderResult { + const codeActions: vscode.CodeAction[] = []; + for (const diagnostic of context.diagnostics) { + if (diagnostic.message === implicitActivationEvent || diagnostic.message === redundantImplicitActivationEvent) { + const codeAction = new vscode.CodeAction(vscode.l10n.t("Remove activation event"), vscode.CodeActionKind.QuickFix); + codeAction.edit = new vscode.WorkspaceEdit(); + const rangeForCharAfter = diagnostic.range.with(diagnostic.range.end, diagnostic.range.end.translate(0, 1)); + if (this.document.getText(rangeForCharAfter) === ',') { + codeAction.edit.delete(this.document.uri, diagnostic.range.with(undefined, diagnostic.range.end.translate(0, 1))); + } else { + codeAction.edit.delete(this.document.uri, diagnostic.range); + } + codeActions.push(codeAction); + } + } + return codeActions; + } + private provideLanguageOverridesCompletionItems(location: Location, position: vscode.Position): vscode.ProviderResult { let range = this.getReplaceRange(location, position); const text = this.document.getText(range); diff --git a/extensions/extension-editing/yarn.lock b/extensions/extension-editing/yarn.lock index ff42c956777..5456b3ec040 100644 --- a/extensions/extension-editing/yarn.lock +++ b/extensions/extension-editing/yarn.lock @@ -27,15 +27,15 @@ entities@~2.1.0: resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== -jsonc-parser@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.2.1.tgz#db73cd59d78cce28723199466b2a03d1be1df2bc" - integrity sha512-o6/yDBYccGvTz1+QFevz6l6OBZ2+fMVu2JZ9CIhzsYRX4mjaK5IyX9eldUdCmga16zlgQxyrj5pt9kzuj2C02w== +jsonc-parser@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz#31ff3f4c2b9793f89c67212627c51c6394f88e76" + integrity sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w== linkify-it@^3.0.1: - version "3.0.2" - resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-3.0.2.tgz#f55eeb8bc1d3ae754049e124ab3bb56d97797fb8" - integrity sha512-gDBO4aHNZS6coiZCKVhSNh43F9ioIL4JwRjLZPkoLIY4yZFwg264Y5lu2x6rb1Js42Gh6Yqm2f6L2AJcnkzinQ== + version "3.0.3" + resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-3.0.3.tgz#a98baf44ce45a550efb4d49c769d07524cc2fa2e" + integrity sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ== dependencies: uc.micro "^1.0.1" diff --git a/extensions/git/package.json b/extensions/git/package.json index 9ad1978be0e..f236c524faf 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -2465,7 +2465,7 @@ "null" ], "default": 50, - "description": "%config.inputValidationSubjectLength%" + "markdownDescription": "%config.inputValidationSubjectLength%" }, "git.detectSubmodules": { "type": "boolean", @@ -2659,7 +2659,7 @@ "description": "%config.useIntegratedAskPass%" }, "git.githubAuthentication": { - "deprecationMessage": "This setting is now deprecated, please use `github.gitAuthentication` instead." + "markdownDeprecationMessage": "This setting is now deprecated, please use `#github.gitAuthentication#` instead." }, "git.timeline.date": { "type": "string", diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 24f03414ce6..de8f10e7174 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -191,7 +191,7 @@ "config.showPushSuccessNotification": "Controls whether to show a notification when a push is successful.", "config.inputValidation": "Controls when to show commit message input validation.", "config.inputValidationLength": "Controls the commit message length threshold for showing a warning.", - "config.inputValidationSubjectLength": "Controls the commit message subject length threshold for showing a warning. Unset it to inherit the value of `config.inputValidationLength`.", + "config.inputValidationSubjectLength": "Controls the commit message subject length threshold for showing a warning. Unset it to inherit the value of `#git.inputValidationLength#`.", "config.detectSubmodules": "Controls whether to automatically detect git submodules.", "config.detectSubmodulesLimit": "Controls the limit of git submodules detected.", "config.alwaysShowStagedChangesResourceGroup": "Always show the Staged Changes resource group.", diff --git a/extensions/git/src/model.ts b/extensions/git/src/model.ts index 03a6ccf18ad..fef934fd506 100644 --- a/extensions/git/src/model.ts +++ b/extensions/git/src/model.ts @@ -495,7 +495,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi this.logger.trace(`Opening repository: ${repoPath}`); const existingRepository = await this.getRepositoryExact(repoPath); if (existingRepository) { - this.logger.trace(`Repository for path ${repoPath} already exists: ${existingRepository.root})`); + this.logger.trace(`Repository for path ${repoPath} already exists: ${existingRepository.root}`); return; } diff --git a/extensions/html-language-features/package.json b/extensions/html-language-features/package.json index 422fce5e7f6..a18aed6e955 100644 --- a/extensions/html-language-features/package.json +++ b/extensions/html-language-features/package.json @@ -54,7 +54,7 @@ "%html.completion.attributeDefaultValue.empty%" ], "default": "doublequotes", - "description": "%html.completion.attributeDefaultValue%" + "markdownDescription": "%html.completion.attributeDefaultValue%" }, "html.customData": { "type": "array", diff --git a/extensions/html-language-features/package.nls.json b/extensions/html-language-features/package.nls.json index acb6474d63e..f36ecf34f02 100644 --- a/extensions/html-language-features/package.nls.json +++ b/extensions/html-language-features/package.nls.json @@ -21,7 +21,7 @@ "html.format.wrapAttributes.preservealigned": "Preserve wrapping of attributes but align.", "html.format.templating.desc": "Honor django, erb, handlebars and php templating language tags.", "html.format.unformattedContentDelimiter.desc": "Keep text content together between this string.", - "html.format.wrapAttributesIndentSize.desc": "Indent wrapped attributes to after N characters. Use `null` to use the default indent size. Ignored if `#html.format.wrapAttributes#` is set to 'aligned'.", + "html.format.wrapAttributesIndentSize.desc": "Indent wrapped attributes to after N characters. Use `null` to use the default indent size. Ignored if `#html.format.wrapAttributes#` is set to `aligned`.", "html.suggest.html5.desc": "Controls whether the built-in HTML language support suggests HTML5 tags, properties and values.", "html.trace.server.desc": "Traces the communication between VS Code and the HTML language server.", "html.validate.scripts": "Controls whether the built-in HTML language support validates embedded scripts.", diff --git a/extensions/ipynb/src/ipynbMain.ts b/extensions/ipynb/src/ipynbMain.ts index 1d61f8a1cae..c256e3b4f65 100644 --- a/extensions/ipynb/src/ipynbMain.ts +++ b/extensions/ipynb/src/ipynbMain.ts @@ -24,7 +24,7 @@ type NotebookMetadata = { pygments_lexer?: string; [propName: string]: unknown; }; - orig_nbformat: number; + orig_nbformat?: number; [propName: string]: unknown; }; @@ -76,9 +76,7 @@ export function activate(context: vscode.ExtensionContext) { data.metadata = { custom: { cells: [], - metadata: { - orig_nbformat: 4 - }, + metadata: {}, nbformat: 4, nbformat_minor: 2 } diff --git a/extensions/ipynb/src/notebookSerializer.ts b/extensions/ipynb/src/notebookSerializer.ts index 968c2738ed4..26cc2b44232 100644 --- a/extensions/ipynb/src/notebookSerializer.ts +++ b/extensions/ipynb/src/notebookSerializer.ts @@ -63,7 +63,7 @@ export class NotebookSerializer implements vscode.NotebookSerializer { // For notebooks without metadata default the language in metadata to the preferred language. if (!json.metadata || (!json.metadata.kernelspec && !json.metadata.language_info)) { - json.metadata = json.metadata || { orig_nbformat: defaultNotebookFormat.major }; + json.metadata = json.metadata || {}; json.metadata.language_info = json.metadata.language_info || { name: preferredCellLanguage }; } @@ -101,8 +101,8 @@ export class NotebookSerializer implements vscode.NotebookSerializer { export function getNotebookMetadata(document: vscode.NotebookDocument | vscode.NotebookData) { const notebookContent: Partial = document.metadata?.custom || {}; notebookContent.cells = notebookContent.cells || []; - notebookContent.nbformat = notebookContent.nbformat || 4; - notebookContent.nbformat_minor = notebookContent.nbformat_minor ?? 2; - notebookContent.metadata = notebookContent.metadata || { orig_nbformat: 4 }; + notebookContent.nbformat = notebookContent.nbformat || defaultNotebookFormat.major; + notebookContent.nbformat_minor = notebookContent.nbformat_minor ?? defaultNotebookFormat.minor; + notebookContent.metadata = notebookContent.metadata || {}; return notebookContent; } diff --git a/extensions/java/cgmanifest.json b/extensions/java/cgmanifest.json index be9e7439fe7..72e0c5c882b 100644 --- a/extensions/java/cgmanifest.json +++ b/extensions/java/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "redhat-developer/vscode-java", "repositoryUrl": "https://github.com/redhat-developer/vscode-java", - "commitHash": "5fb57e8e1c5d776b21be13cd7227b25b87edf4a6" + "commitHash": "5d224a552cf5f0f8ebccf69e43e2575ed2c13839" } }, "license": "MIT", @@ -44,7 +44,7 @@ "suitability for any purpose." ], "description": "This grammar was derived from https://github.com/atom/language-java/blob/master/grammars/java.cson.", - "version": "1.21.0" + "version": "1.22.0" } ], "version": 1 diff --git a/extensions/java/syntaxes/java.tmLanguage.json b/extensions/java/syntaxes/java.tmLanguage.json index 337545b0233..7f969ccb787 100644 --- a/extensions/java/syntaxes/java.tmLanguage.json +++ b/extensions/java/syntaxes/java.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/redhat-developer/vscode-java/commit/5fb57e8e1c5d776b21be13cd7227b25b87edf4a6", + "version": "https://github.com/redhat-developer/vscode-java/commit/5d224a552cf5f0f8ebccf69e43e2575ed2c13839", "name": "Java", "scopeName": "source.java", "patterns": [ diff --git a/extensions/julia/cgmanifest.json b/extensions/julia/cgmanifest.json index 0dac126a5ae..d546ac2db12 100644 --- a/extensions/julia/cgmanifest.json +++ b/extensions/julia/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "JuliaEditorSupport/atom-language-julia", "repositoryUrl": "https://github.com/JuliaEditorSupport/atom-language-julia", - "commitHash": "ccc0277c9ee9af34a0b50e5fa27a6f5191601b8c" + "commitHash": "7cbe6a7c4f2c8275e15f5b6e0722d285730ffb99" } }, "license": "MIT", diff --git a/extensions/julia/syntaxes/julia.tmLanguage.json b/extensions/julia/syntaxes/julia.tmLanguage.json index c4e146ee5e7..66998399c4b 100644 --- a/extensions/julia/syntaxes/julia.tmLanguage.json +++ b/extensions/julia/syntaxes/julia.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/JuliaEditorSupport/atom-language-julia/commit/ccc0277c9ee9af34a0b50e5fa27a6f5191601b8c", + "version": "https://github.com/JuliaEditorSupport/atom-language-julia/commit/7cbe6a7c4f2c8275e15f5b6e0722d285730ffb99", "name": "Julia", "scopeName": "source.julia", "comment": "This grammar is used by Atom (Oniguruma), GitHub (PCRE), and VSCode (Oniguruma),\nso all regexps must be compatible with both engines.\n\nSpecs:\n- https://github.com/kkos/oniguruma/blob/master/doc/RE\n- https://www.pcre.org/current/doc/html/", @@ -356,13 +356,16 @@ "name": "keyword.operator.shift.julia" }, { - "match": "(?:\\s*(::|>:|<:)\\s*((?:(?:Union)?\\([^)]*\\)|[[:alpha:]_$∇][[:word:]⁺-ₜ!′\\.]*(?:(?:{(?:[^{}]|{(?:[^{}]|{[^{}]*})*})*})|(?:\".+?(?:|<:)\\s*((?:(?:Union)?\\([^)]*\\)|[[:alpha:]_$∇][[:word:]⁺-ₜ!′\\.]*(?:(?:{(?:[^{}]|{(?:[^{}]|{[^{}]*})*})*})|(?:\".+?(?\\?\\[\\]\\^\\_\\`\\|]+)", + "begin": "((@)(?i:preamble))\\s*(\\()\\s*", + "beginCaptures": { + "1": { + "name": "keyword.other.preamble.bibtex" + }, + "2": { + "name": "punctuation.definition.keyword.bibtex" + }, + "3": { + "name": "punctuation.section.preamble.begin.bibtex" + } + }, + "end": "\\)", + "endCaptures": { + "0": { + "name": "punctuation.section.preamble.end.bibtex" + } + }, + "name": "meta.preamble.parenthesis.bibtex", + "patterns": [ + { + "include": "#field_value" + } + ] + }, + { + "begin": "((@)(?i:string))\\s*(\\{)\\s*([a-zA-Z!$&*+\\-./:;<>?@\\[\\\\\\]^_`|~][a-zA-Z0-9!$&*+\\-./:;<>?@\\[\\\\\\]^_`|~]*)", "beginCaptures": { "1": { "name": "keyword.other.string-constant.bibtex" @@ -51,12 +95,12 @@ "name": "meta.string-constant.braces.bibtex", "patterns": [ { - "include": "#string_content" + "include": "#field_value" } ] }, { - "begin": "((@)(?i:string))\\s*(\\()\\s*([a-zA-Z0-9\\!\\$\\&\\*\\+\\-\\.\\/\\:\\;\\<\\>\\?\\[\\]\\^\\_\\`\\|]+)", + "begin": "((@)(?i:string))\\s*(\\()\\s*([a-zA-Z!$&*+\\-./:;<>?@\\[\\\\\\]^_`|~][a-zA-Z0-9!$&*+\\-./:;<>?@\\[\\\\\\]^_`|~]*)", "beginCaptures": { "1": { "name": "keyword.other.string-constant.bibtex" @@ -80,12 +124,12 @@ "name": "meta.string-constant.parenthesis.bibtex", "patterns": [ { - "include": "#string_content" + "include": "#field_value" } ] }, { - "begin": "((@)[a-zA-Z]+)\\s*(\\{)\\s*([^\\s,]*)", + "begin": "((@)[a-zA-Z!$&*+\\-./:;<>?@\\[\\\\\\]^_`|~][a-zA-Z0-9!$&*+\\-./:;<>?@\\[\\\\\\]^_`|~]*)\\s*(\\{)\\s*([^\\s,}]*)", "beginCaptures": { "1": { "name": "keyword.other.entry-type.bibtex" @@ -109,13 +153,7 @@ "name": "meta.entry.braces.bibtex", "patterns": [ { - "include": "#percentage_comment" - }, - { - "include": "#url_field" - }, - { - "begin": "([a-zA-Z0-9\\!\\$\\&\\*\\+\\-\\.\\/\\:\\;\\<\\>\\?\\[\\]\\^\\_\\`\\|]+)\\s*(\\=)", + "begin": "([a-zA-Z!$&*+\\-./:;<>?@\\[\\\\\\]^_`|~][a-zA-Z0-9!$&*+\\-./:;<>?@\\[\\\\\\]^_`|~]*)\\s*(\\=)", "beginCaptures": { "1": { "name": "support.function.key.bibtex" @@ -128,23 +166,14 @@ "name": "meta.key-assignment.bibtex", "patterns": [ { - "include": "#percentage_comment" - }, - { - "include": "#integer" - }, - { - "include": "#string_content" - }, - { - "include": "#string_var" + "include": "#field_value" } ] } ] }, { - "begin": "((@)[a-zA-Z]+)\\s*(\\()\\s*([^\\s,]*)", + "begin": "((@)[a-zA-Z!$&*+\\-./:;<>?@\\[\\\\\\]^_`|~][a-zA-Z0-9!$&*+\\-./:;<>?@\\[\\\\\\]^_`|~]*)\\s*(\\()\\s*([^\\s,]*)", "beginCaptures": { "1": { "name": "keyword.other.entry-type.bibtex" @@ -168,13 +197,7 @@ "name": "meta.entry.parenthesis.bibtex", "patterns": [ { - "include": "#percentage_comment" - }, - { - "include": "#url_field" - }, - { - "begin": "([a-zA-Z0-9\\!\\$\\&\\*\\+\\-\\.\\/\\:\\;\\<\\>\\?\\[\\]\\^\\_\\`\\|]+)\\s*(\\=)", + "begin": "([a-zA-Z!$&*+\\-./:;<>?@\\[\\\\\\]^_`|~][a-zA-Z0-9!$&*+\\-./:;<>?@\\[\\\\\\]^_`|~]*)\\s*(\\=)", "beginCaptures": { "1": { "name": "support.function.key.bibtex" @@ -187,16 +210,7 @@ "name": "meta.key-assignment.bibtex", "patterns": [ { - "include": "#percentage_comment" - }, - { - "include": "#integer" - }, - { - "include": "#string_content" - }, - { - "include": "#string_var" + "include": "#field_value" } ] } @@ -209,6 +223,23 @@ } ], "repository": { + "field_value": { + "patterns": [ + { + "include": "#string_content" + }, + { + "include": "#integer" + }, + { + "include": "#string_var" + }, + { + "name": "keyword.operator.bibtex", + "match": "#" + } + ] + }, "integer": { "match": "\\s*(\\d+)\\s*", "captures": { @@ -218,13 +249,13 @@ } }, "nested_braces": { - "begin": "(?\\?\\[\\]\\^\\_\\`\\|]+)\\s*(#)?", + "match": "[a-zA-Z!$&*+\\-./:;<>?@\\[\\\\\\]^_`|~][a-zA-Z0-9!$&*+\\-./:;<>?@\\[\\\\\\]^_`|~]*", "captures": { - "1": { - "name": "keyword.operator.bibtex" - }, - "2": { + "0": { "name": "support.variable.bibtex" - }, - "3": { - "name": "keyword.operator.bibtex" } } }, @@ -259,23 +284,13 @@ "name": "punctuation.definition.string.begin.bibtex" } }, - "end": "(\\})(?=(?:,?\\s*\\}?\\s*\\n)|(?:\\s*#))", + "end": "\\}", "endCaptures": { - "1": { + "0": { "name": "punctuation.definition.string.end.bibtex" } }, "patterns": [ - { - "include": "#url_cmd" - }, - { - "include": "#percentage_comment" - }, - { - "match": "@", - "name": "invalid.illegal.at-sign.bibtex" - }, { "include": "#nested_braces" } @@ -288,7 +303,7 @@ "name": "punctuation.definition.string.begin.bibtex" } }, - "end": "\"(?=(?:,?\\s*\\}?\\s*\\n)|(?:\\s*#))", + "end": "\"", "endCaptures": { "0": { "name": "punctuation.definition.string.end.bibtex" @@ -296,106 +311,11 @@ }, "patterns": [ { - "include": "#url_cmd" - }, - { - "include": "#percentage_comment" - }, - { - "match": "@", - "name": "invalid.illegal.at-sign.bibtex" + "include": "#nested_braces" } ] } ] - }, - "string_url": { - "patterns": [ - { - "begin": "\\{|\"", - "beginCaptures": { - "0": { - "name": "punctuation.definition.string.begin.bibtex" - } - }, - "end": "(\\}|\")(?=(?:,?\\s*\\}?\\s*\\n)|(?:\\s*#))", - "endCaptures": { - "1": { - "name": "punctuation.definition.string.end.bibtex" - } - }, - "contentName": "meta.url.bibtex", - "patterns": [ - { - "include": "#url_cmd" - } - ] - } - ] - }, - "percentage_comment": { - "patterns": [ - { - "begin": "(^[ \\t]+)?(?=%)", - "beginCaptures": { - "1": { - "name": "punctuation.whitespace.comment.leading.bibtex" - } - }, - "end": "(?!\\G)", - "patterns": [ - { - "begin": "(? = new Map(); - private _refreshingPromise: Promise | undefined; private _sessionChangeEmitter: vscode.EventEmitter = new vscode.EventEmitter(); // Used to keep track of current requests when not using the local server approach. @@ -96,6 +97,12 @@ export class AzureActiveDirectoryService { private _codeExchangePromises = new Map>(); private _codeVerfifiers = new Map(); + // Used to keep track of tokens that we need to store but can't because we aren't the focused window. + private _pendingTokensToStore: Map = new Map(); + + // Used to sequence requests to the same scope. + private _sequencer = new SequencerByKey(); + constructor( private readonly _logger: vscode.LogOutputChannel, _context: vscode.ExtensionContext, @@ -105,15 +112,24 @@ export class AzureActiveDirectoryService { private readonly _env: Environment ) { _context.subscriptions.push(this._tokenStorage.onDidChangeInOtherWindow((e) => this.checkForUpdates(e))); + _context.subscriptions.push(vscode.window.onDidChangeWindowState(async (e) => e.focused && await this.storePendingTokens())); + + // In the event that a window isn't focused for a long time, we should still try to store the tokens at some point. + const timer = new IntervalTimer(); + timer.cancelAndSet( + () => !vscode.window.state.focused && this.storePendingTokens(), + // 5 hours + random extra 0-30 seconds so that each window doesn't try to store at the same time + (18000000) + Math.floor(Math.random() * 30000)); + _context.subscriptions.push(timer); } public async initialize(): Promise { - this._logger.info('Reading sessions from secret storage...'); + this._logger.trace('Reading sessions from secret storage...'); const sessions = await this._tokenStorage.getAll(item => this.sessionMatchesEndpoint(item)); - this._logger.info(`Got ${sessions.length} stored sessions`); + this._logger.trace(`Got ${sessions.length} stored sessions`); const refreshes = sessions.map(async session => { - this._logger.trace(`Read the following stored session with scopes: ${session.scope}`); + this._logger.trace(`[${session.scope}] '${session.id}' Read stored session`); const scopes = session.scope.split(' '); const scopeData: IScopeData = { scopes, @@ -187,12 +203,12 @@ export class AzureActiveDirectoryService { return this._sessionChangeEmitter.event; } - async getSessions(scopes?: string[]): Promise { + public getSessions(scopes?: string[]): Promise { if (!scopes) { this._logger.info('Getting sessions for all scopes...'); const sessions = this._tokens.map(token => this.convertToSessionSync(token)); this._logger.info(`Got ${sessions.length} sessions for all scopes...`); - return sessions; + return Promise.resolve(sessions); } let modifiedScopes = [...scopes]; @@ -210,32 +226,7 @@ export class AzureActiveDirectoryService { } modifiedScopes = modifiedScopes.sort(); - let modifiedScopesStr = modifiedScopes.join(' '); - this._logger.info(`Getting sessions for the following scopes: ${modifiedScopesStr}`); - - if (this._refreshingPromise) { - this._logger.info('Refreshing in progress. Waiting for completion before continuing.'); - try { - await this._refreshingPromise; - } catch (e) { - // this will get logged in the refresh function. - } - } - - let matchingTokens = this._tokens.filter(token => token.scope === modifiedScopesStr); - - // The user may still have a token that doesn't have the openid & email scopes so check for that as well. - // Eventually, we should remove this and force the user to re-log in so that we don't have any sessions - // without an idtoken. - if (!matchingTokens.length) { - const fallbackOrderedScopes = scopes.sort().join(' '); - this._logger.trace(`No session found with idtoken scopes... Using fallback scope list of: ${fallbackOrderedScopes}`); - matchingTokens = this._tokens.filter(token => token.scope === fallbackOrderedScopes); - if (matchingTokens.length) { - modifiedScopesStr = fallbackOrderedScopes; - } - } - + const modifiedScopesStr = modifiedScopes.join(' '); const clientId = this.getClientId(scopes); const scopeData: IScopeData = { clientId, @@ -247,31 +238,43 @@ export class AzureActiveDirectoryService { tenant: this.getTenantId(scopes), }; + this._logger.trace(`[${scopeData.scopeStr}] Queued getting sessions`); + return this._sequencer.queue(modifiedScopesStr, () => this.doGetSessions(scopeData)); + } + + private async doGetSessions(scopeData: IScopeData): Promise { + this._logger.info(`[${scopeData.scopeStr}] Getting sessions`); + + const matchingTokens = this._tokens.filter(token => token.scope === scopeData.scopeStr); // If we still don't have a matching token try to get a new token from an existing token by using // the refreshToken. This is documented here: // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#refresh-the-access-token // "Refresh tokens are valid for all permissions that your client has already received consent for." if (!matchingTokens.length) { // Get a token with the correct client id. - const token = clientId === DEFAULT_CLIENT_ID + const token = scopeData.clientId === DEFAULT_CLIENT_ID ? this._tokens.find(t => t.refreshToken && !t.scope.includes('VSCODE_CLIENT_ID')) - : this._tokens.find(t => t.refreshToken && t.scope.includes(`VSCODE_CLIENT_ID:${clientId}`)); + : this._tokens.find(t => t.refreshToken && t.scope.includes(`VSCODE_CLIENT_ID:${scopeData.clientId}`)); if (token) { + this._logger.trace(`[${scopeData.scopeStr}] '${token.sessionId}' Found a matching token with a different scopes '${token.scope}'. Attempting to get a new session using the existing session.`); try { - const itoken = await this.refreshToken(token.refreshToken, scopeData); + const itoken = await this.doRefreshToken(token.refreshToken, scopeData); matchingTokens.push(itoken); } catch (err) { - this._logger.error(`Attempted to get a new session for scopes '${scopeData.scopeStr}' using the existing session with scopes '${token.scope}' but it failed due to: ${err.message ?? err}`); + this._logger.error(`[${scopeData.scopeStr}] Attempted to get a new session using the existing session with scopes '${token.scope}' but it failed due to: ${err.message ?? err}`); } } } - this._logger.info(`Got ${matchingTokens.length} sessions for scopes: ${modifiedScopesStr}`); - return Promise.all(matchingTokens.map(token => this.convertToSession(token, scopeData))); + this._logger.info(`[${scopeData.scopeStr}] Got ${matchingTokens.length} sessions`); + const results = await Promise.allSettled(matchingTokens.map(token => this.convertToSession(token, scopeData))); + return results + .filter(result => result.status === 'fulfilled') + .map(result => (result as PromiseFulfilledResult).value); } - public async createSession(scopes: string[]): Promise { + public createSession(scopes: string[]): Promise { let modifiedScopes = [...scopes]; if (!modifiedScopes.includes('openid')) { modifiedScopes.push('openid'); @@ -296,7 +299,12 @@ export class AzureActiveDirectoryService { tenant: this.getTenantId(scopes), }; - this._logger.info(`Logging in for the following scopes: ${scopeData.scopeStr}`); + this._logger.trace(`[${scopeData.scopeStr}] Queued creating session`); + return this._sequencer.queue(scopeData.scopeStr, () => this.doCreateSession(scopeData)); + } + + private async doCreateSession(scopeData: IScopeData): Promise { + this._logger.info(`[${scopeData.scopeStr}] Creating session`); const runsRemote = vscode.env.remoteName !== undefined; const runsServerless = vscode.env.remoteName === undefined && vscode.env.uiKind === vscode.UIKind.Web; @@ -312,7 +320,7 @@ export class AzureActiveDirectoryService { try { return await this.createSessionWithLocalServer(scopeData); } catch (e) { - this._logger.error(`Error creating session for scopes: ${scopeData.scopeStr} Error: ${e}`); + this._logger.error(`[${scopeData.scopeStr}] Error creating session: ${e}`); // If the error was about starting the server, try directly hitting the login endpoint instead if (e.message === 'Error listening to server' || e.message === 'Closed' || e.message === 'Timeout waiting for port') { @@ -324,6 +332,7 @@ export class AzureActiveDirectoryService { } private async createSessionWithLocalServer(scopeData: IScopeData) { + this._logger.trace(`[${scopeData.scopeStr}] Starting login flow with local server`); const codeVerifier = generateCodeVerifier(); const codeChallenge = await generateCodeChallenge(codeVerifier); const qs = new URLSearchParams({ @@ -352,11 +361,14 @@ export class AzureActiveDirectoryService { } const session = await this.exchangeCodeForSession(codeToExchange, codeVerifier, scopeData); + this._logger.trace(`[${scopeData.scopeStr}] '${session.id}' Sending change event for added session`); this._sessionChangeEmitter.fire({ added: [session], removed: [], changed: [] }); + this._logger.info(`[${scopeData.scopeStr}] '${session.id}' session successfully created!`); return session; } private async createSessionWithoutLocalServer(scopeData: IScopeData): Promise { + this._logger.trace(`[${scopeData.scopeStr}] Starting login flow without local server`); let callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.microsoft-authentication`)); const nonce = generateCodeVerifier(); const callbackQuery = new URLSearchParams(callbackUri.query); @@ -418,25 +430,19 @@ export class AzureActiveDirectoryService { } public async removeSessionById(sessionId: string, writeToDisk: boolean = true): Promise { - this._logger.info(`Logging out of session '${sessionId}'`); const tokenIndex = this._tokens.findIndex(token => token.sessionId === sessionId); if (tokenIndex === -1) { - this._logger.info(`Session not found '${sessionId}'`); + this._logger.warn(`'${sessionId}' Session not found to remove`); return Promise.resolve(undefined); } const token = this._tokens.splice(tokenIndex, 1)[0]; - const session = await this.removeSessionByIToken(token, writeToDisk); - - if (session) { - this._sessionChangeEmitter.fire({ added: [], removed: [session], changed: [] }); - } - - return session; + this._logger.trace(`[${token.scope}] '${sessionId}' Queued removing session`); + return this._sequencer.queue(token.scope, () => this.removeSessionByIToken(token, writeToDisk)); } public async clearSessions() { - this._logger.info('Logging out of all sessions'); + this._logger.trace('Logging out of all sessions'); this._tokens = []; await this._tokenStorage.deleteAll(item => this.sessionMatchesEndpoint(item)); @@ -445,9 +451,11 @@ export class AzureActiveDirectoryService { }); this._refreshTimeouts.clear(); + this._logger.trace('All sessions logged out'); } private async removeSessionByIToken(token: IToken, writeToDisk: boolean = true): Promise { + this._logger.info(`[${token.scope}] '${token.sessionId}' Logging out of session`); this.removeSessionTimeout(token.sessionId); if (writeToDisk) { @@ -460,9 +468,9 @@ export class AzureActiveDirectoryService { } const session = this.convertToSessionSync(token); - this._logger.info(`Sending change event for session that was removed with scopes: ${token.scope}`); + this._logger.trace(`[${token.scope}] '${token.sessionId}' Sending change event for session that was removed`); this._sessionChangeEmitter.fire({ added: [], removed: [session], changed: [] }); - this._logger.info(`Logged out of session '${token.sessionId}' with scopes: ${token.scope}`); + this._logger.info(`[${token.scope}] '${token.sessionId}' Logged out of session successfully!`); return session; } @@ -471,12 +479,14 @@ export class AzureActiveDirectoryService { //#region timeout private setSessionTimeout(sessionId: string, refreshToken: string, scopeData: IScopeData, timeout: number) { + this._logger.trace(`[${scopeData.scopeStr}] '${sessionId}' Setting refresh timeout for ${timeout} milliseconds`); this.removeSessionTimeout(sessionId); this._refreshTimeouts.set(sessionId, setTimeout(async () => { try { const refreshedToken = await this.refreshToken(refreshToken, scopeData, sessionId); - this._logger.info('Triggering change session event...'); + this._logger.trace(`[${scopeData.scopeStr}] '${sessionId}' Sending change event for session that was refreshed`); this._sessionChangeEmitter.fire({ added: [], removed: [], changed: [this.convertToSessionSync(refreshedToken)] }); + this._logger.trace(`[${scopeData.scopeStr}] '${sessionId}' refresh timeout complete`); } catch (e) { if (e.message !== REFRESH_NETWORK_FAILURE) { vscode.window.showErrorMessage(vscode.l10n.t('You have been signed out because reading stored authentication information failed.')); @@ -500,12 +510,13 @@ export class AzureActiveDirectoryService { private convertToTokenSync(json: ITokenResponse, scopeData: IScopeData, existingId?: string): IToken { let claims = undefined; + this._logger.trace(`[${scopeData.scopeStr}] '${existingId ?? 'new'}' Attempting to parse token response.`); try { if (json.id_token) { claims = JSON.parse(base64Decode(json.id_token.split('.')[1])); } else { - this._logger.info('Attempting to parse access_token instead since no id_token was included in the response.'); + this._logger.warn(`[${scopeData.scopeStr}] '${existingId ?? 'new'}' Attempting to parse access_token instead since no id_token was included in the response.`); claims = JSON.parse(base64Decode(json.access_token.split('.')[1])); } } catch (e) { @@ -520,6 +531,8 @@ export class AzureActiveDirectoryService { } const id = `${claims.tid}/${(claims.oid ?? (claims.altsecid ?? '' + claims.ipd ?? ''))}`; + const sessionId = existingId || `${id}/${randomUUID()}`; + this._logger.trace(`[${scopeData.scopeStr}] '${sessionId}' Token response parsed successfully.`); return { expiresIn: json.expires_in, expiresAt: json.expires_in ? Date.now() + json.expires_in * 1000 : undefined, @@ -527,7 +540,7 @@ export class AzureActiveDirectoryService { idToken: json.id_token, refreshToken: json.refresh_token, scope: scopeData.scopeStr, - sessionId: existingId || `${id}/${randomUUID()}`, + sessionId, account: { label, id, @@ -552,9 +565,7 @@ export class AzureActiveDirectoryService { private async convertToSession(token: IToken, scopeData: IScopeData): Promise { if (token.accessToken && (!token.expiresAt || token.expiresAt > Date.now())) { - token.expiresAt - ? this._logger.info(`Token available from cache (for scopes ${token.scope}), expires in ${token.expiresAt - Date.now()} milliseconds`) - : this._logger.info('Token available from cache (for scopes ${token.scope})'); + this._logger.trace(`[${scopeData.scopeStr}] '${token.sessionId}' Token available from cache${token.expiresAt ? `, expires in ${token.expiresAt - Date.now()} milliseconds` : ''}.`); return { id: token.sessionId, accessToken: token.accessToken, @@ -565,7 +576,7 @@ export class AzureActiveDirectoryService { } try { - this._logger.info(`Token expired or unavailable (for scopes ${token.scope}), trying refresh`); + this._logger.trace(`[${scopeData.scopeStr}] '${token.sessionId}' Token expired or unavailable, trying refresh`); const refreshedToken = await this.refreshToken(token.refreshToken, scopeData, token.sessionId); if (refreshedToken.accessToken) { return { @@ -588,18 +599,13 @@ export class AzureActiveDirectoryService { //#region refresh logic - private async refreshToken(refreshToken: string, scopeData: IScopeData, sessionId?: string): Promise { - this._refreshingPromise = this.doRefreshToken(refreshToken, scopeData, sessionId); - try { - const result = await this._refreshingPromise; - return result; - } finally { - this._refreshingPromise = undefined; - } + private refreshToken(refreshToken: string, scopeData: IScopeData, sessionId?: string): Promise { + this._logger.trace(`[${scopeData.scopeStr}] '${sessionId ?? 'new'}' Queued refreshing token`); + return this._sequencer.queue(scopeData.scopeStr, () => this.doRefreshToken(refreshToken, scopeData, sessionId)); } private async doRefreshToken(refreshToken: string, scopeData: IScopeData, sessionId?: string): Promise { - this._logger.info(`Refreshing token for scopes: ${scopeData.scopeStr}`); + this._logger.trace(`[${scopeData.scopeStr}] '${sessionId ?? 'new'}' Refreshing token`); const postData = new URLSearchParams({ refresh_token: refreshToken, client_id: scopeData.clientId, @@ -614,7 +620,7 @@ export class AzureActiveDirectoryService { this.setSessionTimeout(token.sessionId, token.refreshToken, scopeData, token.expiresIn * AzureActiveDirectoryService.REFRESH_TIMEOUT_MODIFIER); } this.setToken(token, scopeData); - this._logger.info(`Token refresh success for scopes: ${token.scope}`); + this._logger.trace(`[${scopeData.scopeStr}] '${token.sessionId}' Token refresh success`); return token; } catch (e) { if (e.message === REFRESH_NETWORK_FAILURE) { @@ -625,7 +631,7 @@ export class AzureActiveDirectoryService { } throw e; } - this._logger.error(`Refreshing token failed (for scopes: ${scopeData.scopeStr}): ${e.message}`); + this._logger.error(`[${scopeData.scopeStr}] '${sessionId ?? 'new'}' Refreshing token failed: ${e.message}`); throw e; } } @@ -690,6 +696,7 @@ export class AzureActiveDirectoryService { const session = await this.exchangeCodeForSession(code, verifier, scopeData); this._sessionChangeEmitter.fire({ added: [session], removed: [], changed: [] }); + this._logger.info(`[${scopeData.scopeStr}] '${session.id}' session successfully created!`); resolve(session); } catch (err) { reject(err); @@ -705,6 +712,7 @@ export class AzureActiveDirectoryService { } private async handleCodeInputBox(inputBox: vscode.InputBox, verifier: string, scopeData: IScopeData): Promise { + this._logger.trace(`[${scopeData.scopeStr}] Starting login flow with input box`); inputBox.ignoreFocusOut = true; inputBox.title = vscode.l10n.t('Microsoft Authentication'); inputBox.prompt = vscode.l10n.t('Provide the authorization code to complete the sign in flow.'); @@ -716,7 +724,9 @@ export class AzureActiveDirectoryService { if (code) { inputBox.dispose(); const session = await this.exchangeCodeForSession(code, verifier, scopeData); + this._logger.trace(`[${scopeData.scopeStr}] '${session.id}' sending session changed event because session was added.`); this._sessionChangeEmitter.fire({ added: [session], removed: [], changed: [] }); + this._logger.trace(`[${scopeData.scopeStr}] '${session.id}' session successfully created!`); resolve(session); } }); @@ -730,7 +740,7 @@ export class AzureActiveDirectoryService { } private async exchangeCodeForSession(code: string, codeVerifier: string, scopeData: IScopeData): Promise { - this._logger.info(`Exchanging login code for token for scopes: ${scopeData.scopeStr}`); + this._logger.trace(`[${scopeData.scopeStr}] Exchanging login code for session`); let token: IToken | undefined; try { const postData = new URLSearchParams({ @@ -743,10 +753,10 @@ export class AzureActiveDirectoryService { }).toString(); const json = await this.fetchTokenResponse(postData, scopeData); - this._logger.info(`Exchanging login code for token (for scopes: ${scopeData.scopeStr}) succeeded!`); + this._logger.trace(`[${scopeData.scopeStr}] Exchanging code for token succeeded!`); token = this.convertToTokenSync(json, scopeData); } catch (e) { - this._logger.error(`Error exchanging code for token (for scopes ${scopeData.scopeStr}): ${e}`); + this._logger.error(`[${scopeData.scopeStr}] Error exchanging code for token: ${e}`); throw e; } @@ -754,7 +764,7 @@ export class AzureActiveDirectoryService { this.setSessionTimeout(token.sessionId, token.refreshToken, scopeData, token.expiresIn * AzureActiveDirectoryService.REFRESH_TIMEOUT_MODIFIER); } this.setToken(token, scopeData); - this._logger.info(`Login successful for scopes: ${scopeData.scopeStr}`); + this._logger.trace(`[${scopeData.scopeStr}] '${token.sessionId}' Exchanging login code for session succeeded!`); return await this.convertToSession(token, scopeData); } @@ -789,7 +799,7 @@ export class AzureActiveDirectoryService { if (!result || result.status > 499) { if (attempts > 3) { - this._logger.error(`Fetching token failed for scopes (${scopeData.scopeStr}): ${result ? await result.text() : errorMessage}`); + this._logger.error(`[${scopeData.scopeStr}] Fetching token failed: ${result ? await result.text() : errorMessage}`); break; } // Exponential backoff @@ -813,7 +823,7 @@ export class AzureActiveDirectoryService { //#region storage operations private setToken(token: IToken, scopeData: IScopeData): void { - this._logger.info(`Setting token for scopes: ${scopeData.scopeStr}`); + this._logger.trace(`[${scopeData.scopeStr}] '${token.sessionId}' Setting token`); const existingTokenIndex = this._tokens.findIndex(t => t.sessionId === token.sessionId); if (existingTokenIndex > -1) { @@ -823,41 +833,18 @@ export class AzureActiveDirectoryService { } // Don't await because setting the token is only useful for any new windows that open. - this.storeToken(token, scopeData); + void this.storeToken(token, scopeData); } private async storeToken(token: IToken, scopeData: IScopeData): Promise { if (!vscode.window.state.focused) { - const shouldStore = await new Promise((resolve, _) => { - // To handle the case where the window is not focused for a long time. We want to store the token - // at some point so that the next time they _do_ interact with VS Code, they don't have to sign in again. - const timer = setTimeout( - () => resolve(true), - // 5 hours + random extra 0-30 seconds so that each window doesn't try to store at the same time - (18000000) + Math.floor(Math.random() * 30000) - ); - const dispose = vscode.Disposable.from( - vscode.window.onDidChangeWindowState(e => { - if (e.focused) { - resolve(true); - dispose.dispose(); - clearTimeout(timer); - } - }), - this._tokenStorage.onDidChangeInOtherWindow(e => { - if (e.updated.includes(token.sessionId)) { - resolve(false); - dispose.dispose(); - clearTimeout(timer); - } - }) - ); - }); - - if (!shouldStore) { - this._logger.info(`Not storing token for scopes ${scopeData.scopeStr} because it was added in another window`); - return; + if (this._pendingTokensToStore.has(token.sessionId)) { + this._logger.trace(`[${scopeData.scopeStr}] '${token.sessionId}' Window is not focused, replacing token to be stored`); + } else { + this._logger.trace(`[${scopeData.scopeStr}] '${token.sessionId}' Window is not focused, pending storage of token`); } + this._pendingTokensToStore.set(token.sessionId, token); + return; } await this._tokenStorage.store(token.sessionId, { @@ -867,7 +854,31 @@ export class AzureActiveDirectoryService { account: token.account, endpoint: this._env.activeDirectoryEndpointUrl, }); - this._logger.info(`Stored token for scopes: ${scopeData.scopeStr}`); + this._logger.trace(`[${scopeData.scopeStr}] '${token.sessionId}' Stored token`); + } + + private async storePendingTokens(): Promise { + if (this._pendingTokensToStore.size === 0) { + this._logger.trace('No pending tokens to store'); + return; + } + + const tokens = [...this._pendingTokensToStore.values()]; + this._pendingTokensToStore.clear(); + + this._logger.trace(`Storing ${tokens.length} pending tokens...`); + await Promise.allSettled(tokens.map(async token => { + this._logger.trace(`[${token.scope}] '${token.sessionId}' Storing pending token`); + await this._tokenStorage.store(token.sessionId, { + id: token.sessionId, + refreshToken: token.refreshToken, + scope: token.scope, + account: token.account, + endpoint: this._env.activeDirectoryEndpointUrl, + }); + this._logger.trace(`[${token.scope}] '${token.sessionId}' Stored pending token`); + })); + this._logger.trace('Done storing pending tokens'); } private async checkForUpdates(e: IDidChangeInOtherWindowEvent): Promise { @@ -875,7 +886,7 @@ export class AzureActiveDirectoryService { const session = await this._tokenStorage.get(key); if (!session) { this._logger.error('session not found that was apparently just added'); - return; + continue; } if (!this.sessionMatchesEndpoint(session)) { @@ -895,36 +906,46 @@ export class AzureActiveDirectoryService { clientId: this.getClientId(scopes), tenant: this.getTenantId(scopes), }; - this._logger.info(`Session added in another window with scopes: ${session.scope}`); + this._logger.trace(`[${scopeData.scopeStr}] '${session.id}' Session added in another window`); const token = await this.refreshToken(session.refreshToken, scopeData, session.id); - this._logger.info(`Sending change event for session that was added with scopes: ${scopeData.scopeStr}`); + this._logger.trace(`[${scopeData.scopeStr}] '${token.sessionId}' Sending change event for session that was added`); this._sessionChangeEmitter.fire({ added: [this.convertToSessionSync(token)], removed: [], changed: [] }); - return; + this._logger.trace(`[${scopeData.scopeStr}] '${token.sessionId}' Session added in another window added here`); + continue; } catch (e) { // Network failures will automatically retry on next poll. if (e.message !== REFRESH_NETWORK_FAILURE) { vscode.window.showErrorMessage(vscode.l10n.t('You have been signed out because reading stored authentication information failed.')); await this.removeSessionById(session.id); } - return; + continue; } } } for (const { value } of e.removed) { + this._logger.trace(`[${value.scope}] '${value.id}' Session removed in another window`); if (!this.sessionMatchesEndpoint(value)) { // If the session wasn't made for this login endpoint, ignore this update + this._logger.trace(`[${value.scope}] '${value.id}' Session doesn't match endpoint. Skipping...`); continue; } - this._logger.info(`Session removed in another window with scopes: ${value.scope}`); await this.removeSessionById(value.id, false); + this._logger.trace(`[${value.scope}] '${value.id}' Session removed in another window removed here`); } // NOTE: We don't need to handle changed sessions because all that really would give us is a new refresh token // because access tokens are not stored in Secret Storage due to their short lifespan. This new refresh token // is not useful in this window because we really only care about the lifetime of the _access_ token which we // are already managing (see usages of `setSessionTimeout`). + // However, in order to minimize the amount of times we store tokens, if a token was stored via another window, + // we cancel any pending token storage operations. + for (const sessionId of e.updated) { + if (this._pendingTokensToStore.delete(sessionId)) { + this._logger.trace(`'${sessionId}' Cancelled pending token storage because token was updated in another window`); + } + } } private sessionMatchesEndpoint(session: IStoredSession): boolean { diff --git a/extensions/microsoft-authentication/src/common/async.ts b/extensions/microsoft-authentication/src/common/async.ts new file mode 100644 index 00000000000..527b5bbb399 --- /dev/null +++ b/extensions/microsoft-authentication/src/common/async.ts @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vscode'; + +export class SequencerByKey { + + private promiseMap = new Map>(); + + queue(key: TKey, promiseTask: () => Promise): Promise { + const runningPromise = this.promiseMap.get(key) ?? Promise.resolve(); + const newPromise = runningPromise + .catch(() => { }) + .then(promiseTask) + .finally(() => { + if (this.promiseMap.get(key) === newPromise) { + this.promiseMap.delete(key); + } + }); + this.promiseMap.set(key, newPromise); + return newPromise; + } +} + +export class IntervalTimer extends Disposable { + + private _token: any; + + constructor() { + super(() => this.cancel()); + this._token = -1; + } + + cancel(): void { + if (this._token !== -1) { + clearInterval(this._token); + this._token = -1; + } + } + + cancelAndSet(runner: () => void, interval: number): void { + this.cancel(); + this._token = setInterval(() => { + runner(); + }, interval); + } +} diff --git a/extensions/microsoft-authentication/src/utils.ts b/extensions/microsoft-authentication/src/common/uri.ts similarity index 100% rename from extensions/microsoft-authentication/src/utils.ts rename to extensions/microsoft-authentication/src/common/uri.ts diff --git a/extensions/microsoft-authentication/src/extension.ts b/extensions/microsoft-authentication/src/extension.ts index 767c0a17963..02cfb4643f4 100644 --- a/extensions/microsoft-authentication/src/extension.ts +++ b/extensions/microsoft-authentication/src/extension.ts @@ -72,7 +72,7 @@ async function initMicrosoftSovereignCloudAuthProvider(context: vscode.Extension scopes: JSON.stringify(scopes.map(s => s.replace(/[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}/i, '{guid}'))), }); - return await aadService.createSession(scopes.sort()); + return await aadService.createSession(scopes); } catch (e) { /* __GDPR__ "loginFailed" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often users run into issues with the login flow." } @@ -138,7 +138,7 @@ export async function activate(context: vscode.ExtensionContext) { scopes: JSON.stringify(scopes.map(s => s.replace(/[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}/i, '{guid}'))), }); - return await loginService.createSession(scopes.sort()); + return await loginService.createSession(scopes); } catch (e) { /* __GDPR__ "loginFailed" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often users run into issues with the login flow." } diff --git a/extensions/notebook-renderers/src/index.ts b/extensions/notebook-renderers/src/index.ts index c5b4a631afc..6ee655c509c 100644 --- a/extensions/notebook-renderers/src/index.ts +++ b/extensions/notebook-renderers/src/index.ts @@ -228,7 +228,9 @@ function onKeypressHandler(e: KeyboardEvent) { if (e.ctrlKey || e.shiftKey) { return; } - if (e.code === 'ArrowDown' || e.code === 'End' || e.code === 'ArrowUp' || e.code === 'Home') { + if (e.code === 'ArrowDown' || e.code === 'ArrowUp' || + e.code === 'End' || e.code === 'Home' || + e.code === 'PageUp' || e.code === 'PageDown') { // These should change the scroll position, not adjust the selected cell in the notebook e.stopPropagation(); } diff --git a/extensions/perl/package.json b/extensions/perl/package.json index 3357e1ff3bd..003ec922c32 100644 --- a/extensions/perl/package.json +++ b/extensions/perl/package.json @@ -9,7 +9,7 @@ "vscode": "*" }, "scripts": { - "update-grammar": "node ../node_modules/vscode-grammar-updater/bin textmate/perl.tmbundle Syntaxes/Perl.plist ./syntaxes/perl.tmLanguage.json Syntaxes/Perl%%206.tmLanguage ./syntaxes/perl6.tmLanguage.json" + "update-grammar": "node ../node_modules/vscode-grammar-updater/bin textmate/perl.tmbundle Syntaxes/Perl.plist ./syntaxes/perl.tmLanguage.json Syntaxes/Perl%206.tmLanguage ./syntaxes/perl6.tmLanguage.json" }, "contributes": { "languages": [ @@ -31,18 +31,23 @@ "configuration": "./perl.language-configuration.json" }, { - "id": "perl6", + "id": "raku", "aliases": [ - "Perl 6", + "Raku", + "Perl6", "perl6" ], "extensions": [ + ".raku", + ".rakumod", + ".rakutest", + ".rakudoc", + ".nqp", ".p6", ".pl6", - ".pm6", - ".nqp" + ".pm6" ], - "firstLine": "(^#!.*\\bperl6\\b)|use\\s+v6", + "firstLine": "(^#!.*\\bperl6\\b)|use\\s+v6|raku|=begin\\spod|my\\sclass", "configuration": "./perl6.language-configuration.json" } ], @@ -56,7 +61,7 @@ ] }, { - "language": "perl6", + "language": "raku", "scopeName": "source.perl.6", "path": "./syntaxes/perl6.tmLanguage.json" } diff --git a/extensions/theme-abyss/themes/abyss-color-theme.json b/extensions/theme-abyss/themes/abyss-color-theme.json index e81c3b9adca..f0e4d4bb352 100644 --- a/extensions/theme-abyss/themes/abyss-color-theme.json +++ b/extensions/theme-abyss/themes/abyss-color-theme.json @@ -278,18 +278,14 @@ } ], "colors": { - "editor.background": "#000c18", "editor.foreground": "#6688cc", - // Base // "foreground": "", "focusBorder": "#596F99", // "contrastActiveBorder": "", // "contrastBorder": "", - // "widget.shadow": "", - "input.background": "#181f2f", // "input.border": "", // "input.foreground": "", @@ -300,17 +296,13 @@ "inputValidation.warningBorder": "#5B7E7A", "inputValidation.errorBackground": "#A22D44", "inputValidation.errorBorder": "#AB395B", - "badge.background": "#0063a5", "progressBar.background": "#0063a5", - "dropdown.background": "#181f2f", // "dropdown.foreground": "", // "dropdown.border": "", - "button.background": "#2B3C5D", // "button.foreground": "", - "list.activeSelectionBackground": "#08286b", // "list.activeSelectionForeground": "", "quickInputList.focusBackground": "#08286b", @@ -318,12 +310,10 @@ "list.inactiveSelectionBackground": "#152037", "list.dropBackground": "#041D52", "list.highlightForeground": "#0063a5", - "scrollbar.shadow": "#515E91AA", "scrollbarSlider.activeBackground": "#3B3F5188", "scrollbarSlider.background": "#1F2230AA", "scrollbarSlider.hoverBackground": "#3B3F5188", - // Editor "editorWidget.background": "#262641", "editorCursor.foreground": "#ddbb88", @@ -350,14 +340,12 @@ // "editor.selectionHighlightBackground": "", // "editor.wordHighlightBackground": "", // "editor.wordHighlightStrongBackground": "", - // Editor: Suggest Widget // "editorSuggestWidget.background": "", // "editorSuggestWidget.border": "", // "editorSuggestWidget.foreground": "", // "editorSuggestWidget.highlightForeground": "", // "editorSuggestWidget.selectedBackground": "", - // Editor: Peek View "peekViewResult.background": "#060621", // "peekViewResult.lineForeground": "", @@ -371,7 +359,6 @@ "peekViewResult.matchHighlightBackground": "#eeeeee44", // "peekViewTitleLabel.foreground": "", // "peekViewTitleDescription.foreground": "", - // Ports "ports.iconRunningProcessForeground": "#80a2c2", // Editor: Diff @@ -379,23 +366,18 @@ // "diffEditor.insertedTextBorder": "", "diffEditor.removedTextBackground": "#892F4688", // "diffEditor.removedTextBorder": "", - - // Editor: Minimap "minimap.selectionHighlight": "#750000", - // Workbench: Title "titleBar.activeBackground": "#10192c", // "titleBar.activeForeground": "", // "titleBar.inactiveBackground": "", // "titleBar.inactiveForeground": "", - // Workbench: Editors // "editorGroupHeader.noTabsBackground": "", "editorGroup.border": "#2b2b4a", "editorGroup.dropBackground": "#25375daa", "editorGroupHeader.tabsBackground": "#1c1c2a", - // Workbench: Tabs "tab.border": "#2b2b4a", // "tab.activeBackground": "", @@ -403,26 +385,21 @@ // "tab.activeForeground": "", // "tab.inactiveForeground": "", "tab.lastPinnedBorder": "#2b3c5d", - // Workbench: Activity Bar "activityBar.background": "#051336", // "activityBar.foreground": "", // "activityBarBadge.background": "", // "activityBarBadge.foreground": "", - "activityBarItem.profilesBackground": "#082877", - // Workbench: Panel // "panel.background": "", "panel.border": "#2b2b4a", // "panelTitle.activeBorder": "", // "panelTitle.activeForeground": "", // "panelTitle.inactiveForeground": "", - // Workbench: Side Bar "sideBar.background": "#060621", // "sideBarTitle.foreground": "", "sideBarSectionHeader.background": "#10192c", - // Workbench: Status Bar "statusBar.background": "#10192c", "statusBar.noFolderBackground": "#10192c", @@ -433,20 +410,16 @@ "statusBarItem.prominentHoverBackground": "#0063a5dd", // "statusBarItem.activeBackground": "", // "statusBarItem.hoverBackground": "", - // Workbench: Debug "debugToolBar.background": "#051336", "debugExceptionWidget.background": "#051336", "debugExceptionWidget.border": "#AB395B", - // Workbench: Quick Open "pickerGroup.border": "#596F99", "pickerGroup.foreground": "#596F99", - // Workbench: Extensions "extensionButton.prominentBackground": "#5f8b3b", "extensionButton.prominentHoverBackground": "#5f8b3bbb", - // Workbench: Terminal "terminal.ansiBlack": "#111111", "terminal.ansiRed": "#ff9da4", diff --git a/extensions/theme-defaults/themes/dark_plus.json b/extensions/theme-defaults/themes/dark_plus.json index ed80b1785f6..5565e2dbec6 100644 --- a/extensions/theme-defaults/themes/dark_plus.json +++ b/extensions/theme-defaults/themes/dark_plus.json @@ -77,6 +77,7 @@ "source.cpp keyword.operator.new", "keyword.operator.delete", "keyword.other.using", + "keyword.other.directive.using", "keyword.other.operator", "entity.name.operator" ], diff --git a/extensions/theme-defaults/themes/hc_black.json b/extensions/theme-defaults/themes/hc_black.json index 816fbf9395a..7b671065646 100644 --- a/extensions/theme-defaults/themes/hc_black.json +++ b/extensions/theme-defaults/themes/hc_black.json @@ -406,6 +406,7 @@ "source.cpp keyword.operator.new", "source.cpp keyword.operator.delete", "keyword.other.using", + "keyword.other.directive.using", "keyword.other.operator" ], "settings": { diff --git a/extensions/theme-defaults/themes/hc_light.json b/extensions/theme-defaults/themes/hc_light.json index 17c1af9ef34..aaf0e2f9cf3 100644 --- a/extensions/theme-defaults/themes/hc_light.json +++ b/extensions/theme-defaults/themes/hc_light.json @@ -442,6 +442,7 @@ "source.cpp keyword.operator.new", "source.cpp keyword.operator.delete", "keyword.other.using", + "keyword.other.directive.using", "keyword.other.operator", "entity.name.operator" ], diff --git a/extensions/theme-defaults/themes/light_plus.json b/extensions/theme-defaults/themes/light_plus.json index f73b79579f0..7f77883f3ee 100644 --- a/extensions/theme-defaults/themes/light_plus.json +++ b/extensions/theme-defaults/themes/light_plus.json @@ -77,6 +77,7 @@ "source.cpp keyword.operator.new", "source.cpp keyword.operator.delete", "keyword.other.using", + "keyword.other.directive.using", "keyword.other.operator", "entity.name.operator" ], diff --git a/extensions/theme-kimbie-dark/themes/kimbie-dark-color-theme.json b/extensions/theme-kimbie-dark/themes/kimbie-dark-color-theme.json index 3554c486209..ec50a96586a 100644 --- a/extensions/theme-kimbie-dark/themes/kimbie-dark-color-theme.json +++ b/extensions/theme-kimbie-dark/themes/kimbie-dark-color-theme.json @@ -32,7 +32,6 @@ "ports.iconRunningProcessForeground": "#369432", "activityBar.background": "#221a0f", "activityBar.foreground": "#d3af86", - "activityBarItem.profilesBackground": "#47351d", "sideBar.background": "#362712", "menu.background": "#362712", "menu.foreground": "#CCCCCC", @@ -124,6 +123,7 @@ "keyword.operator.new.cpp", "keyword.operator.delete.cpp", "keyword.other.using", + "keyword.other.directive.using", "keyword.other.operator" ], "settings": { diff --git a/extensions/theme-monokai-dimmed/themes/dimmed-monokai-color-theme.json b/extensions/theme-monokai-dimmed/themes/dimmed-monokai-color-theme.json index 691680512a4..00ba8b3ace6 100644 --- a/extensions/theme-monokai-dimmed/themes/dimmed-monokai-color-theme.json +++ b/extensions/theme-monokai-dimmed/themes/dimmed-monokai-color-theme.json @@ -269,6 +269,7 @@ "keyword.operator.new.cpp", "keyword.operator.delete.cpp", "keyword.other.using", + "keyword.other.directive.using", "keyword.other.operator" ], "settings": { diff --git a/extensions/theme-tomorrow-night-blue/themes/tomorrow-night-blue-color-theme.json b/extensions/theme-tomorrow-night-blue/themes/tomorrow-night-blue-color-theme.json index b0bdf8e90a9..840a1764bf1 100644 --- a/extensions/theme-tomorrow-night-blue/themes/tomorrow-night-blue-color-theme.json +++ b/extensions/theme-tomorrow-night-blue/themes/tomorrow-night-blue-color-theme.json @@ -36,7 +36,6 @@ "statusBar.noFolderBackground": "#001126", "statusBar.debuggingBackground": "#001126", "activityBar.background": "#001733", - "activityBarItem.profilesBackground": "#003271", "progressBar.background": "#bbdaffcc", "badge.background": "#bbdaffcc", "badge.foreground": "#001733", diff --git a/extensions/tunnel-forwarding/src/extension.ts b/extensions/tunnel-forwarding/src/extension.ts index f6ef85e71b1..951fc6f5b8d 100644 --- a/extensions/tunnel-forwarding/src/extension.ts +++ b/extensions/tunnel-forwarding/src/extension.ts @@ -67,7 +67,7 @@ export async function activate(context: vscode.ExtensionContext) { } const logger = new Logger(vscode.l10n.t('Port Forwarding')); - const provider = new TunnelProvider(logger); + const provider = new TunnelProvider(logger, context); context.subscriptions.push( vscode.commands.registerCommand('tunnel-forwarding.showLog', () => logger.show()), @@ -120,6 +120,8 @@ class Logger { } } +const didWarnPublicKey = 'didWarnPublic'; + class TunnelProvider implements vscode.TunnelProvider { private readonly tunnels = new Set(); private readonly stateChange = new vscode.EventEmitter(); @@ -136,10 +138,16 @@ class TunnelProvider implements vscode.TunnelProvider { public readonly onDidStateChange = this.stateChange.event; - constructor(private readonly logger: Logger) { } + constructor(private readonly logger: Logger, private readonly context: vscode.ExtensionContext) { } /** @inheritdoc */ - public async provideTunnel(tunnelOptions: vscode.TunnelOptions): Promise { + public async provideTunnel(tunnelOptions: vscode.TunnelOptions): Promise { + if (tunnelOptions.privacy === TunnelPrivacyId.Public) { + if (!(await this.consentPublicPort(tunnelOptions.remoteAddress.port))) { + return; + } + } + const tunnel = new Tunnel( tunnelOptions.remoteAddress, (tunnelOptions.privacy as TunnelPrivacyId) || TunnelPrivacyId.Private, @@ -184,6 +192,31 @@ class TunnelProvider implements vscode.TunnelProvider { this.updateActivePortsIfRunning(); } + private async consentPublicPort(portNumber: number) { + const didWarn = this.context.globalState.get(didWarnPublicKey, false); + if (didWarn) { + return true; + } + + const continueOpt = vscode.l10n.t('Continue'); + const dontShowAgain = vscode.l10n.t("Don't show again"); + const r = await vscode.window.showWarningMessage( + vscode.l10n.t("You're about to create a publicly forwarded port. Anyone on the internet will be able to connect to the service listening on port {0}. You should only proceed if this service is secure and non-sensitive.", portNumber), + { modal: true }, + continueOpt, + dontShowAgain, + ); + if (r === continueOpt) { + // continue + } else if (r === dontShowAgain) { + await this.context.globalState.update(didWarnPublicKey, true); + } else { + return false; + } + + return true; + } + private isInStateWithProcess(process: ChildProcessWithoutNullStreams) { return ( (this.state.state === State.Starting || this.state.state === State.Active) && diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index 38730118883..eda688b9a93 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -21,7 +21,8 @@ "restrictedConfigurations": [ "typescript.tsdk", "typescript.tsserver.pluginPaths", - "typescript.npm" + "typescript.npm", + "typescript.tsserver.nodePath" ] } }, @@ -1132,7 +1133,7 @@ "typescript.tsserver.maxTsServerMemory": { "type": "number", "default": 3072, - "description": "%configuration.tsserver.maxTsServerMemory%", + "markdownDescription": "%configuration.tsserver.maxTsServerMemory%", "scope": "window" }, "typescript.tsserver.experimental.enableProjectDiagnostics": { @@ -1251,6 +1252,11 @@ "description": "%configuration.tsserver.web.projectWideIntellisense.suppressSemanticErrors%", "scope": "window" }, + "typescript.tsserver.nodePath": { + "type": "string", + "description": "%configuration.tsserver.nodePath%", + "scope": "window" + }, "typescript.experimental.tsserver.web.typeAcquisition.enabled": { "type": "boolean", "default": false, diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index 641a4092870..c235219fef2 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -70,7 +70,7 @@ "configuration.tsserver.useSyntaxServer.always": "Use a lighter weight syntax server to handle all IntelliSense operations. This syntax server can only provide IntelliSense for opened files.", "configuration.tsserver.useSyntaxServer.never": "Don't use a dedicated syntax server. Use a single server to handle all IntelliSense operations.", "configuration.tsserver.useSyntaxServer.auto": "Spawn both a full server and a lighter weight server dedicated to syntax operations. The syntax server is used to speed up syntax operations and provide IntelliSense while projects are loading.", - "configuration.tsserver.maxTsServerMemory": "The maximum amount of memory (in MB) to allocate to the TypeScript server process.", + "configuration.tsserver.maxTsServerMemory": "The maximum amount of memory (in MB) to allocate to the TypeScript server process. To use a memory limit greater than 4 GB, use `#typescript.tsserver.nodePath#` to run TS Server with a custom Node installation.", "configuration.tsserver.experimental.enableProjectDiagnostics": "(Experimental) Enables project wide error reporting.", "typescript.locale": "Sets the locale used to report JavaScript and TypeScript errors. Defaults to use VS Code's locale.", "configuration.implicitProjectConfig.module": "Sets the module system for the program. See more: https://www.typescriptlang.org/tsconfig#module.", @@ -213,6 +213,7 @@ "configuration.suggest.objectLiteralMethodSnippets.enabled": "Enable/disable snippet completions for methods in object literals. Requires using TypeScript 4.7+ in the workspace.", "configuration.tsserver.web.projectWideIntellisense.enabled": "Enable/disable project-wide IntelliSense on web. Requires that VS Code is running in a trusted context.", "configuration.tsserver.web.projectWideIntellisense.suppressSemanticErrors": "Suppresses semantic errors. This is needed when using external packages as these can't be included analyzed on web.", + "configuration.tsserver.nodePath": "Run TS Server on a custom Node installation. This can be a path to a Node executable, or 'node' if you want VS Code to detect a Node installation.", "configuration.experimental.tsserver.web.typeAcquisition.enabled": "Enable/disable package acquisition on the web.", "walkthroughs.nodejsWelcome.title": "Get started with JavaScript and Node.js", "walkthroughs.nodejsWelcome.description": "Make the most of Visual Studio Code's first-class JavaScript experience.", diff --git a/extensions/typescript-language-features/src/configuration/configuration.browser.ts b/extensions/typescript-language-features/src/configuration/configuration.browser.ts index cfe7ed8b74d..15d4705de0e 100644 --- a/extensions/typescript-language-features/src/configuration/configuration.browser.ts +++ b/extensions/typescript-language-features/src/configuration/configuration.browser.ts @@ -16,4 +16,13 @@ export class BrowserServiceConfigurationProvider extends BaseServiceConfiguratio protected readLocalTsdk(_configuration: vscode.WorkspaceConfiguration): string | null { return null; } + + // On browsers, we don't run TSServer on Node + protected readLocalNodePath(_configuration: vscode.WorkspaceConfiguration): string | null { + return null; + } + + protected override readGlobalNodePath(_configuration: vscode.WorkspaceConfiguration): string | null { + return null; + } } diff --git a/extensions/typescript-language-features/src/configuration/configuration.electron.ts b/extensions/typescript-language-features/src/configuration/configuration.electron.ts index db84603c314..0c2a7ab12f7 100644 --- a/extensions/typescript-language-features/src/configuration/configuration.electron.ts +++ b/extensions/typescript-language-features/src/configuration/configuration.electron.ts @@ -6,7 +6,10 @@ import * as os from 'os'; import * as path from 'path'; import * as vscode from 'vscode'; +import * as child_process from 'child_process'; +import * as fs from 'fs'; import { BaseServiceConfigurationProvider } from './configuration'; +import { RelativeWorkspacePathResolver } from '../utils/relativePathResolver'; export class ElectronServiceConfigurationProvider extends BaseServiceConfigurationProvider { @@ -35,4 +38,65 @@ export class ElectronServiceConfigurationProvider extends BaseServiceConfigurati } return null; } + + protected readLocalNodePath(configuration: vscode.WorkspaceConfiguration): string | null { + return this.validatePath(this.readLocalNodePathWorker(configuration)); + } + + private readLocalNodePathWorker(configuration: vscode.WorkspaceConfiguration): string | null { + const inspect = configuration.inspect('typescript.tsserver.nodePath'); + if (inspect?.workspaceValue && typeof inspect.workspaceValue === 'string') { + if (inspect.workspaceValue === 'node') { + return this.findNodePath(); + } + const fixedPath = this.fixPathPrefixes(inspect.workspaceValue); + if (!path.isAbsolute(fixedPath)) { + const workspacePath = RelativeWorkspacePathResolver.asAbsoluteWorkspacePath(fixedPath); + return workspacePath || null; + } + return fixedPath; + } + return null; + } + + protected readGlobalNodePath(configuration: vscode.WorkspaceConfiguration): string | null { + return this.validatePath(this.readGlobalNodePathWorker(configuration)); + } + + private readGlobalNodePathWorker(configuration: vscode.WorkspaceConfiguration): string | null { + const inspect = configuration.inspect('typescript.tsserver.nodePath'); + if (inspect?.globalValue && typeof inspect.globalValue === 'string') { + if (inspect.globalValue === 'node') { + return this.findNodePath(); + } + const fixedPath = this.fixPathPrefixes(inspect.globalValue); + if (path.isAbsolute(fixedPath)) { + return fixedPath; + } + } + return null; + } + + private findNodePath(): string | null { + try { + const out = child_process.execFileSync('node', ['-e', 'console.log(process.execPath)'], { + windowsHide: true, + timeout: 2000, + cwd: vscode.workspace.workspaceFolders?.[0].uri.fsPath, + encoding: 'utf-8', + }); + return out.trim(); + } catch (error) { + vscode.window.showWarningMessage(vscode.l10n.t("Could not detect a Node installation to run TS Server.")); + return null; + } + } + + private validatePath(nodePath: string | null): string | null { + if (nodePath && (!fs.existsSync(nodePath) || fs.lstatSync(nodePath).isDirectory())) { + vscode.window.showWarningMessage(vscode.l10n.t("The path {0} doesn\'t point to a valid Node installation to run TS Server. Falling back to bundled Node.", nodePath)); + return null; + } + return nodePath; + } } diff --git a/extensions/typescript-language-features/src/configuration/configuration.ts b/extensions/typescript-language-features/src/configuration/configuration.ts index f3a5817146b..0d60cd74932 100644 --- a/extensions/typescript-language-features/src/configuration/configuration.ts +++ b/extensions/typescript-language-features/src/configuration/configuration.ts @@ -120,6 +120,8 @@ export interface TypeScriptServiceConfiguration { readonly watchOptions: Proto.WatchOptions | undefined; readonly includePackageJsonAutoImports: 'auto' | 'on' | 'off' | undefined; readonly enableTsServerTracing: boolean; + readonly localNodePath: string | null; + readonly globalNodePath: string | null; } export function areServiceConfigurationsEqual(a: TypeScriptServiceConfiguration, b: TypeScriptServiceConfiguration): boolean { @@ -154,11 +156,15 @@ export abstract class BaseServiceConfigurationProvider implements ServiceConfigu watchOptions: this.readWatchOptions(configuration), includePackageJsonAutoImports: this.readIncludePackageJsonAutoImports(configuration), enableTsServerTracing: this.readEnableTsServerTracing(configuration), + localNodePath: this.readLocalNodePath(configuration), + globalNodePath: this.readGlobalNodePath(configuration), }; } protected abstract readGlobalTsdk(configuration: vscode.WorkspaceConfiguration): string | null; protected abstract readLocalTsdk(configuration: vscode.WorkspaceConfiguration): string | null; + protected abstract readLocalNodePath(configuration: vscode.WorkspaceConfiguration): string | null; + protected abstract readGlobalNodePath(configuration: vscode.WorkspaceConfiguration): string | null; protected readTsServerLogLevel(configuration: vscode.WorkspaceConfiguration): TsServerLogLevel { const setting = configuration.get('typescript.tsserver.log', 'off'); diff --git a/extensions/typescript-language-features/src/tsServer/nodeManager.ts b/extensions/typescript-language-features/src/tsServer/nodeManager.ts new file mode 100644 index 00000000000..037fc1898e8 --- /dev/null +++ b/extensions/typescript-language-features/src/tsServer/nodeManager.ts @@ -0,0 +1,149 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { TypeScriptServiceConfiguration } from '../configuration/configuration'; +import { setImmediate } from '../utils/async'; +import { Disposable } from '../utils/dispose'; + + +const useWorkspaceNodeStorageKey = 'typescript.useWorkspaceNode'; +const lastKnownWorkspaceNodeStorageKey = 'typescript.lastKnownWorkspaceNode'; +type UseWorkspaceNodeState = undefined | boolean; +type LastKnownWorkspaceNodeState = undefined | string; + +export class NodeVersionManager extends Disposable { + private _currentVersion: string | undefined; + + public constructor( + private configuration: TypeScriptServiceConfiguration, + private readonly workspaceState: vscode.Memento + ) { + super(); + + this._currentVersion = this.configuration.globalNodePath || undefined; + if (vscode.workspace.isTrusted) { + const workspaceVersion = this.configuration.localNodePath; + if (workspaceVersion) { + const useWorkspaceNode = this.canUseWorkspaceNode(workspaceVersion); + if (useWorkspaceNode === undefined) { + setImmediate(() => { + this.promptAndSetWorkspaceNode(); + }); + } + else if (useWorkspaceNode) { + this._currentVersion = workspaceVersion; + } + } + } + else { + this._disposables.push(vscode.workspace.onDidGrantWorkspaceTrust(() => { + const workspaceVersion = this.configuration.localNodePath; + if (workspaceVersion) { + const useWorkspaceNode = this.canUseWorkspaceNode(workspaceVersion); + if (useWorkspaceNode === undefined) { + setImmediate(() => { + this.promptAndSetWorkspaceNode(); + }); + } + else if (useWorkspaceNode) { + this.updateActiveVersion(workspaceVersion); + } + } + })); + } + } + + private readonly _onDidPickNewVersion = this._register(new vscode.EventEmitter()); + public readonly onDidPickNewVersion = this._onDidPickNewVersion.event; + + public get currentVersion(): string | undefined { + return this._currentVersion; + } + + public async updateConfiguration(nextConfiguration: TypeScriptServiceConfiguration) { + const oldConfiguration = this.configuration; + this.configuration = nextConfiguration; + if (oldConfiguration.globalNodePath !== nextConfiguration.globalNodePath + || oldConfiguration.localNodePath !== nextConfiguration.localNodePath) { + await this.computeNewVersion(); + } + } + + private async computeNewVersion() { + let version = this.configuration.globalNodePath || undefined; + const workspaceVersion = this.configuration.localNodePath; + if (vscode.workspace.isTrusted && workspaceVersion) { + const useWorkspaceNode = this.canUseWorkspaceNode(workspaceVersion); + if (useWorkspaceNode === undefined) { + version = await this.promptUseWorkspaceNode() || version; + } + else if (useWorkspaceNode) { + version = workspaceVersion; + } + } + this.updateActiveVersion(version); + } + + private async promptUseWorkspaceNode(): Promise { + const workspaceVersion = this.configuration.localNodePath; + if (workspaceVersion === null) { + throw new Error('Could not prompt to use workspace Node installation because no workspace Node installation is specified'); + } + + const allow = vscode.l10n.t("Yes"); + const disallow = vscode.l10n.t("No"); + const dismiss = vscode.l10n.t("Not now"); + + const result = await vscode.window.showInformationMessage(vscode.l10n.t("This workspace wants to use the Node installation at '{0}' to run TS Server. Would you like to use it?", workspaceVersion), + allow, + disallow, + dismiss, + ); + + let version = undefined; + switch (result) { + case allow: + await this.setUseWorkspaceNodeState(true, workspaceVersion); + version = workspaceVersion; + break; + case disallow: + await this.setUseWorkspaceNodeState(false, workspaceVersion); + break; + case dismiss: + await this.setUseWorkspaceNodeState(undefined, workspaceVersion); + break; + } + return version; + } + + private async promptAndSetWorkspaceNode(): Promise { + const version = await this.promptUseWorkspaceNode(); + if (version !== undefined) { + this.updateActiveVersion(version); + } + } + + private updateActiveVersion(pickedVersion: string | undefined): void { + const oldVersion = this.currentVersion; + this._currentVersion = pickedVersion; + if (oldVersion !== pickedVersion) { + this._onDidPickNewVersion.fire(); + } + } + + private canUseWorkspaceNode(nodeVersion: string): boolean | undefined { + const lastKnownWorkspaceNode = this.workspaceState.get(lastKnownWorkspaceNodeStorageKey); + if (lastKnownWorkspaceNode === nodeVersion) { + return this.workspaceState.get(useWorkspaceNodeStorageKey); + } + return undefined; + } + + private async setUseWorkspaceNodeState(allow: boolean | undefined, nodeVersion: string) { + await this.workspaceState.update(lastKnownWorkspaceNodeStorageKey, nodeVersion); + await this.workspaceState.update(useWorkspaceNodeStorageKey, allow); + } +} diff --git a/extensions/typescript-language-features/src/tsServer/server.ts b/extensions/typescript-language-features/src/tsServer/server.ts index 421c5f5d8e9..883aa6830bd 100644 --- a/extensions/typescript-language-features/src/tsServer/server.ts +++ b/extensions/typescript-language-features/src/tsServer/server.ts @@ -19,6 +19,7 @@ import type * as Proto from './protocol/protocol'; import { EventName } from './protocol/protocol.const'; import { TypeScriptVersionManager } from './versionManager'; import { TypeScriptVersion } from './versionProvider'; +import { NodeVersionManager } from './nodeManager'; export enum ExecutionTarget { Semantic, @@ -70,6 +71,7 @@ export interface TsServerProcessFactory { kind: TsServerProcessKind, configuration: TypeScriptServiceConfiguration, versionManager: TypeScriptVersionManager, + nodeVersionManager: NodeVersionManager, tsServerLog: TsServerLog | undefined, ): TsServerProcess; } diff --git a/extensions/typescript-language-features/src/tsServer/serverProcess.browser.ts b/extensions/typescript-language-features/src/tsServer/serverProcess.browser.ts index c57e6d352c9..bb57c2644b4 100644 --- a/extensions/typescript-language-features/src/tsServer/serverProcess.browser.ts +++ b/extensions/typescript-language-features/src/tsServer/serverProcess.browser.ts @@ -13,6 +13,7 @@ import type * as Proto from './protocol/protocol'; import { TsServerLog, TsServerProcess, TsServerProcessFactory, TsServerProcessKind } from './server'; import { TypeScriptVersionManager } from './versionManager'; import { TypeScriptVersion } from './versionProvider'; +import { NodeVersionManager } from './nodeManager'; type BrowserWatchEvent = { type: 'watchDirectory' | 'watchFile'; @@ -40,6 +41,7 @@ export class WorkerServerProcessFactory implements TsServerProcessFactory { kind: TsServerProcessKind, _configuration: TypeScriptServiceConfiguration, _versionManager: TypeScriptVersionManager, + _nodeVersionManager: NodeVersionManager, tsServerLog: TsServerLog | undefined, ) { const tsServerPath = version.tsServerPath; diff --git a/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts b/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts index b5848d5eb9f..8b0ec2fb7b7 100644 --- a/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts +++ b/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts @@ -15,6 +15,7 @@ import type * as Proto from './protocol/protocol'; import { TsServerLog, TsServerProcess, TsServerProcessFactory, TsServerProcessKind } from './server'; import { TypeScriptVersionManager } from './versionManager'; import { TypeScriptVersion } from './versionProvider'; +import { NodeVersionManager } from './nodeManager'; const defaultSize: number = 8192; @@ -134,10 +135,12 @@ class Reader extends Disposable { } } -function generatePatchedEnv(env: any, modulePath: string): any { +function generatePatchedEnv(env: any, modulePath: string, hasExecPath: boolean): any { const newEnv = Object.assign({}, env); - newEnv['ELECTRON_RUN_AS_NODE'] = '1'; + if (!hasExecPath) { + newEnv['ELECTRON_RUN_AS_NODE'] = '1'; + } newEnv['NODE_PATH'] = path.join(modulePath, '..', '..', '..'); // Ensure we always have a PATH set @@ -253,6 +256,7 @@ export class ElectronServiceProcessFactory implements TsServerProcessFactory { kind: TsServerProcessKind, configuration: TypeScriptServiceConfiguration, versionManager: TypeScriptVersionManager, + nodeVersionManager: NodeVersionManager, _tsserverLog: TsServerLog | undefined, ): TsServerProcess { let tsServerPath = version.tsServerPath; @@ -263,20 +267,30 @@ export class ElectronServiceProcessFactory implements TsServerProcessFactory { tsServerPath = versionManager.currentVersion.tsServerPath; } - const useIpc = version.apiVersion?.gte(API.v460); + const execPath = nodeVersionManager.currentVersion; + const env = generatePatchedEnv(process.env, tsServerPath, !!execPath); const runtimeArgs = [...args]; + const execArgv = getExecArgv(kind, configuration); + const useIpc = !execPath && version.apiVersion?.gte(API.v460); if (useIpc) { runtimeArgs.push('--useNodeIpc'); } - const childProcess = child_process.fork(tsServerPath, runtimeArgs, { - silent: true, - cwd: undefined, - env: generatePatchedEnv(process.env, tsServerPath), - execArgv: getExecArgv(kind, configuration), - stdio: useIpc ? ['pipe', 'pipe', 'pipe', 'ipc'] : undefined, - }); + const childProcess = execPath ? + child_process.spawn(execPath, [...execArgv, tsServerPath, ...runtimeArgs], { + shell: true, + windowsHide: true, + cwd: undefined, + env, + }) : + child_process.fork(tsServerPath, runtimeArgs, { + silent: true, + cwd: undefined, + env, + execArgv, + stdio: useIpc ? ['pipe', 'pipe', 'pipe', 'ipc'] : undefined, + }); return useIpc ? new IpcChildServerProcess(childProcess) : new StdioChildServerProcess(childProcess); } diff --git a/extensions/typescript-language-features/src/tsServer/spawner.ts b/extensions/typescript-language-features/src/tsServer/spawner.ts index 0fa9bedf4a6..52dcf5baa19 100644 --- a/extensions/typescript-language-features/src/tsServer/spawner.ts +++ b/extensions/typescript-language-features/src/tsServer/spawner.ts @@ -19,6 +19,7 @@ import { PluginManager } from './plugins'; import { GetErrRoutingTsServer, ITypeScriptServer, SingleTsServer, SyntaxRoutingTsServer, TsServerDelegate, TsServerLog, TsServerProcessFactory, TsServerProcessKind } from './server'; import { TypeScriptVersionManager } from './versionManager'; import { ITypeScriptVersionProvider, TypeScriptVersion } from './versionProvider'; +import { NodeVersionManager } from './nodeManager'; const enum CompositeServerType { /** Run a single server that handles all commands */ @@ -44,6 +45,7 @@ export class TypeScriptServerSpawner { public constructor( private readonly _versionProvider: ITypeScriptVersionProvider, private readonly _versionManager: TypeScriptVersionManager, + private readonly _nodeVersionManager: NodeVersionManager, private readonly _logDirectoryProvider: ILogDirectoryProvider, private readonly _pluginPathsProvider: TypeScriptPluginPathsProvider, private readonly _logger: Logger, @@ -160,7 +162,7 @@ export class TypeScriptServerSpawner { } this._logger.info(`<${kind}> Forking...`); - const process = this._factory.fork(version, args, kind, configuration, this._versionManager, tsServerLog); + const process = this._factory.fork(version, args, kind, configuration, this._versionManager, this._nodeVersionManager, tsServerLog); this._logger.info(`<${kind}> Starting...`); return new SingleTsServer( diff --git a/extensions/typescript-language-features/src/typescriptServiceClient.ts b/extensions/typescript-language-features/src/typescriptServiceClient.ts index e00bed60b3f..5b7591bfd8f 100644 --- a/extensions/typescript-language-features/src/typescriptServiceClient.ts +++ b/extensions/typescript-language-features/src/typescriptServiceClient.ts @@ -30,6 +30,7 @@ import { TelemetryProperties, TelemetryReporter, VSCodeTelemetryReporter } from import Tracer from './logging/tracer'; import { ProjectType, inferredProjectCompilerOptions } from './tsconfig'; import { Schemes } from './configuration/schemes'; +import { NodeVersionManager } from './tsServer/nodeManager'; export interface TsDiagnostics { @@ -103,6 +104,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType private _configuration: TypeScriptServiceConfiguration; private readonly pluginPathsProvider: TypeScriptPluginPathsProvider; private readonly _versionManager: TypeScriptVersionManager; + private readonly _nodeVersionManager: NodeVersionManager; private readonly logger: Logger; private readonly tracer: Tracer; @@ -173,6 +175,11 @@ export default class TypeScriptServiceClient extends Disposable implements IType this.restartTsServer(); })); + this._nodeVersionManager = this._register(new NodeVersionManager(this._configuration, context.workspaceState)); + this._register(this._nodeVersionManager.onDidPickNewVersion(() => { + this.restartTsServer(); + })); + this.bufferSyncSupport = new BufferSyncSupport(this, allModeIds, onCaseInsenitiveFileSystem); this.onReady(() => { this.bufferSyncSupport.listen(); }); @@ -192,6 +199,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType this.versionProvider.updateConfiguration(this._configuration); this._versionManager.updateConfiguration(this._configuration); this.pluginPathsProvider.updateConfiguration(this._configuration); + this._nodeVersionManager.updateConfiguration(this._configuration); if (this.serverState.type === ServerState.Type.Running) { if (!this._configuration.implicitProjectConfiguration.isEqualTo(oldConfiguration.implicitProjectConfiguration)) { @@ -212,8 +220,9 @@ export default class TypeScriptServiceClient extends Disposable implements IType } return this.apiVersion.fullVersionString; }); + this.diagnosticsManager = new DiagnosticsManager('typescript', this._configuration, this.telemetryReporter, onCaseInsenitiveFileSystem); - this.typescriptServerSpawner = new TypeScriptServerSpawner(this.versionProvider, this._versionManager, this.logDirectoryProvider, this.pluginPathsProvider, this.logger, this.telemetryReporter, this.tracer, this.processFactory); + this.typescriptServerSpawner = new TypeScriptServerSpawner(this.versionProvider, this._versionManager, this._nodeVersionManager, this.logDirectoryProvider, this.pluginPathsProvider, this.logger, this.telemetryReporter, this.tracer, this.processFactory); this._register(this.pluginManager.onDidUpdateConfig(update => { this.configurePlugin(update.pluginId, update.config); @@ -387,6 +396,10 @@ export default class TypeScriptServiceClient extends Disposable implements IType } this.info(`Using tsserver from: ${version.path}`); + const nodePath = this._nodeVersionManager.currentVersion; + if (nodePath) { + this.info(`Using Node installation from ${nodePath} to run TS Server`); + } const apiVersion = version.apiVersion || API.defaultVersion; const mytoken = ++this.token; diff --git a/extensions/vb/package.json b/extensions/vb/package.json index 6e5dd080c0b..801ef7180da 100644 --- a/extensions/vb/package.json +++ b/extensions/vb/package.json @@ -9,7 +9,7 @@ "vscode": "*" }, "scripts": { - "update-grammar": "node ../node_modules/vscode-grammar-updater/bin textmate/asp.vb.net.tmbundle Syntaxes/ASP%%20VB.net.plist ./syntaxes/asp-vb-net.tmlanguage.json" + "update-grammar": "node ../node_modules/vscode-grammar-updater/bin textmate/asp.vb.net.tmbundle Syntaxes/ASP%20VB.net.plist ./syntaxes/asp-vb-net.tmlanguage.json" }, "contributes": { "languages": [ diff --git a/extensions/vscode-colorize-tests/package.json b/extensions/vscode-colorize-tests/package.json index 8cb8442b6f2..eb72136ccf4 100644 --- a/extensions/vscode-colorize-tests/package.json +++ b/extensions/vscode-colorize-tests/package.json @@ -17,7 +17,7 @@ "vscode:prepublish": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:vscode-colorize-tests ./tsconfig.json" }, "dependencies": { - "jsonc-parser": "2.2.1" + "jsonc-parser": "^3.2.0" }, "devDependencies": { "@types/node": "18.x" diff --git a/extensions/vscode-colorize-tests/test/colorize-fixtures/test.p6 b/extensions/vscode-colorize-tests/test/colorize-fixtures/test.p6 new file mode 100644 index 00000000000..e8b55eb6f3d --- /dev/null +++ b/extensions/vscode-colorize-tests/test/colorize-fixtures/test.p6 @@ -0,0 +1,27 @@ +# Example taken from https://en.wikipedia.org/wiki/Raku_(programming_language) +class Point is rw { + has $.x; + has $.y; + + method distance( Point $p ) { + sqrt(($!x - $p.x) ** 2 + ($!y - $p.y) ** 2) + } + + method distance-to-center { + self.distance: Point.new(x => 0, y => 0) + } +} + +my $point = Point.new( x => 1.2, y => -3.7 ); +say "Point's location: (", $point.x, ', ', $point.y, ')'; +# OUTPUT: Point's location: (1.2, -3.7) + +# Changing x and y (note methods "x" and "y" used as lvalues): +$point.x = 3; +$point.y = 4; +say "Point's location: (", $point.x, ', ', $point.y, ')'; +# OUTPUT: Point's location: (3, 4) + +my $other-point = Point.new(x => -5, y => 10); +$point.distance($other-point); #=> 10 +$point.distance-to-center; #=> 5 diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_bib.json b/extensions/vscode-colorize-tests/test/colorize-results/test_bib.json index 05b234780ac..0bf691548b3 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_bib.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_bib.json @@ -1,21 +1,7 @@ [ { - "c": "%", - "t": "text.bibtex comment.line.percentage.bibtex punctuation.definition.comment.bibtex", - "r": { - "dark_plus": "comment: #6A9955", - "light_plus": "comment: #008000", - "dark_vs": "comment: #6A9955", - "light_vs": "comment: #008000", - "hc_black": "comment: #7CA668", - "dark_modern": "comment: #6A9955", - "hc_light": "comment: #515151", - "light_modern": "comment: #008000" - } - }, - { - "c": " a sample bibliography file", - "t": "text.bibtex comment.line.percentage.bibtex", + "c": "% a sample bibliography file", + "t": "text.bibtex comment.block.bibtex", "r": { "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", @@ -29,7 +15,7 @@ }, { "c": "%", - "t": "text.bibtex comment.line.percentage.bibtex punctuation.definition.comment.bibtex", + "t": "text.bibtex comment.block.bibtex", "r": { "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", @@ -1386,8 +1372,8 @@ } }, { - "c": "%", - "t": "text.bibtex comment.line.percentage.bibtex punctuation.definition.comment.bibtex", + "c": "% The authors mentioned here are almost, but not quite,", + "t": "text.bibtex comment.block.bibtex", "r": { "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", @@ -1400,36 +1386,8 @@ } }, { - "c": " The authors mentioned here are almost, but not quite,", - "t": "text.bibtex comment.line.percentage.bibtex", - "r": { - "dark_plus": "comment: #6A9955", - "light_plus": "comment: #008000", - "dark_vs": "comment: #6A9955", - "light_vs": "comment: #008000", - "hc_black": "comment: #7CA668", - "dark_modern": "comment: #6A9955", - "hc_light": "comment: #515151", - "light_modern": "comment: #008000" - } - }, - { - "c": "%", - "t": "text.bibtex comment.line.percentage.bibtex punctuation.definition.comment.bibtex", - "r": { - "dark_plus": "comment: #6A9955", - "light_plus": "comment: #008000", - "dark_vs": "comment: #6A9955", - "light_vs": "comment: #008000", - "hc_black": "comment: #7CA668", - "dark_modern": "comment: #6A9955", - "hc_light": "comment: #515151", - "light_modern": "comment: #008000" - } - }, - { - "c": " entirely unrelated to Matt Groening.", - "t": "text.bibtex comment.line.percentage.bibtex", + "c": "% entirely unrelated to Matt Groening.", + "t": "text.bibtex comment.block.bibtex", "r": { "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_cs.json b/extensions/vscode-colorize-tests/test/colorize-results/test_cs.json index 34a6e9e8654..e846aa23b4e 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_cs.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_cs.json @@ -1,16 +1,16 @@ [ { "c": "using", - "t": "source.cs keyword.other.using.cs", + "t": "source.cs keyword.other.directive.using.cs", "r": { - "dark_plus": "keyword.other.using: #C586C0", - "light_plus": "keyword.other.using: #AF00DB", + "dark_plus": "keyword.other.directive.using: #C586C0", + "light_plus": "keyword.other.directive.using: #AF00DB", "dark_vs": "keyword: #569CD6", "light_vs": "keyword: #0000FF", - "hc_black": "keyword.other.using: #C586C0", - "dark_modern": "keyword.other.using: #C586C0", - "hc_light": "keyword.other.using: #B5200D", - "light_modern": "keyword.other.using: #AF00DB" + "hc_black": "keyword.other.directive.using: #C586C0", + "dark_modern": "keyword.other.directive.using: #C586C0", + "hc_light": "keyword.other.directive.using: #B5200D", + "light_modern": "keyword.other.directive.using: #AF00DB" } }, { @@ -57,16 +57,16 @@ }, { "c": "namespace", - "t": "source.cs keyword.other.namespace.cs", + "t": "source.cs storage.type.namespace.cs", "r": { - "dark_plus": "keyword: #569CD6", - "light_plus": "keyword: #0000FF", - "dark_vs": "keyword: #569CD6", - "light_vs": "keyword: #0000FF", - "hc_black": "keyword: #569CD6", - "dark_modern": "keyword: #569CD6", - "hc_light": "keyword: #0F4A85", - "light_modern": "keyword: #0000FF" + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" } }, { @@ -127,16 +127,16 @@ }, { "c": "class", - "t": "source.cs keyword.other.class.cs", + "t": "source.cs storage.type.class.cs", "r": { - "dark_plus": "keyword: #569CD6", - "light_plus": "keyword: #0000FF", - "dark_vs": "keyword: #569CD6", - "light_vs": "keyword: #0000FF", - "hc_black": "keyword: #569CD6", - "dark_modern": "keyword: #569CD6", - "hc_light": "keyword: #0F4A85", - "light_modern": "keyword: #0000FF" + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" } }, { @@ -211,7 +211,7 @@ }, { "c": "static", - "t": "source.cs storage.modifier.cs", + "t": "source.cs storage.modifier.static.cs", "r": { "dark_plus": "storage.modifier: #569CD6", "light_plus": "storage.modifier: #0000FF", @@ -239,7 +239,7 @@ }, { "c": "void", - "t": "source.cs keyword.type.cs", + "t": "source.cs keyword.type.void.cs", "r": { "dark_plus": "keyword: #569CD6", "light_plus": "keyword: #0000FF", @@ -295,7 +295,7 @@ }, { "c": "string", - "t": "source.cs keyword.type.cs", + "t": "source.cs keyword.type.string.cs", "r": { "dark_plus": "keyword: #569CD6", "light_plus": "keyword: #0000FF", @@ -421,7 +421,7 @@ }, { "c": "int", - "t": "source.cs keyword.type.cs", + "t": "source.cs keyword.type.int.cs", "r": { "dark_plus": "keyword: #569CD6", "light_plus": "keyword: #0000FF", @@ -799,7 +799,7 @@ }, { "c": "const", - "t": "source.cs storage.modifier.cs", + "t": "source.cs storage.modifier.const.cs", "r": { "dark_plus": "storage.modifier: #569CD6", "light_plus": "storage.modifier: #0000FF", @@ -827,7 +827,7 @@ }, { "c": "double", - "t": "source.cs keyword.type.cs", + "t": "source.cs keyword.type.double.cs", "r": { "dark_plus": "keyword: #569CD6", "light_plus": "keyword: #0000FF", @@ -1023,7 +1023,7 @@ }, { "c": "int", - "t": "source.cs keyword.type.cs", + "t": "source.cs keyword.type.int.cs", "r": { "dark_plus": "keyword: #569CD6", "light_plus": "keyword: #0000FF", @@ -1177,7 +1177,7 @@ }, { "c": "double", - "t": "source.cs keyword.type.cs", + "t": "source.cs keyword.type.double.cs", "r": { "dark_plus": "keyword: #569CD6", "light_plus": "keyword: #0000FF", @@ -1429,16 +1429,16 @@ }, { "c": " ", - "t": "source.cs punctuation.whitespace.comment.leading.cs", + "t": "source.cs comment.line.double-slash.cs punctuation.whitespace.comment.leading.cs", "r": { - "dark_plus": "default: #D4D4D4", - "light_plus": "default: #000000", - "dark_vs": "default: #D4D4D4", - "light_vs": "default: #000000", - "hc_black": "default: #FFFFFF", - "dark_modern": "default: #CCCCCC", - "hc_light": "default: #292929", - "light_modern": "default: #3B3B3B" + "dark_plus": "comment: #6A9955", + "light_plus": "comment: #008000", + "dark_vs": "comment: #6A9955", + "light_vs": "comment: #008000", + "hc_black": "comment: #7CA668", + "dark_modern": "comment: #6A9955", + "hc_light": "comment: #515151", + "light_modern": "comment: #008000" } }, { @@ -1751,7 +1751,7 @@ }, { "c": "public", - "t": "source.cs storage.modifier.cs", + "t": "source.cs storage.modifier.public.cs", "r": { "dark_plus": "storage.modifier: #569CD6", "light_plus": "storage.modifier: #0000FF", @@ -1779,7 +1779,7 @@ }, { "c": "void", - "t": "source.cs keyword.type.cs", + "t": "source.cs keyword.type.void.cs", "r": { "dark_plus": "keyword: #569CD6", "light_plus": "keyword: #0000FF", @@ -1947,16 +1947,16 @@ }, { "c": "new", - "t": "source.cs keyword.other.new.cs", + "t": "source.cs keyword.operator.expression.new.cs", "r": { - "dark_plus": "keyword: #569CD6", - "light_plus": "keyword: #0000FF", - "dark_vs": "keyword: #569CD6", - "light_vs": "keyword: #0000FF", - "hc_black": "keyword: #569CD6", - "dark_modern": "keyword: #569CD6", - "hc_light": "keyword: #0F4A85", - "light_modern": "keyword: #0000FF" + "dark_plus": "keyword.operator.expression: #569CD6", + "light_plus": "keyword.operator.expression: #0000FF", + "dark_vs": "keyword.operator.expression: #569CD6", + "light_vs": "keyword.operator.expression: #0000FF", + "hc_black": "keyword.operator.expression: #569CD6", + "dark_modern": "keyword.operator.expression: #569CD6", + "hc_light": "keyword.operator.expression: #0F4A85", + "light_modern": "keyword.operator.expression: #0000FF" } }, { @@ -2003,7 +2003,7 @@ }, { "c": "int", - "t": "source.cs keyword.type.cs", + "t": "source.cs keyword.type.int.cs", "r": { "dark_plus": "keyword: #569CD6", "light_plus": "keyword: #0000FF", @@ -2115,7 +2115,7 @@ }, { "c": "int", - "t": "source.cs keyword.type.cs", + "t": "source.cs keyword.type.int.cs", "r": { "dark_plus": "keyword: #569CD6", "light_plus": "keyword: #0000FF", @@ -2255,16 +2255,16 @@ }, { "c": "new", - "t": "source.cs keyword.other.new.cs", + "t": "source.cs keyword.operator.expression.new.cs", "r": { - "dark_plus": "keyword: #569CD6", - "light_plus": "keyword: #0000FF", - "dark_vs": "keyword: #569CD6", - "light_vs": "keyword: #0000FF", - "hc_black": "keyword: #569CD6", - "dark_modern": "keyword: #569CD6", - "hc_light": "keyword: #0F4A85", - "light_modern": "keyword: #0000FF" + "dark_plus": "keyword.operator.expression: #569CD6", + "light_plus": "keyword.operator.expression: #0000FF", + "dark_vs": "keyword.operator.expression: #569CD6", + "light_vs": "keyword.operator.expression: #0000FF", + "hc_black": "keyword.operator.expression: #569CD6", + "dark_modern": "keyword.operator.expression: #569CD6", + "hc_light": "keyword.operator.expression: #0F4A85", + "light_modern": "keyword.operator.expression: #0000FF" } }, { @@ -2311,7 +2311,7 @@ }, { "c": "int", - "t": "source.cs keyword.type.cs", + "t": "source.cs keyword.type.int.cs", "r": { "dark_plus": "keyword: #569CD6", "light_plus": "keyword: #0000FF", @@ -2423,7 +2423,7 @@ }, { "c": "int", - "t": "source.cs keyword.type.cs", + "t": "source.cs keyword.type.int.cs", "r": { "dark_plus": "keyword: #569CD6", "light_plus": "keyword: #0000FF", @@ -2521,16 +2521,16 @@ }, { "c": "new", - "t": "source.cs keyword.other.new.cs", + "t": "source.cs keyword.operator.expression.new.cs", "r": { - "dark_plus": "keyword: #569CD6", - "light_plus": "keyword: #0000FF", - "dark_vs": "keyword: #569CD6", - "light_vs": "keyword: #0000FF", - "hc_black": "keyword: #569CD6", - "dark_modern": "keyword: #569CD6", - "hc_light": "keyword: #0F4A85", - "light_modern": "keyword: #0000FF" + "dark_plus": "keyword.operator.expression: #569CD6", + "light_plus": "keyword.operator.expression: #0000FF", + "dark_vs": "keyword.operator.expression: #569CD6", + "light_vs": "keyword.operator.expression: #0000FF", + "hc_black": "keyword.operator.expression: #569CD6", + "dark_modern": "keyword.operator.expression: #569CD6", + "hc_light": "keyword.operator.expression: #0F4A85", + "light_modern": "keyword.operator.expression: #0000FF" } }, { @@ -2577,7 +2577,7 @@ }, { "c": "int", - "t": "source.cs keyword.type.cs", + "t": "source.cs keyword.type.int.cs", "r": { "dark_plus": "keyword: #569CD6", "light_plus": "keyword: #0000FF", diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_cshtml.json b/extensions/vscode-colorize-tests/test/colorize-results/test_cshtml.json index 5945550fd4e..4307f03ed1e 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_cshtml.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_cshtml.json @@ -43,16 +43,16 @@ }, { "c": "var", - "t": "text.html.cshtml meta.structure.razor.codeblock source.cs keyword.other.var.cs", + "t": "text.html.cshtml meta.structure.razor.codeblock source.cs storage.type.var.cs", "r": { - "dark_plus": "keyword: #569CD6", - "light_plus": "keyword: #0000FF", - "dark_vs": "keyword: #569CD6", - "light_vs": "keyword: #0000FF", - "hc_black": "keyword: #569CD6", - "dark_modern": "keyword: #569CD6", - "hc_light": "keyword: #0F4A85", - "light_modern": "keyword: #0000FF" + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" } }, { @@ -169,16 +169,16 @@ }, { "c": "var", - "t": "text.html.cshtml meta.structure.razor.codeblock source.cs keyword.other.var.cs", + "t": "text.html.cshtml meta.structure.razor.codeblock source.cs storage.type.var.cs", "r": { - "dark_plus": "keyword: #569CD6", - "light_plus": "keyword: #0000FF", - "dark_vs": "keyword: #569CD6", - "light_vs": "keyword: #0000FF", - "hc_black": "keyword: #569CD6", - "dark_modern": "keyword: #569CD6", - "hc_light": "keyword: #0F4A85", - "light_modern": "keyword: #0000FF" + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" } }, { @@ -505,16 +505,16 @@ }, { "c": " ", - "t": "text.html.cshtml meta.structure.razor.codeblock source.cs meta.statement.if.razor meta.structure.razor.csharp.codeblock punctuation.whitespace.comment.leading.cs", + "t": "text.html.cshtml meta.structure.razor.codeblock source.cs meta.statement.if.razor meta.structure.razor.csharp.codeblock comment.line.double-slash.cs punctuation.whitespace.comment.leading.cs", "r": { - "dark_plus": "default: #D4D4D4", - "light_plus": "default: #000000", - "dark_vs": "default: #D4D4D4", - "light_vs": "default: #000000", - "hc_black": "default: #FFFFFF", - "dark_modern": "default: #CCCCCC", - "hc_light": "default: #292929", - "light_modern": "default: #3B3B3B" + "dark_plus": "comment: #6A9955", + "light_plus": "comment: #008000", + "dark_vs": "comment: #6A9955", + "light_vs": "comment: #008000", + "hc_black": "comment: #7CA668", + "dark_modern": "comment: #6A9955", + "hc_light": "comment: #515151", + "light_modern": "comment: #008000" } }, { @@ -561,16 +561,16 @@ }, { "c": "var", - "t": "text.html.cshtml meta.structure.razor.codeblock source.cs meta.statement.if.razor meta.structure.razor.csharp.codeblock keyword.other.var.cs", + "t": "text.html.cshtml meta.structure.razor.codeblock source.cs meta.statement.if.razor meta.structure.razor.csharp.codeblock storage.type.var.cs", "r": { - "dark_plus": "keyword: #569CD6", - "light_plus": "keyword: #0000FF", - "dark_vs": "keyword: #569CD6", - "light_vs": "keyword: #0000FF", - "hc_black": "keyword: #569CD6", - "dark_modern": "keyword: #569CD6", - "hc_light": "keyword: #0F4A85", - "light_modern": "keyword: #0000FF" + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" } }, { @@ -757,16 +757,16 @@ }, { "c": "var", - "t": "text.html.cshtml meta.structure.razor.codeblock source.cs meta.statement.if.razor meta.structure.razor.csharp.codeblock keyword.other.var.cs", + "t": "text.html.cshtml meta.structure.razor.codeblock source.cs meta.statement.if.razor meta.structure.razor.csharp.codeblock storage.type.var.cs", "r": { - "dark_plus": "keyword: #569CD6", - "light_plus": "keyword: #0000FF", - "dark_vs": "keyword: #569CD6", - "light_vs": "keyword: #0000FF", - "hc_black": "keyword: #569CD6", - "dark_modern": "keyword: #569CD6", - "hc_light": "keyword: #0F4A85", - "light_modern": "keyword: #0000FF" + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" } }, { @@ -939,16 +939,16 @@ }, { "c": " ", - "t": "text.html.cshtml meta.structure.razor.codeblock source.cs meta.statement.if.razor meta.structure.razor.csharp.codeblock punctuation.whitespace.comment.leading.cs", + "t": "text.html.cshtml meta.structure.razor.codeblock source.cs meta.statement.if.razor meta.structure.razor.csharp.codeblock comment.line.double-slash.cs punctuation.whitespace.comment.leading.cs", "r": { - "dark_plus": "default: #D4D4D4", - "light_plus": "default: #000000", - "dark_vs": "default: #D4D4D4", - "light_vs": "default: #000000", - "hc_black": "default: #FFFFFF", - "dark_modern": "default: #CCCCCC", - "hc_light": "default: #292929", - "light_modern": "default: #3B3B3B" + "dark_plus": "comment: #6A9955", + "light_plus": "comment: #008000", + "dark_vs": "comment: #6A9955", + "light_vs": "comment: #008000", + "hc_black": "comment: #7CA668", + "dark_modern": "comment: #6A9955", + "hc_light": "comment: #515151", + "light_modern": "comment: #008000" } }, { diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_p6.json b/extensions/vscode-colorize-tests/test/colorize-results/test_p6.json new file mode 100644 index 00000000000..c4c2106a171 --- /dev/null +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_p6.json @@ -0,0 +1,1836 @@ +[ + { + "c": "#", + "t": "source.perl.6 comment.line.number-sign.perl punctuation.definition.comment.perl", + "r": { + "dark_plus": "comment: #6A9955", + "light_plus": "comment: #008000", + "dark_vs": "comment: #6A9955", + "light_vs": "comment: #008000", + "hc_black": "comment: #7CA668", + "dark_modern": "comment: #6A9955", + "hc_light": "comment: #515151", + "light_modern": "comment: #008000" + } + }, + { + "c": " Example taken from https://en.wikipedia.org/wiki/Raku_(programming_language)", + "t": "source.perl.6 comment.line.number-sign.perl", + "r": { + "dark_plus": "comment: #6A9955", + "light_plus": "comment: #008000", + "dark_vs": "comment: #6A9955", + "light_vs": "comment: #008000", + "hc_black": "comment: #7CA668", + "dark_modern": "comment: #6A9955", + "hc_light": "comment: #515151", + "light_modern": "comment: #008000" + } + }, + { + "c": "class", + "t": "source.perl.6 meta.class.perl.6 storage.type.class.perl.6", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": " ", + "t": "source.perl.6 meta.class.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "Point", + "t": "source.perl.6 meta.class.perl.6 entity.name.type.class.perl.6", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": " ", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "is", + "t": "source.perl.6 storage.modifier.type.constraints.perl", + "r": { + "dark_plus": "storage.modifier: #569CD6", + "light_plus": "storage.modifier: #0000FF", + "dark_vs": "storage.modifier: #569CD6", + "light_vs": "storage.modifier: #0000FF", + "hc_black": "storage.modifier: #569CD6", + "dark_modern": "storage.modifier: #569CD6", + "hc_light": "storage.modifier: #0F4A85", + "light_modern": "storage.modifier: #0000FF" + } + }, + { + "c": " ", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "rw", + "t": "source.perl.6 storage.modifier.perl", + "r": { + "dark_plus": "storage.modifier: #569CD6", + "light_plus": "storage.modifier: #0000FF", + "dark_vs": "storage.modifier: #569CD6", + "light_vs": "storage.modifier: #0000FF", + "hc_black": "storage.modifier: #569CD6", + "dark_modern": "storage.modifier: #569CD6", + "hc_light": "storage.modifier: #0F4A85", + "light_modern": "storage.modifier: #0000FF" + } + }, + { + "c": " {", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "has", + "t": "source.perl.6 storage.type.variable.perl", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": " $.", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "x", + "t": "source.perl.6 keyword.operator.perl", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": ";", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "has", + "t": "source.perl.6 storage.type.variable.perl", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": " $.y;", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "method", + "t": "source.perl.6 storage.type.declare.routine.perl", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": " distance( Point ", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "$p", + "t": "source.perl.6 variable.other.identifier.perl.6", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": " ) {", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "sqrt", + "t": "source.perl.6 support.function.perl", + "r": { + "dark_plus": "support.function: #DCDCAA", + "light_plus": "support.function: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.function: #DCDCAA", + "dark_modern": "support.function: #DCDCAA", + "hc_light": "support.function: #5E2CBC", + "light_modern": "support.function: #795E26" + } + }, + { + "c": "((", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "$!x", + "t": "source.perl.6 variable.other.identifier.perl.6", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": " - ", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "$p", + "t": "source.perl.6 variable.other.identifier.perl.6", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": ".", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "x", + "t": "source.perl.6 keyword.operator.perl", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": ") ** 2 + (", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "$!y", + "t": "source.perl.6 variable.other.identifier.perl.6", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": " - ", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "$p", + "t": "source.perl.6 variable.other.identifier.perl.6", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": ".y) ** 2)", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " }", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "method", + "t": "source.perl.6 storage.type.declare.routine.perl", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": " distance-", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "to", + "t": "source.perl.6 support.function.perl", + "r": { + "dark_plus": "support.function: #DCDCAA", + "light_plus": "support.function: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.function: #DCDCAA", + "dark_modern": "support.function: #DCDCAA", + "hc_light": "support.function: #5E2CBC", + "light_modern": "support.function: #795E26" + } + }, + { + "c": "-center {", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "self", + "t": "source.perl.6 variable.language.perl", + "r": { + "dark_plus": "variable.language: #569CD6", + "light_plus": "variable.language: #0000FF", + "dark_vs": "variable.language: #569CD6", + "light_vs": "variable.language: #0000FF", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable.language: #569CD6", + "hc_light": "variable.language: #0F4A85", + "light_modern": "variable.language: #0000FF" + } + }, + { + "c": ".distance: Point.", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "new", + "t": "source.perl.6 support.function.perl", + "r": { + "dark_plus": "support.function: #DCDCAA", + "light_plus": "support.function: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.function: #DCDCAA", + "dark_modern": "support.function: #DCDCAA", + "hc_light": "support.function: #5E2CBC", + "light_modern": "support.function: #795E26" + } + }, + { + "c": "(", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "x", + "t": "source.perl.6 keyword.operator.perl", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " => 0, y => 0)", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " }", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "}", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "my", + "t": "source.perl.6 storage.type.variable.perl", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": " ", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "$point", + "t": "source.perl.6 variable.other.identifier.perl.6", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": " = Point.", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "new", + "t": "source.perl.6 support.function.perl", + "r": { + "dark_plus": "support.function: #DCDCAA", + "light_plus": "support.function: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.function: #DCDCAA", + "dark_modern": "support.function: #DCDCAA", + "hc_light": "support.function: #5E2CBC", + "light_modern": "support.function: #795E26" + } + }, + { + "c": "( ", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "x", + "t": "source.perl.6 keyword.operator.perl", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " => 1.2, y => -3.7 );", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "say", + "t": "source.perl.6 support.function.perl", + "r": { + "dark_plus": "support.function: #DCDCAA", + "light_plus": "support.function: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.function: #DCDCAA", + "dark_modern": "support.function: #DCDCAA", + "hc_light": "support.function: #5E2CBC", + "light_modern": "support.function: #795E26" + } + }, + { + "c": " ", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "\"", + "t": "source.perl.6 string.quoted.double.perl punctuation.definition.string.begin.perl", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, + { + "c": "Point's location: (", + "t": "source.perl.6 string.quoted.double.perl", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, + { + "c": "\"", + "t": "source.perl.6 string.quoted.double.perl punctuation.definition.string.end.perl", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, + { + "c": ", ", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "$point", + "t": "source.perl.6 variable.other.identifier.perl.6", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": ".", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "x", + "t": "source.perl.6 keyword.operator.perl", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": ", ", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "'", + "t": "source.perl.6 string.quoted.single.perl punctuation.definition.string.begin.perl", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, + { + "c": ", ", + "t": "source.perl.6 string.quoted.single.perl", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, + { + "c": "'", + "t": "source.perl.6 string.quoted.single.perl punctuation.definition.string.end.perl", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, + { + "c": ", ", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "$point", + "t": "source.perl.6 variable.other.identifier.perl.6", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": ".y, ", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "'", + "t": "source.perl.6 string.quoted.single.perl punctuation.definition.string.begin.perl", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, + { + "c": ")", + "t": "source.perl.6 string.quoted.single.perl", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, + { + "c": "'", + "t": "source.perl.6 string.quoted.single.perl punctuation.definition.string.end.perl", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, + { + "c": ";", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "#", + "t": "source.perl.6 comment.line.number-sign.perl punctuation.definition.comment.perl", + "r": { + "dark_plus": "comment: #6A9955", + "light_plus": "comment: #008000", + "dark_vs": "comment: #6A9955", + "light_vs": "comment: #008000", + "hc_black": "comment: #7CA668", + "dark_modern": "comment: #6A9955", + "hc_light": "comment: #515151", + "light_modern": "comment: #008000" + } + }, + { + "c": " OUTPUT: Point's location: (1.2, -3.7)", + "t": "source.perl.6 comment.line.number-sign.perl", + "r": { + "dark_plus": "comment: #6A9955", + "light_plus": "comment: #008000", + "dark_vs": "comment: #6A9955", + "light_vs": "comment: #008000", + "hc_black": "comment: #7CA668", + "dark_modern": "comment: #6A9955", + "hc_light": "comment: #515151", + "light_modern": "comment: #008000" + } + }, + { + "c": "#", + "t": "source.perl.6 comment.line.number-sign.perl punctuation.definition.comment.perl", + "r": { + "dark_plus": "comment: #6A9955", + "light_plus": "comment: #008000", + "dark_vs": "comment: #6A9955", + "light_vs": "comment: #008000", + "hc_black": "comment: #7CA668", + "dark_modern": "comment: #6A9955", + "hc_light": "comment: #515151", + "light_modern": "comment: #008000" + } + }, + { + "c": " Changing x and y (note methods \"x\" and \"y\" used as lvalues):", + "t": "source.perl.6 comment.line.number-sign.perl", + "r": { + "dark_plus": "comment: #6A9955", + "light_plus": "comment: #008000", + "dark_vs": "comment: #6A9955", + "light_vs": "comment: #008000", + "hc_black": "comment: #7CA668", + "dark_modern": "comment: #6A9955", + "hc_light": "comment: #515151", + "light_modern": "comment: #008000" + } + }, + { + "c": "$point", + "t": "source.perl.6 variable.other.identifier.perl.6", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": ".", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "x", + "t": "source.perl.6 keyword.operator.perl", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " = 3;", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "$point", + "t": "source.perl.6 variable.other.identifier.perl.6", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": ".y = 4;", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "say", + "t": "source.perl.6 support.function.perl", + "r": { + "dark_plus": "support.function: #DCDCAA", + "light_plus": "support.function: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.function: #DCDCAA", + "dark_modern": "support.function: #DCDCAA", + "hc_light": "support.function: #5E2CBC", + "light_modern": "support.function: #795E26" + } + }, + { + "c": " ", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "\"", + "t": "source.perl.6 string.quoted.double.perl punctuation.definition.string.begin.perl", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, + { + "c": "Point's location: (", + "t": "source.perl.6 string.quoted.double.perl", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, + { + "c": "\"", + "t": "source.perl.6 string.quoted.double.perl punctuation.definition.string.end.perl", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, + { + "c": ", ", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "$point", + "t": "source.perl.6 variable.other.identifier.perl.6", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": ".", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "x", + "t": "source.perl.6 keyword.operator.perl", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": ", ", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "'", + "t": "source.perl.6 string.quoted.single.perl punctuation.definition.string.begin.perl", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, + { + "c": ", ", + "t": "source.perl.6 string.quoted.single.perl", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, + { + "c": "'", + "t": "source.perl.6 string.quoted.single.perl punctuation.definition.string.end.perl", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, + { + "c": ", ", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "$point", + "t": "source.perl.6 variable.other.identifier.perl.6", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": ".y, ", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "'", + "t": "source.perl.6 string.quoted.single.perl punctuation.definition.string.begin.perl", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, + { + "c": ")", + "t": "source.perl.6 string.quoted.single.perl", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, + { + "c": "'", + "t": "source.perl.6 string.quoted.single.perl punctuation.definition.string.end.perl", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, + { + "c": ";", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "#", + "t": "source.perl.6 comment.line.number-sign.perl punctuation.definition.comment.perl", + "r": { + "dark_plus": "comment: #6A9955", + "light_plus": "comment: #008000", + "dark_vs": "comment: #6A9955", + "light_vs": "comment: #008000", + "hc_black": "comment: #7CA668", + "dark_modern": "comment: #6A9955", + "hc_light": "comment: #515151", + "light_modern": "comment: #008000" + } + }, + { + "c": " OUTPUT: Point's location: (3, 4)", + "t": "source.perl.6 comment.line.number-sign.perl", + "r": { + "dark_plus": "comment: #6A9955", + "light_plus": "comment: #008000", + "dark_vs": "comment: #6A9955", + "light_vs": "comment: #008000", + "hc_black": "comment: #7CA668", + "dark_modern": "comment: #6A9955", + "hc_light": "comment: #515151", + "light_modern": "comment: #008000" + } + }, + { + "c": "my", + "t": "source.perl.6 storage.type.variable.perl", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": " ", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "$other-point", + "t": "source.perl.6 variable.other.identifier.perl.6", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": " = Point.", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "new", + "t": "source.perl.6 support.function.perl", + "r": { + "dark_plus": "support.function: #DCDCAA", + "light_plus": "support.function: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.function: #DCDCAA", + "dark_modern": "support.function: #DCDCAA", + "hc_light": "support.function: #5E2CBC", + "light_modern": "support.function: #795E26" + } + }, + { + "c": "(", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "x", + "t": "source.perl.6 keyword.operator.perl", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " => -5, y => 10);", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "$point", + "t": "source.perl.6 variable.other.identifier.perl.6", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": ".distance(", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "$other-point", + "t": "source.perl.6 variable.other.identifier.perl.6", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": "); ", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "#", + "t": "source.perl.6 comment.line.number-sign.perl punctuation.definition.comment.perl", + "r": { + "dark_plus": "comment: #6A9955", + "light_plus": "comment: #008000", + "dark_vs": "comment: #6A9955", + "light_vs": "comment: #008000", + "hc_black": "comment: #7CA668", + "dark_modern": "comment: #6A9955", + "hc_light": "comment: #515151", + "light_modern": "comment: #008000" + } + }, + { + "c": "=> 10", + "t": "source.perl.6 comment.line.number-sign.perl", + "r": { + "dark_plus": "comment: #6A9955", + "light_plus": "comment: #008000", + "dark_vs": "comment: #6A9955", + "light_vs": "comment: #008000", + "hc_black": "comment: #7CA668", + "dark_modern": "comment: #6A9955", + "hc_light": "comment: #515151", + "light_modern": "comment: #008000" + } + }, + { + "c": "$point", + "t": "source.perl.6 variable.other.identifier.perl.6", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": ".distance-", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "to", + "t": "source.perl.6 support.function.perl", + "r": { + "dark_plus": "support.function: #DCDCAA", + "light_plus": "support.function: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.function: #DCDCAA", + "dark_modern": "support.function: #DCDCAA", + "hc_light": "support.function: #5E2CBC", + "light_modern": "support.function: #795E26" + } + }, + { + "c": "-center; ", + "t": "source.perl.6", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "#", + "t": "source.perl.6 comment.line.number-sign.perl punctuation.definition.comment.perl", + "r": { + "dark_plus": "comment: #6A9955", + "light_plus": "comment: #008000", + "dark_vs": "comment: #6A9955", + "light_vs": "comment: #008000", + "hc_black": "comment: #7CA668", + "dark_modern": "comment: #6A9955", + "hc_light": "comment: #515151", + "light_modern": "comment: #008000" + } + }, + { + "c": "=> 5", + "t": "source.perl.6 comment.line.number-sign.perl", + "r": { + "dark_plus": "comment: #6A9955", + "light_plus": "comment: #008000", + "dark_vs": "comment: #6A9955", + "light_vs": "comment: #008000", + "hc_black": "comment: #7CA668", + "dark_modern": "comment: #6A9955", + "hc_light": "comment: #515151", + "light_modern": "comment: #008000" + } + } +] \ No newline at end of file diff --git a/extensions/vscode-colorize-tests/yarn.lock b/extensions/vscode-colorize-tests/yarn.lock index 4c166d0a3c8..a7a6fa446ca 100644 --- a/extensions/vscode-colorize-tests/yarn.lock +++ b/extensions/vscode-colorize-tests/yarn.lock @@ -7,7 +7,7 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== -jsonc-parser@2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.2.1.tgz#db73cd59d78cce28723199466b2a03d1be1df2bc" - integrity sha512-o6/yDBYccGvTz1+QFevz6l6OBZ2+fMVu2JZ9CIhzsYRX4mjaK5IyX9eldUdCmga16zlgQxyrj5pt9kzuj2C02w== +jsonc-parser@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz#31ff3f4c2b9793f89c67212627c51c6394f88e76" + integrity sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w== diff --git a/package.json b/package.json index bf70dc18097..9608a109672 100644 --- a/package.json +++ b/package.json @@ -94,14 +94,14 @@ "vscode-oniguruma": "1.7.0", "vscode-regexpp": "^3.1.0", "vscode-textmate": "9.0.0", - "xterm": "5.3.0-beta.61", - "xterm-addon-canvas": "0.5.0-beta.22", - "xterm-addon-image": "0.6.0-beta.14", - "xterm-addon-search": "0.13.0-beta.20", - "xterm-addon-serialize": "0.11.0-beta.20", - "xterm-addon-unicode11": "0.6.0-beta.12", - "xterm-addon-webgl": "0.16.0-beta.30", - "xterm-headless": "5.3.0-beta.61", + "xterm": "5.4.0-beta.17", + "xterm-addon-canvas": "0.6.0-beta.16", + "xterm-addon-image": "0.6.0-beta.21", + "xterm-addon-search": "0.14.0-beta.15", + "xterm-addon-serialize": "0.12.0-beta.15", + "xterm-addon-unicode11": "0.7.0-beta.15", + "xterm-addon-webgl": "0.17.0-beta.15", + "xterm-headless": "5.4.0-beta.17", "yauzl": "^2.9.2", "yazl": "^2.4.3" }, @@ -210,7 +210,7 @@ "ts-loader": "^9.4.2", "ts-node": "^10.9.1", "tsec": "0.2.7", - "typescript": "^5.3.0-dev.20230905", + "typescript": "^5.3.0-dev.20230911", "typescript-formatter": "7.1.0", "underscore": "^1.12.1", "util": "^0.12.4", diff --git a/remote/package.json b/remote/package.json index f064462fc27..c3bf8bddc07 100644 --- a/remote/package.json +++ b/remote/package.json @@ -26,14 +26,14 @@ "vscode-oniguruma": "1.7.0", "vscode-regexpp": "^3.1.0", "vscode-textmate": "9.0.0", - "xterm": "5.3.0-beta.61", - "xterm-addon-canvas": "0.5.0-beta.22", - "xterm-addon-image": "0.6.0-beta.14", - "xterm-addon-search": "0.13.0-beta.20", - "xterm-addon-serialize": "0.11.0-beta.20", - "xterm-addon-unicode11": "0.6.0-beta.12", - "xterm-addon-webgl": "0.16.0-beta.30", - "xterm-headless": "5.3.0-beta.61", + "xterm": "5.4.0-beta.17", + "xterm-addon-canvas": "0.6.0-beta.16", + "xterm-addon-image": "0.6.0-beta.21", + "xterm-addon-search": "0.14.0-beta.15", + "xterm-addon-serialize": "0.12.0-beta.15", + "xterm-addon-unicode11": "0.7.0-beta.15", + "xterm-addon-webgl": "0.17.0-beta.15", + "xterm-headless": "5.4.0-beta.17", "yauzl": "^2.9.2", "yazl": "^2.4.3" } diff --git a/remote/web/package.json b/remote/web/package.json index 6075a563e34..da3d76208b2 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -11,11 +11,11 @@ "tas-client-umd": "0.1.8", "vscode-oniguruma": "1.7.0", "vscode-textmate": "9.0.0", - "xterm": "5.3.0-beta.61", - "xterm-addon-canvas": "0.5.0-beta.22", - "xterm-addon-image": "0.6.0-beta.14", - "xterm-addon-search": "0.13.0-beta.20", - "xterm-addon-unicode11": "0.6.0-beta.12", - "xterm-addon-webgl": "0.16.0-beta.30" + "xterm": "5.4.0-beta.17", + "xterm-addon-canvas": "0.6.0-beta.16", + "xterm-addon-image": "0.6.0-beta.21", + "xterm-addon-search": "0.14.0-beta.15", + "xterm-addon-unicode11": "0.7.0-beta.15", + "xterm-addon-webgl": "0.17.0-beta.15" } } diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index b56fac51a45..73f51c00124 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -68,32 +68,32 @@ vscode-textmate@9.0.0: resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-9.0.0.tgz#313c6c8792b0507aef35aeb81b6b370b37c44d6c" integrity sha512-Cl65diFGxz7gpwbav10HqiY/eVYTO1sjQpmRmV991Bj7wAoOAjGQ97PpQcXorDE2Uc4hnGWLY17xme+5t6MlSg== -xterm-addon-canvas@0.5.0-beta.22: - version "0.5.0-beta.22" - resolved "https://registry.yarnpkg.com/xterm-addon-canvas/-/xterm-addon-canvas-0.5.0-beta.22.tgz#513f0c2b7cf96073f47627b27e8965c1b1a22431" - integrity sha512-9F6ZI0DMRgffVfHkLkDwl5n8VscvCaV10tWI3skXOX7Y7Aws6OEeglkOPoU3IllofCU792kHKM4pPoToUxTltg== +xterm-addon-canvas@0.6.0-beta.16: + version "0.6.0-beta.16" + resolved "https://registry.yarnpkg.com/xterm-addon-canvas/-/xterm-addon-canvas-0.6.0-beta.16.tgz#6d6a475824c3c0129f3cb38eaea69cf12386cc31" + integrity sha512-uh90h+uozwrZbc/yMhbRnKIY7J34CTse6njibajdeK+0hQvj09HnBXgkvALImR/sEwyIz8SVtSL+OOZTlmAHIQ== -xterm-addon-image@0.6.0-beta.14: - version "0.6.0-beta.14" - resolved "https://registry.yarnpkg.com/xterm-addon-image/-/xterm-addon-image-0.6.0-beta.14.tgz#75fc3f824123183a4bbb5306e22f8b2c6966b0a6" - integrity sha512-D5Gh5JTKhHaPt1KwQNf6diF37KA4eToJw3XId1wy62tWmSqfq+QflhOGTfd+SnSQYCktU05ETzM+0tncIU62pQ== +xterm-addon-image@0.6.0-beta.21: + version "0.6.0-beta.21" + resolved "https://registry.yarnpkg.com/xterm-addon-image/-/xterm-addon-image-0.6.0-beta.21.tgz#e3708bc504c56a23ff31f12a2eeb335331a92aac" + integrity sha512-8/PTaXVPa4kQ0xzVeuZZk10OpbZBj2cgfwhM2B0ChSPvwrk0lX+ksnXdtDKH3tg+JYvo7fIhNXtkr4NwWt7VJQ== -xterm-addon-search@0.13.0-beta.20: - version "0.13.0-beta.20" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.13.0-beta.20.tgz#8ddd0513e2a70fcefa325722100d2e1bfaf3b9cb" - integrity sha512-wrx6187cJ1UenGL6ZeYv3jFvRPhhENTfbC+Hv1Fnww8LmsKhcj+0+Pm6yInNjX/9hNVsNzdqKyqNeEMoykyoyA== +xterm-addon-search@0.14.0-beta.15: + version "0.14.0-beta.15" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.14.0-beta.15.tgz#fcea611a04a8f4fd5dd3ec5e3403cce9cde72f0a" + integrity sha512-wPk3FzOFIeEAgVMjNL6CHoAfPcmk3DWwf28AtICfJ512JVJ3d0o9mUEh853m08/eaJJikAMXMEgTGRgjNtZU3Q== -xterm-addon-unicode11@0.6.0-beta.12: - version "0.6.0-beta.12" - resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.6.0-beta.12.tgz#ac6df9d635325dc692e4c602e74a2fc27a09405c" - integrity sha512-9wWWf/5nFafYgq0pn9EgAWnXaXGleVxfjNOqavpLRYFv0nw42QbaYyGvnGcxyYHM5Aqx/8rYE/DDVWZBqQZdYA== +xterm-addon-unicode11@0.7.0-beta.15: + version "0.7.0-beta.15" + resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.7.0-beta.15.tgz#07359424a862aedc456f35f95126739cd4fbe7de" + integrity sha512-CdPuahtDiacsBO618XTNc+z3diWPVzvpVlk0NcIsUpcdsF+44rgRBFaD7mT4tfAFU1w1ibTzTfOakrFlYZuM0A== -xterm-addon-webgl@0.16.0-beta.30: - version "0.16.0-beta.30" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.16.0-beta.30.tgz#820d5c65f868b14ec4177bfb8a294931a53616bf" - integrity sha512-39qPHPFmNENxcHf8/CzGHS6wzKMMegoRkHB1+scqtBhSxFaD8tX5Ye33HZIEdQ9nXe9xtr4FWVp77T+n9hdrew== +xterm-addon-webgl@0.17.0-beta.15: + version "0.17.0-beta.15" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.17.0-beta.15.tgz#53e15a170e4e9d8ddafb8971b2e63b4c596b9744" + integrity sha512-3JJ8KumPzFut1yloNhIrvObBwqp8kbqBI/up77MIyGE/6WiGhgecUFPmT8rYUCv4g/GBXoMan755yOEUNkKtcg== -xterm@5.3.0-beta.61: - version "5.3.0-beta.61" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.3.0-beta.61.tgz#a6c27d90a5314da51d80deeb32f3bd77f1e1c8f6" - integrity sha512-rJHpCc48GSpHnu0SSERynQ80D5ikvFVsqhv6JdmeONTrnAFRr134OglJRIpbi2YK8UPbV6F6Dfqm/AQh+9GZzA== +xterm@5.4.0-beta.17: + version "5.4.0-beta.17" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.4.0-beta.17.tgz#7e7841b09339670c5ada967b73a1416d1b3dc9f7" + integrity sha512-hTSJHkyH/m26nQJt1oNI1KJU18JVJpOy41pP9WRRFQx1KoHhYq4HMRl7LO42SJt6Vs0mig0UYkbEnRWfT4FCww== diff --git a/remote/yarn.lock b/remote/yarn.lock index e2a07ab9843..cbd6bbbb384 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -667,45 +667,45 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -xterm-addon-canvas@0.5.0-beta.22: - version "0.5.0-beta.22" - resolved "https://registry.yarnpkg.com/xterm-addon-canvas/-/xterm-addon-canvas-0.5.0-beta.22.tgz#513f0c2b7cf96073f47627b27e8965c1b1a22431" - integrity sha512-9F6ZI0DMRgffVfHkLkDwl5n8VscvCaV10tWI3skXOX7Y7Aws6OEeglkOPoU3IllofCU792kHKM4pPoToUxTltg== +xterm-addon-canvas@0.6.0-beta.16: + version "0.6.0-beta.16" + resolved "https://registry.yarnpkg.com/xterm-addon-canvas/-/xterm-addon-canvas-0.6.0-beta.16.tgz#6d6a475824c3c0129f3cb38eaea69cf12386cc31" + integrity sha512-uh90h+uozwrZbc/yMhbRnKIY7J34CTse6njibajdeK+0hQvj09HnBXgkvALImR/sEwyIz8SVtSL+OOZTlmAHIQ== -xterm-addon-image@0.6.0-beta.14: - version "0.6.0-beta.14" - resolved "https://registry.yarnpkg.com/xterm-addon-image/-/xterm-addon-image-0.6.0-beta.14.tgz#75fc3f824123183a4bbb5306e22f8b2c6966b0a6" - integrity sha512-D5Gh5JTKhHaPt1KwQNf6diF37KA4eToJw3XId1wy62tWmSqfq+QflhOGTfd+SnSQYCktU05ETzM+0tncIU62pQ== +xterm-addon-image@0.6.0-beta.21: + version "0.6.0-beta.21" + resolved "https://registry.yarnpkg.com/xterm-addon-image/-/xterm-addon-image-0.6.0-beta.21.tgz#e3708bc504c56a23ff31f12a2eeb335331a92aac" + integrity sha512-8/PTaXVPa4kQ0xzVeuZZk10OpbZBj2cgfwhM2B0ChSPvwrk0lX+ksnXdtDKH3tg+JYvo7fIhNXtkr4NwWt7VJQ== -xterm-addon-search@0.13.0-beta.20: - version "0.13.0-beta.20" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.13.0-beta.20.tgz#8ddd0513e2a70fcefa325722100d2e1bfaf3b9cb" - integrity sha512-wrx6187cJ1UenGL6ZeYv3jFvRPhhENTfbC+Hv1Fnww8LmsKhcj+0+Pm6yInNjX/9hNVsNzdqKyqNeEMoykyoyA== +xterm-addon-search@0.14.0-beta.15: + version "0.14.0-beta.15" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.14.0-beta.15.tgz#fcea611a04a8f4fd5dd3ec5e3403cce9cde72f0a" + integrity sha512-wPk3FzOFIeEAgVMjNL6CHoAfPcmk3DWwf28AtICfJ512JVJ3d0o9mUEh853m08/eaJJikAMXMEgTGRgjNtZU3Q== -xterm-addon-serialize@0.11.0-beta.20: - version "0.11.0-beta.20" - resolved "https://registry.yarnpkg.com/xterm-addon-serialize/-/xterm-addon-serialize-0.11.0-beta.20.tgz#e879b34d214761403f1081833f9221c6903bf0c3" - integrity sha512-OXnC1SATaz7kEFjFWhyv9MJaXi8yHdPjazpGLNi11h33CRTKtCQiqqPBHU87dztnXmpEX6Jw0/jr3zlyXuAmnw== +xterm-addon-serialize@0.12.0-beta.15: + version "0.12.0-beta.15" + resolved "https://registry.yarnpkg.com/xterm-addon-serialize/-/xterm-addon-serialize-0.12.0-beta.15.tgz#8e210d566c03b616823317a31dbce474a728b3c9" + integrity sha512-4iF9pQ/q6831ayADPlngAM0aa/mhzvZ4XEd4UNJIgW7mpgoSTvnbh6d6lb37pNkpdDFIRLJv6rlqDjlh6EkoJg== -xterm-addon-unicode11@0.6.0-beta.12: - version "0.6.0-beta.12" - resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.6.0-beta.12.tgz#ac6df9d635325dc692e4c602e74a2fc27a09405c" - integrity sha512-9wWWf/5nFafYgq0pn9EgAWnXaXGleVxfjNOqavpLRYFv0nw42QbaYyGvnGcxyYHM5Aqx/8rYE/DDVWZBqQZdYA== +xterm-addon-unicode11@0.7.0-beta.15: + version "0.7.0-beta.15" + resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.7.0-beta.15.tgz#07359424a862aedc456f35f95126739cd4fbe7de" + integrity sha512-CdPuahtDiacsBO618XTNc+z3diWPVzvpVlk0NcIsUpcdsF+44rgRBFaD7mT4tfAFU1w1ibTzTfOakrFlYZuM0A== -xterm-addon-webgl@0.16.0-beta.30: - version "0.16.0-beta.30" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.16.0-beta.30.tgz#820d5c65f868b14ec4177bfb8a294931a53616bf" - integrity sha512-39qPHPFmNENxcHf8/CzGHS6wzKMMegoRkHB1+scqtBhSxFaD8tX5Ye33HZIEdQ9nXe9xtr4FWVp77T+n9hdrew== +xterm-addon-webgl@0.17.0-beta.15: + version "0.17.0-beta.15" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.17.0-beta.15.tgz#53e15a170e4e9d8ddafb8971b2e63b4c596b9744" + integrity sha512-3JJ8KumPzFut1yloNhIrvObBwqp8kbqBI/up77MIyGE/6WiGhgecUFPmT8rYUCv4g/GBXoMan755yOEUNkKtcg== -xterm-headless@5.3.0-beta.61: - version "5.3.0-beta.61" - resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-5.3.0-beta.61.tgz#28654550cb572709b99ea3eb8672d4568ae141c9" - integrity sha512-yfkbPLUtKjE4K7DsZ204A1BuOKpu6Usqi6rIYWT4XRMi+LjnkTbBjGr2BSjyJ3Gmtm+cSgBD0SvRN+V3xNxbxA== +xterm-headless@5.4.0-beta.17: + version "5.4.0-beta.17" + resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-5.4.0-beta.17.tgz#cb779eba8da66ecad6f4be41af649453bb3260e9" + integrity sha512-ax2asr5QS4EnJAmRnKDDnAqESst+r2G/MckaYdxTDgX0pc997TU4B/JQ6c5BhxVLQvRwmotTm11/n909HiHadQ== -xterm@5.3.0-beta.61: - version "5.3.0-beta.61" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.3.0-beta.61.tgz#a6c27d90a5314da51d80deeb32f3bd77f1e1c8f6" - integrity sha512-rJHpCc48GSpHnu0SSERynQ80D5ikvFVsqhv6JdmeONTrnAFRr134OglJRIpbi2YK8UPbV6F6Dfqm/AQh+9GZzA== +xterm@5.4.0-beta.17: + version "5.4.0-beta.17" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.4.0-beta.17.tgz#7e7841b09339670c5ada967b73a1416d1b3dc9f7" + integrity sha512-hTSJHkyH/m26nQJt1oNI1KJU18JVJpOy41pP9WRRFQx1KoHhYq4HMRl7LO42SJt6Vs0mig0UYkbEnRWfT4FCww== yallist@^4.0.0: version "4.0.0" diff --git a/scripts/test-integration.bat b/scripts/test-integration.bat index 55d7202968f..fb9498937f6 100644 --- a/scripts/test-integration.bat +++ b/scripts/test-integration.bat @@ -35,7 +35,7 @@ if %errorlevel% neq 0 exit /b %errorlevel% :: Tests in the extension host -set API_TESTS_EXTRA_ARGS=--disable-telemetry --skip-welcome --skip-release-notes --crash-reporter-directory=%VSCODECRASHDIR% --logsPath=%VSCODELOGSDIR% --no-cached-data --disable-updates --disable-keytar --disable-extensions --disable-workspace-trust --user-data-dir=%VSCODEUSERDATADIR% +set API_TESTS_EXTRA_ARGS=--disable-telemetry --skip-welcome --skip-release-notes --crash-reporter-directory=%VSCODECRASHDIR% --logsPath=%VSCODELOGSDIR% --no-cached-data --disable-updates --use-inmemory-secretstorage --disable-extensions --disable-workspace-trust --user-data-dir=%VSCODEUSERDATADIR% echo. echo ### API tests (folder) diff --git a/scripts/test-integration.sh b/scripts/test-integration.sh index b6f3ec01538..85e4f80dea6 100755 --- a/scripts/test-integration.sh +++ b/scripts/test-integration.sh @@ -44,7 +44,7 @@ echo # Tests in the extension host -API_TESTS_EXTRA_ARGS="--disable-telemetry --skip-welcome --skip-release-notes --crash-reporter-directory=$VSCODECRASHDIR --logsPath=$VSCODELOGSDIR --no-cached-data --disable-updates --disable-keytar --disable-extensions --disable-workspace-trust --user-data-dir=$VSCODEUSERDATADIR" +API_TESTS_EXTRA_ARGS="--disable-telemetry --skip-welcome --skip-release-notes --crash-reporter-directory=$VSCODECRASHDIR --logsPath=$VSCODELOGSDIR --no-cached-data --disable-updates --use-inmemory-secretstorage --disable-extensions --disable-workspace-trust --user-data-dir=$VSCODEUSERDATADIR" if [ -z "$INTEGRATION_TEST_APP_NAME" ]; then kill_app() { true; } diff --git a/scripts/test-remote-integration.bat b/scripts/test-remote-integration.bat index e0fda5d1c4e..c583f906503 100644 --- a/scripts/test-remote-integration.bat +++ b/scripts/test-remote-integration.bat @@ -55,7 +55,7 @@ echo Storing log files into '%VSCODELOGSDIR%' :: Tests in the extension host -set API_TESTS_EXTRA_ARGS=--disable-telemetry --skip-welcome --skip-release-notes --crash-reporter-directory=%VSCODECRASHDIR% --logsPath=%VSCODELOGSDIR% --no-cached-data --disable-updates --disable-keytar --disable-inspect --disable-workspace-trust --user-data-dir=%VSCODEUSERDATADIR% +set API_TESTS_EXTRA_ARGS=--disable-telemetry --skip-welcome --skip-release-notes --crash-reporter-directory=%VSCODECRASHDIR% --logsPath=%VSCODELOGSDIR% --no-cached-data --disable-updates --use-inmemory-secretstorage --disable-inspect --disable-workspace-trust --user-data-dir=%VSCODEUSERDATADIR% echo. echo ### API tests (folder) diff --git a/scripts/test-remote-integration.sh b/scripts/test-remote-integration.sh index 8163e796645..d3e9f2a5265 100755 --- a/scripts/test-remote-integration.sh +++ b/scripts/test-remote-integration.sh @@ -68,7 +68,7 @@ else kill_app() { killall $INTEGRATION_TEST_APP_NAME || true; } fi -API_TESTS_EXTRA_ARGS="--disable-telemetry --skip-welcome --skip-release-notes --crash-reporter-directory=$VSCODECRASHDIR --logsPath=$VSCODELOGSDIR --no-cached-data --disable-updates --disable-keytar --disable-workspace-trust --user-data-dir=$VSCODEUSERDATADIR" +API_TESTS_EXTRA_ARGS="--disable-telemetry --skip-welcome --skip-release-notes --crash-reporter-directory=$VSCODECRASHDIR --logsPath=$VSCODELOGSDIR --no-cached-data --disable-updates --use-inmemory-secretstorage --disable-workspace-trust --user-data-dir=$VSCODEUSERDATADIR" echo "Storing crash reports into '$VSCODECRASHDIR'." echo "Storing log files into '$VSCODELOGSDIR'." diff --git a/src/vs/base/browser/iframe.ts b/src/vs/base/browser/iframe.ts index 40ca2f9f507..2a32decc492 100644 --- a/src/vs/base/browser/iframe.ts +++ b/src/vs/base/browser/iframe.ts @@ -123,3 +123,26 @@ export class IframeUtils { }; } } + +/** + * Returns a sha-256 composed of `parentOrigin` and `salt` converted to base 32 + */ +export async function parentOriginHash(parentOrigin: string, salt: string): Promise { + // This same code is also inlined at `src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html` + if (!crypto.subtle) { + throw new Error(`'crypto.subtle' is not available so webviews will not work. This is likely because the editor is not running in a secure context (https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts).`); + } + + const strData = JSON.stringify({ parentOrigin, salt }); + const encoder = new TextEncoder(); + const arrData = encoder.encode(strData); + const hash = await crypto.subtle.digest('sha-256', arrData); + return sha256AsBase32(hash); +} + +function sha256AsBase32(bytes: ArrayBuffer): string { + const array = Array.from(new Uint8Array(bytes)); + const hexArray = array.map(b => b.toString(16).padStart(2, '0')).join(''); + // sha256 has 256 bits, so we need at most ceil(lg(2^256-1)/lg(32)) = 52 chars to represent it in base 32 + return BigInt(`0x${hexArray}`).toString(32).padStart(52, '0'); +} diff --git a/src/vs/base/browser/touch.ts b/src/vs/base/browser/touch.ts index 5ef1672c3f1..0fe7149d05a 100644 --- a/src/vs/base/browser/touch.ts +++ b/src/vs/base/browser/touch.ts @@ -6,7 +6,7 @@ import * as DomUtils from 'vs/base/browser/dom'; import * as arrays from 'vs/base/common/arrays'; import { memoize } from 'vs/base/common/decorators'; -import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, markAsSingleton, toDisposable } from 'vs/base/common/lifecycle'; import { LinkedList } from 'vs/base/common/linkedList'; export namespace EventType { @@ -99,7 +99,7 @@ export class Gesture extends Disposable { return Disposable.None; } if (!Gesture.INSTANCE) { - Gesture.INSTANCE = new Gesture(); + Gesture.INSTANCE = markAsSingleton(new Gesture()); } const remove = Gesture.INSTANCE.targets.push(element); @@ -111,7 +111,7 @@ export class Gesture extends Disposable { return Disposable.None; } if (!Gesture.INSTANCE) { - Gesture.INSTANCE = new Gesture(); + Gesture.INSTANCE = markAsSingleton(new Gesture()); } const remove = Gesture.INSTANCE.ignoreTargets.push(element); diff --git a/src/vs/base/browser/ui/grid/gridview.ts b/src/vs/base/browser/ui/grid/gridview.ts index 9445c64286a..bdc630dbcf7 100644 --- a/src/vs/base/browser/ui/grid/gridview.ts +++ b/src/vs/base/browser/ui/grid/gridview.ts @@ -248,7 +248,7 @@ class BranchNode implements ISplitView, IDisposable { readonly element: HTMLElement; readonly children: Node[] = []; - private splitview: SplitView; + private splitview: SplitView; private _size: number; get size(): number { return this._size; } @@ -511,14 +511,27 @@ class BranchNode implements ISplitView, IDisposable { this.onDidChildrenChange(); } - removeChild(index: number, sizing?: Sizing): void { + removeChild(index: number, sizing?: Sizing): Node { index = validateIndex(index, this.children.length); - this.splitview.removeView(index, sizing); + const result = this.splitview.removeView(index, sizing); this.children.splice(index, 1); this.updateBoundarySashes(); this.onDidChildrenChange(); + + return result; + } + + removeAllChildren(): Node[] { + const result = this.splitview.removeAllViews(); + + this.children.splice(0, this.children.length); + + this.updateBoundarySashes(); + this.onDidChildrenChange(); + + return result; } moveChild(from: number, to: number): void { @@ -904,7 +917,10 @@ export interface INodeDescriptor { visible?: boolean; } -function flipNode(node: T, size: number, orthogonalSize: number): T { +function flipNode(node: BranchNode, size: number, orthogonalSize: number): BranchNode; +function flipNode(node: LeafNode, size: number, orthogonalSize: number): LeafNode; +function flipNode(node: Node, size: number, orthogonalSize: number): Node; +function flipNode(node: Node, size: number, orthogonalSize: number): Node { if (node instanceof BranchNode) { const result = new BranchNode(orthogonal(node.orientation), node.layoutController, node.styles, node.splitviewProportionalLayout, size, orthogonalSize, node.edgeSnapping); @@ -925,9 +941,12 @@ function flipNode(node: T, size: number, orthogonalSize: number) result.addChild(flipNode(child, orthogonalSize, newSize), newSize, 0, true); } - return result as T; + node.dispose(); + return result; } else { - return new LeafNode((node as LeafNode).view, orthogonal(node.orientation), node.layoutController, orthogonalSize) as T; + const result = new LeafNode(node.view, orthogonal(node.orientation), node.layoutController, orthogonalSize); + node.dispose(); + return result; } } @@ -1162,8 +1181,13 @@ export class GridView implements IDisposable { if (parent instanceof BranchNode) { const node = new LeafNode(view, orthogonal(parent.orientation), this.layoutController, parent.orthogonalSize); - parent.addChild(node, size, index); + try { + parent.addChild(node, size, index); + } catch (err) { + node.dispose(); + throw err; + } } else { const [, grandParent] = tail(pathToParent); const [, parentIndex] = tail(rest); @@ -1175,7 +1199,8 @@ export class GridView implements IDisposable { newSiblingSize = Sizing.Invisible(newSiblingCachedVisibleSize); } - grandParent.removeChild(parentIndex); + const oldChild = grandParent.removeChild(parentIndex); + oldChild.dispose(); const newParent = new BranchNode(parent.orientation, parent.layoutController, this.styles, this.proportionalLayout, parent.size, parent.orthogonalSize, grandParent.edgeSnapping); grandParent.addChild(newParent, parent.size, parentIndex); @@ -1218,6 +1243,7 @@ export class GridView implements IDisposable { } parent.removeChild(index, sizing); + node.dispose(); if (parent.children.length === 0) { throw new Error('Invalid grid state'); @@ -1237,6 +1263,7 @@ export class GridView implements IDisposable { // we must promote sibling to be the new root parent.removeChild(0); + parent.dispose(); this.root = sibling; this.boundarySashes = this.boundarySashes; this.trySet2x2(); @@ -1246,19 +1273,20 @@ export class GridView implements IDisposable { const [, grandParent] = tail(pathToParent); const [, parentIndex] = tail(rest); - const sibling = parent.children[0]; const isSiblingVisible = parent.isChildVisible(0); - parent.removeChild(0); + const sibling = parent.removeChild(0); const sizes = grandParent.children.map((_, i) => grandParent.getChildSize(i)); grandParent.removeChild(parentIndex, sizing); + parent.dispose(); if (sibling instanceof BranchNode) { sizes.splice(parentIndex, 1, ...sibling.children.map(c => c.size)); - for (let i = 0; i < sibling.children.length; i++) { - const child = sibling.children[i]; - grandParent.addChild(child, child.size, parentIndex + i); + const siblingChildren = sibling.removeAllChildren(); + + for (let i = 0; i < siblingChildren.length; i++) { + grandParent.addChild(siblingChildren[i], siblingChildren[i].size, parentIndex + i); } } else { const newSibling = new LeafNode(sibling.view, orthogonal(sibling.orientation), this.layoutController, sibling.size); @@ -1266,6 +1294,8 @@ export class GridView implements IDisposable { grandParent.addChild(newSibling, sizing, parentIndex); } + sibling.dispose(); + for (let i = 0; i < sizes.length; i++) { grandParent.resizeChild(i, sizes[i]); } @@ -1654,9 +1684,6 @@ export class GridView implements IDisposable { dispose(): void { this.onDidSashResetRelay.dispose(); this.root.dispose(); - - if (this.element && this.element.parentElement) { - this.element.parentElement.removeChild(this.element); - } + this.element.parentElement?.removeChild(this.element); } } diff --git a/src/vs/base/browser/ui/splitview/splitview.ts b/src/vs/base/browser/ui/splitview/splitview.ts index 28f18d42537..05b3e5a45f7 100644 --- a/src/vs/base/browser/ui/splitview/splitview.ts +++ b/src/vs/base/browser/ui/splitview/splitview.ts @@ -119,7 +119,7 @@ export interface IView { /** * A descriptor for a {@link SplitView} instance. */ -export interface ISplitViewDescriptor { +export interface ISplitViewDescriptor = IView> { /** * The layout size of the {@link SplitView}. @@ -150,11 +150,11 @@ export interface ISplitViewDescriptor { * * @defaultValue `true` */ - readonly view: IView; + readonly view: TView; }[]; } -export interface ISplitViewOptions { +export interface ISplitViewOptions = IView> { /** * Which axis the views align on. @@ -184,7 +184,7 @@ export interface ISplitViewOptions { * An initial description of this {@link SplitView} instance, allowing * to initialze all views within the ctor. */ - readonly descriptor?: ISplitViewDescriptor; + readonly descriptor?: ISplitViewDescriptor; /** * The scrollbar visibility setting for whenever the views within @@ -207,7 +207,7 @@ interface ISashEvent { type ViewItemSize = number | { cachedVisibleSize: number }; -abstract class ViewItem { +abstract class ViewItem> { private _size: number; set size(size: number) { @@ -259,7 +259,7 @@ abstract class ViewItem { constructor( protected container: HTMLElement, - readonly view: IView, + readonly view: TView, size: ViewItemSize, private disposable: IDisposable ) { @@ -285,7 +285,7 @@ abstract class ViewItem { } } -class VerticalViewItem extends ViewItem { +class VerticalViewItem> extends ViewItem { layoutContainer(offset: number): void { this.container.style.top = `${offset}px`; @@ -293,7 +293,7 @@ class VerticalViewItem extends ViewItem { } } -class HorizontalViewItem extends ViewItem { +class HorizontalViewItem> extends ViewItem { layoutContainer(offset: number): void { this.container.style.left = `${offset}px`; @@ -413,7 +413,7 @@ export namespace Sizing { * - View swap/move support * - Alt key modifier behavior, macOS style */ -export class SplitView extends Disposable { +export class SplitView = IView> extends Disposable { /** * This {@link SplitView}'s orientation. @@ -433,7 +433,7 @@ export class SplitView extends Disposable { private layoutContext: TLayoutContext | undefined; private contentSize = 0; private proportions: (number | undefined)[] | undefined = undefined; - private viewItems: ViewItem[] = []; + private viewItems: ViewItem[] = []; sashItems: ISashItem[] = []; // used in tests private sashDragState: ISashDragState | undefined; private state: State = State.Idle; @@ -549,7 +549,7 @@ export class SplitView extends Disposable { /** * Create a new {@link SplitView} instance. */ - constructor(container: HTMLElement, options: ISplitViewOptions = {}) { + constructor(container: HTMLElement, options: ISplitViewOptions = {}) { super(); this.orientation = options.orientation ?? Orientation.VERTICAL; @@ -636,7 +636,7 @@ export class SplitView extends Disposable { * @param index The index to insert the view on. * @param skipLayout Whether layout should be skipped. */ - addView(view: IView, size: number | Sizing, index = this.viewItems.length, skipLayout?: boolean): void { + addView(view: TView, size: number | Sizing, index = this.viewItems.length, skipLayout?: boolean): void { this.doAddView(view, size, index, skipLayout); } @@ -646,7 +646,7 @@ export class SplitView extends Disposable { * @param index The index where the {@link IView view} is located. * @param sizing Whether to distribute other {@link IView view}'s sizes. */ - removeView(index: number, sizing?: Sizing): IView { + removeView(index: number, sizing?: Sizing): TView { if (this.state !== State.Idle) { throw new Error('Cant modify splitview'); } @@ -695,6 +695,31 @@ export class SplitView extends Disposable { return result; } + removeAllViews(): TView[] { + if (this.state !== State.Idle) { + throw new Error('Cant modify splitview'); + } + + this.state = State.Busy; + + const viewItems = this.viewItems.splice(0, this.viewItems.length); + + for (const viewItem of viewItems) { + viewItem.dispose(); + } + + const sashItems = this.sashItems.splice(0, this.sashItems.length); + + for (const sashItem of sashItems) { + sashItem.disposable.dispose(); + } + + this.relayout(); + this.state = State.Idle; + + return viewItems.map(i => i.view); + } + /** * Move a {@link IView view} to a different index. * @@ -951,7 +976,7 @@ export class SplitView extends Disposable { } } - private onViewChange(item: ViewItem, size: number | undefined): void { + private onViewChange(item: ViewItem, size: number | undefined): void { const index = this.viewItems.indexOf(item); if (index < 0 || index >= this.viewItems.length) { @@ -1024,7 +1049,7 @@ export class SplitView extends Disposable { * Distribute the entire {@link SplitView} size among all {@link IView views}. */ distributeViewSizes(): void { - const flexibleViewItems: ViewItem[] = []; + const flexibleViewItems: ViewItem[] = []; let flexibleSize = 0; for (const item of this.viewItems) { @@ -1058,7 +1083,7 @@ export class SplitView extends Disposable { return this.viewItems[index].size; } - private doAddView(view: IView, size: number | Sizing, index = this.viewItems.length, skipLayout?: boolean): void { + private doAddView(view: TView, size: number | Sizing, index = this.viewItems.length, skipLayout?: boolean): void { if (this.state !== State.Idle) { throw new Error('Cant modify splitview'); } diff --git a/src/vs/base/common/arraysFind.ts b/src/vs/base/common/arraysFind.ts index 8ef2d2ad936..7b24f5ade42 100644 --- a/src/vs/base/common/arraysFind.ts +++ b/src/vs/base/common/arraysFind.ts @@ -5,7 +5,7 @@ import { Comparator } from './arrays'; -export function findLast(array: readonly T[], predicate: (item: T) => boolean): T | undefined { +export function findLast(array: readonly T[], predicate: (item: T) => boolean, fromIdx?: number): T | undefined { const idx = findLastIdx(array, predicate); if (idx === -1) { return undefined; @@ -13,8 +13,8 @@ export function findLast(array: readonly T[], predicate: (item: T) => boolean return array[idx]; } -export function findLastIdx(array: readonly T[], predicate: (item: T) => boolean): number { - for (let i = array.length - 1; i >= 0; i--) { +export function findLastIdx(array: readonly T[], predicate: (item: T) => boolean, fromIndex = array.length - 1): number { + for (let i = fromIndex; i >= 0; i--) { const element = array[i]; if (predicate(element)) { diff --git a/src/vs/base/common/async.ts b/src/vs/base/common/async.ts index 152a6af7e4e..8ce413f6e70 100644 --- a/src/vs/base/common/async.ts +++ b/src/vs/base/common/async.ts @@ -6,7 +6,7 @@ import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { CancellationError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable, IDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableMap, IDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { extUri as defaultExtUri, IExtUri } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { setTimeout0 } from 'vs/base/common/platform'; @@ -182,7 +182,7 @@ export class Throttler implements IDisposable { queue(promiseFactory: ITask>): Promise { if (this.isDisposed) { - throw new Error('Throttler is disposed'); + return Promise.reject(new Error('Throttler is disposed')); } if (this.activePromise) { @@ -717,6 +717,9 @@ export class ResourceQueue implements IDisposable { private readonly drainers = new Set>(); + private drainListeners: DisposableMap | undefined = undefined; + private drainListenerCount = 0; + async whenDrained(): Promise { if (this.isDrained()) { return; @@ -744,12 +747,25 @@ export class ResourceQueue implements IDisposable { let queue = this.queues.get(key); if (!queue) { queue = new Queue(); - Event.once(queue.onDrained)(() => { + const drainListenerId = this.drainListenerCount++; + const drainListener = Event.once(queue.onDrained)(() => { queue?.dispose(); this.queues.delete(key); this.onDidQueueDrain(); + + this.drainListeners?.deleteAndDispose(drainListenerId); + + if (this.drainListeners?.size === 0) { + this.drainListeners.dispose(); + this.drainListeners = undefined; + } }); + if (!this.drainListeners) { + this.drainListeners = new DisposableMap(); + } + this.drainListeners.set(drainListenerId, drainListener); + this.queues.set(key, queue); } @@ -786,6 +802,8 @@ export class ResourceQueue implements IDisposable { // promises when the resource queue is being // disposed. this.releaseDrainers(); + + this.drainListeners?.dispose(); } } diff --git a/src/vs/base/common/event.ts b/src/vs/base/common/event.ts index 2e5f7fc27a1..b8e1da4b777 100644 --- a/src/vs/base/common/event.ts +++ b/src/vs/base/common/event.ts @@ -394,7 +394,7 @@ export namespace Event { * this.onInstallExtension = Event.buffer(service.onInstallExtension, true); * ``` */ - export function buffer(event: Event, flushAfterTimeout = false, _buffer: T[] = []): Event { + export function buffer(event: Event, flushAfterTimeout = false, _buffer: T[] = [], disposable?: DisposableStore): Event { let buffer: T[] | null = _buffer.slice(); let listener: IDisposable | null = event(e => { @@ -405,6 +405,10 @@ export namespace Event { } }); + if (disposable) { + disposable.add(listener); + } + const flush = () => { buffer?.forEach(e => emitter.fire(e)); buffer = null; @@ -414,6 +418,9 @@ export namespace Event { onWillAddFirstListener() { if (!listener) { listener = event(e => emitter.fire(e)); + if (disposable) { + disposable.add(listener); + } } }, @@ -435,6 +442,10 @@ export namespace Event { } }); + if (disposable) { + disposable.add(emitter); + } + return emitter.event; } /** diff --git a/src/vs/base/common/lifecycle.ts b/src/vs/base/common/lifecycle.ts index 1682b7932ae..3d5dd09598d 100644 --- a/src/vs/base/common/lifecycle.ts +++ b/src/vs/base/common/lifecycle.ts @@ -539,6 +539,10 @@ export class DisposableMap implements ID return this._store.has(key); } + get size(): number { + return this._store.size; + } + get(key: K): V | undefined { return this._store.get(key); } diff --git a/src/vs/base/common/observableInternal/base.ts b/src/vs/base/common/observableInternal/base.ts index 31e345a0bf2..966b07b191c 100644 --- a/src/vs/base/common/observableInternal/base.ts +++ b/src/vs/base/common/observableInternal/base.ts @@ -58,6 +58,7 @@ export interface IObservable { * (see {@link ConvenientObservable.map}). */ map(fn: (value: T, reader: IReader) => TNew): IObservable; + map(owner: object, fn: (value: T, reader: IReader) => TNew): IObservable; /** * A human-readable name for debugging purposes. @@ -165,9 +166,15 @@ export abstract class ConvenientObservable implements IObservable(fn: (value: T, reader: IReader) => TNew): IObservable { + public map(fn: (value: T, reader: IReader) => TNew): IObservable; + public map(owner: object, fn: (value: T, reader: IReader) => TNew): IObservable; + public map(fnOrOwner: object | ((value: T, reader: IReader) => TNew), fnOrUndefined?: (value: T, reader: IReader) => TNew): IObservable { + const owner = fnOrUndefined === undefined ? undefined : fnOrOwner as object; + const fn = fnOrUndefined === undefined ? fnOrOwner as (value: T, reader: IReader) => TNew : fnOrUndefined; + return _derived( { + owner, debugName: () => { const name = getFunctionName(fn); if (name !== undefined) { @@ -180,7 +187,10 @@ export abstract class ConvenientObservable implements IObservable fn(this.read(reader), reader), diff --git a/src/vs/base/common/observableInternal/derived.ts b/src/vs/base/common/observableInternal/derived.ts index 0f1abf6043e..53c2a3a7eee 100644 --- a/src/vs/base/common/observableInternal/derived.ts +++ b/src/vs/base/common/observableInternal/derived.ts @@ -26,7 +26,7 @@ export function derived(computeFnOrOwner: ((reader: IReader) => T) | object, export function derivedOpts( options: { owner?: object; - debugName?: string | (() => string); + debugName?: string | (() => string | undefined); equalityComparer?: EqualityComparer; }, computeFn: (reader: IReader) => T diff --git a/src/vs/base/common/paging.ts b/src/vs/base/common/paging.ts index 1d9b7938169..07599e730bc 100644 --- a/src/vs/base/common/paging.ts +++ b/src/vs/base/common/paging.ts @@ -118,7 +118,7 @@ export class PagedModel implements IPagedModel { }); } - cancellationToken.onCancellationRequested(() => { + const listener = cancellationToken.onCancellationRequested(() => { if (!page.cts) { return; } @@ -132,7 +132,8 @@ export class PagedModel implements IPagedModel { page.promiseIndexes.add(index); - return page.promise.then(() => page.elements[indexInPage]); + return page.promise.then(() => page.elements[indexInPage]) + .finally(() => listener.dispose()); } } diff --git a/src/vs/base/common/stream.ts b/src/vs/base/common/stream.ts index 055558fc748..d6cd674b1de 100644 --- a/src/vs/base/common/stream.ts +++ b/src/vs/base/common/stream.ts @@ -622,11 +622,15 @@ export function peekStream(stream: ReadableStream, maxChunks: number): Pro // Error Listener const errorListener = (error: Error) => { + streamListeners.dispose(); + return reject(error); }; // End Listener const endListener = () => { + streamListeners.dispose(); + return resolve({ stream, buffer, ended: true }); }; diff --git a/src/vs/base/node/zip.ts b/src/vs/base/node/zip.ts index 767d0a5aec6..cc4c65a625f 100644 --- a/src/vs/base/node/zip.ts +++ b/src/vs/base/node/zip.ts @@ -107,12 +107,12 @@ function extractZip(zipfile: ZipFile, targetPath: string, options: IOptions, tok let last = createCancelablePromise(() => Promise.resolve()); let extractedEntriesCount = 0; - token.onCancellationRequested(() => { + const listener = token.onCancellationRequested(() => { last.cancel(); zipfile.close(); }); - return new Promise((c, e) => { + return new Promise((c, e) => { const throttler = new Sequencer(); const readNextEntry = (token: CancellationToken) => { @@ -158,7 +158,7 @@ function extractZip(zipfile: ZipFile, targetPath: string, options: IOptions, tok last = createCancelablePromise(token => throttler.queue(() => stream.then(stream => extractEntry(stream, fileName, mode, targetPath, options, token).then(() => readNextEntry(token)))).then(null, e)); }); - }); + }).finally(() => listener.dispose()); } function openZip(zipFile: string, lazy: boolean = false): Promise { diff --git a/src/vs/base/parts/ipc/common/ipc.ts b/src/vs/base/parts/ipc/common/ipc.ts index 747e8513b1f..db61aed4ff1 100644 --- a/src/vs/base/parts/ipc/common/ipc.ts +++ b/src/vs/base/parts/ipc/common/ipc.ts @@ -427,7 +427,6 @@ export class ChannelServer implements IChannelServer { this.sendResponse({ id, data, type: ResponseType.PromiseSuccess }); - this.activeRequests.delete(request.id); }, err => { if (err instanceof Error) { this.sendResponse({ @@ -440,7 +439,8 @@ export class ChannelServer implements IChannelServer{ id, data: err, type: ResponseType.PromiseErrorObj }); } - + }).finally(() => { + disposable.dispose(); this.activeRequests.delete(request.id); }); @@ -639,7 +639,10 @@ export class ChannelClient implements IChannelClient, IDisposable { this.activeRequests.add(disposable); }); - return result.finally(() => { this.activeRequests.delete(disposable); }); + return result.finally(() => { + disposable.dispose(); + this.activeRequests.delete(disposable); + }); } private requestEvent(channelName: string, name: string, arg?: any): Event { @@ -795,6 +798,8 @@ export class IPCServer implements IChannelServer, I private readonly _onDidRemoveConnection = new Emitter>(); readonly onDidRemoveConnection: Event> = this._onDidRemoveConnection.event; + private disposables = new DisposableStore(); + get connections(): Connection[] { const result: Connection[] = []; this._connections.forEach(ctx => result.push(ctx)); @@ -802,10 +807,10 @@ export class IPCServer implements IChannelServer, I } constructor(onDidClientConnect: Event) { - onDidClientConnect(({ protocol, onDidClientDisconnect }) => { + this.disposables.add(onDidClientConnect(({ protocol, onDidClientDisconnect }) => { const onFirstMessage = Event.once(protocol.onMessage); - onFirstMessage(msg => { + this.disposables.add(onFirstMessage(msg => { const reader = new BufferReader(msg); const ctx = deserialize(reader) as TContext; @@ -818,14 +823,14 @@ export class IPCServer implements IChannelServer, I this._connections.add(connection); this._onDidAddConnection.fire(connection); - onDidClientDisconnect(() => { + this.disposables.add(onDidClientDisconnect(() => { channelServer.dispose(); channelClient.dispose(); this._connections.delete(connection); this._onDidRemoveConnection.fire(connection); - }); - }); - }); + })); + })); + })); } /** @@ -879,7 +884,7 @@ export class IPCServer implements IChannelServer, I private getMulticastEvent(channelName: string, clientFilter: (client: Client) => boolean, eventName: string, arg: any): Event { const that = this; - let disposables = new DisposableStore(); + let disposables: DisposableStore | undefined; // Create an emitter which hooks up to all clients // as soon as first listener is added. It also @@ -922,7 +927,8 @@ export class IPCServer implements IChannelServer, I disposables.add(eventMultiplexer); }, onDidRemoveLastListener: () => { - disposables.dispose(); + disposables?.dispose(); + disposables = undefined; } }); @@ -932,14 +938,21 @@ export class IPCServer implements IChannelServer, I registerChannel(channelName: string, channel: IServerChannel): void { this.channels.set(channelName, channel); - this._connections.forEach(connection => { + for (const connection of this._connections) { connection.channelServer.registerChannel(channelName, channel); - }); + } } dispose(): void { - this.channels.clear(); + this.disposables.dispose(); + + for (const connection of this._connections) { + connection.channelClient.dispose(); + connection.channelServer.dispose(); + } + this._connections.clear(); + this.channels.clear(); this._onDidAddConnection.dispose(); this._onDidRemoveConnection.dispose(); } @@ -1074,7 +1087,7 @@ export namespace ProxyChannel { export interface ICreateServiceChannelOptions extends IProxyOptions { } - export function fromService(service: unknown, options?: ICreateServiceChannelOptions): IServerChannel { + export function fromService(service: unknown, disposables: DisposableStore, options?: ICreateServiceChannelOptions): IServerChannel { const handler = service as { [key: string]: unknown }; const disableMarshalling = options && options.disableMarshalling; @@ -1083,7 +1096,7 @@ export namespace ProxyChannel { const mapEventNameToEvent = new Map>(); for (const key in handler) { if (propertyIsEvent(key)) { - mapEventNameToEvent.set(key, Event.buffer(handler[key] as Event, true)); + mapEventNameToEvent.set(key, Event.buffer(handler[key] as Event, true, undefined, disposables)); } } diff --git a/src/vs/base/parts/ipc/test/common/ipc.test.ts b/src/vs/base/parts/ipc/test/common/ipc.test.ts index eaead87178e..35184c08217 100644 --- a/src/vs/base/parts/ipc/test/common/ipc.test.ts +++ b/src/vs/base/parts/ipc/test/common/ipc.test.ts @@ -9,9 +9,11 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { canceled } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { BufferReader, BufferWriter, ClientConnectionEvent, deserialize, IChannel, IMessagePassingProtocol, IPCClient, IPCServer, IServerChannel, ProxyChannel, serialize } from 'vs/base/parts/ipc/common/ipc'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; class QueueProtocol implements IMessagePassingProtocol { @@ -111,6 +113,8 @@ interface ITestService { class TestService implements ITestService { + private disposables = new DisposableStore(); + private readonly _onPong = new Emitter(); readonly onPong = this._onPong.event; @@ -131,7 +135,7 @@ class TestService implements ITestService { return Promise.reject(canceled()); } - return new Promise((_, e) => cancellationToken.onCancellationRequested(() => e(canceled()))); + return new Promise((_, e) => this.disposables.add(cancellationToken.onCancellationRequested(() => e(canceled())))); } buffersLength(buffers: VSBuffer[]): Promise { @@ -149,6 +153,10 @@ class TestService implements ITestService { context(context?: unknown): Promise { return Promise.resolve(context); } + + dispose() { + this.disposables.dispose(); + } } class TestChannel implements IServerChannel { @@ -213,6 +221,8 @@ class TestChannelClient implements ITestService { suite('Base IPC', function () { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + test('createProtocolPair', async function () { const [clientProtocol, serverProtocol] = createProtocolPair(); @@ -236,21 +246,16 @@ suite('Base IPC', function () { let ipcService: ITestService; setup(function () { - service = new TestService(); - const testServer = new TestIPCServer(); + service = store.add(new TestService()); + const testServer = store.add(new TestIPCServer()); server = testServer; server.registerChannel(TestChannelId, new TestChannel(service)); - client = testServer.createConnection('client1'); + client = store.add(testServer.createConnection('client1')); ipcService = new TestChannelClient(client.getChannel(TestChannelId)); }); - teardown(function () { - client.dispose(); - server.dispose(); - }); - test('call success', async function () { const r = await ipcService.marco(); return assert.strictEqual(r, 'polo'); @@ -301,7 +306,7 @@ suite('Base IPC', function () { test('listen to events', async function () { const messages: string[] = []; - ipcService.onPong(msg => messages.push(msg)); + store.add(ipcService.onPong(msg => messages.push(msg))); await timeout(0); assert.deepStrictEqual(messages, []); @@ -343,20 +348,21 @@ suite('Base IPC', function () { let service: TestService; let ipcService: ITestService; + const disposables = new DisposableStore(); + setup(function () { - service = new TestService(); - const testServer = new TestIPCServer(); + service = store.add(new TestService()); + const testServer = disposables.add(new TestIPCServer()); server = testServer; - server.registerChannel(TestChannelId, ProxyChannel.fromService(service)); + server.registerChannel(TestChannelId, ProxyChannel.fromService(service, disposables)); - client = testServer.createConnection('client1'); + client = disposables.add(testServer.createConnection('client1')); ipcService = ProxyChannel.toService(client.getChannel(TestChannelId)); }); teardown(function () { - client.dispose(); - server.dispose(); + disposables.clear(); }); test('call success', async function () { @@ -376,7 +382,7 @@ suite('Base IPC', function () { test('listen to events', async function () { const messages: string[] = []; - ipcService.onPong(msg => messages.push(msg)); + disposables.add(ipcService.onPong(msg => messages.push(msg))); await timeout(0); assert.deepStrictEqual(messages, []); @@ -409,20 +415,21 @@ suite('Base IPC', function () { let service: TestService; let ipcService: ITestService; + const disposables = new DisposableStore(); + setup(function () { - service = new TestService(); - const testServer = new TestIPCServer(); + service = store.add(new TestService()); + const testServer = disposables.add(new TestIPCServer()); server = testServer; - server.registerChannel(TestChannelId, ProxyChannel.fromService(service)); + server.registerChannel(TestChannelId, ProxyChannel.fromService(service, disposables)); - client = testServer.createConnection('client1'); + client = disposables.add(testServer.createConnection('client1')); ipcService = ProxyChannel.toService(client.getChannel(TestChannelId), { context: 'Super Context' }); }); teardown(function () { - client.dispose(); - server.dispose(); + disposables.clear(); }); test('call extra context', async function () { @@ -433,20 +440,20 @@ suite('Base IPC', function () { suite('one to many', function () { test('all clients get pinged', async function () { - const service = new TestService(); + const service = store.add(new TestService()); const channel = new TestChannel(service); - const server = new TestIPCServer(); + const server = store.add(new TestIPCServer()); server.registerChannel('channel', channel); let client1GotPinged = false; - const client1 = server.createConnection('client1'); + const client1 = store.add(server.createConnection('client1')); const ipcService1 = new TestChannelClient(client1.getChannel('channel')); - ipcService1.onPong(() => client1GotPinged = true); + store.add(ipcService1.onPong(() => client1GotPinged = true)); let client2GotPinged = false; - const client2 = server.createConnection('client2'); + const client2 = store.add(server.createConnection('client2')); const ipcService2 = new TestChannelClient(client2.getChannel('channel')); - ipcService2.onPong(() => client2GotPinged = true); + store.add(ipcService2.onPong(() => client2GotPinged = true)); await timeout(1); service.ping('hello'); @@ -454,24 +461,20 @@ suite('Base IPC', function () { await timeout(1); assert(client1GotPinged, 'client 1 got pinged'); assert(client2GotPinged, 'client 2 got pinged'); - - client1.dispose(); - client2.dispose(); - server.dispose(); }); test('server gets pings from all clients (broadcast channel)', async function () { - const server = new TestIPCServer(); + const server = store.add(new TestIPCServer()); const client1 = server.createConnection('client1'); - const clientService1 = new TestService(); + const clientService1 = store.add(new TestService()); const clientChannel1 = new TestChannel(clientService1); client1.registerChannel('channel', clientChannel1); const pings: string[] = []; const channel = server.getChannel('channel', () => true); const service = new TestChannelClient(channel); - service.onPong(msg => pings.push(msg)); + store.add(service.onPong(msg => pings.push(msg))); await timeout(1); clientService1.ping('hello 1'); @@ -480,7 +483,7 @@ suite('Base IPC', function () { assert.deepStrictEqual(pings, ['hello 1']); const client2 = server.createConnection('client2'); - const clientService2 = new TestService(); + const clientService2 = store.add(new TestService()); const clientChannel2 = new TestChannel(clientService2); client2.registerChannel('channel', clientChannel2); @@ -503,7 +506,6 @@ suite('Base IPC', function () { assert.deepStrictEqual(pings, ['hello 1', 'hello 2', 'hello again 2']); client2.dispose(); - server.dispose(); }); }); }); diff --git a/src/vs/base/parts/storage/common/storage.ts b/src/vs/base/parts/storage/common/storage.ts index 304da70de7d..eb9607e6c3e 100644 --- a/src/vs/base/parts/storage/common/storage.ts +++ b/src/vs/base/parts/storage/common/storage.ts @@ -123,7 +123,7 @@ export class Storage extends Disposable implements IStorage { private cache = new Map(); - private readonly flushDelayer = new ThrottledDelayer(Storage.DEFAULT_FLUSH_DELAY); + private readonly flushDelayer = this._register(new ThrottledDelayer(Storage.DEFAULT_FLUSH_DELAY)); private pendingDeletes = new Set(); private pendingInserts = new Map(); @@ -392,6 +392,10 @@ export class Storage extends Disposable implements IStorage { } private async doFlush(delay?: number): Promise { + if (this.options.hint === StorageHint.STORAGE_IN_MEMORY) { + return this.flushPending(); // return early if in-memory + } + return this.flushDelayer.trigger(() => this.flushPending(), delay); } @@ -406,12 +410,6 @@ export class Storage extends Disposable implements IStorage { isInMemory(): boolean { return this.options.hint === StorageHint.STORAGE_IN_MEMORY; } - - override dispose(): void { - this.flushDelayer.dispose(); - - super.dispose(); - } } export class InMemoryStorageDatabase implements IStorageDatabase { diff --git a/src/vs/base/test/browser/actionbar.test.ts b/src/vs/base/test/browser/actionbar.test.ts index 5c593ce1b69..fc119f36de9 100644 --- a/src/vs/base/test/browser/actionbar.test.ts +++ b/src/vs/base/test/browser/actionbar.test.ts @@ -6,16 +6,19 @@ import * as assert from 'assert'; import { ActionBar, prepareActions } from 'vs/base/browser/ui/actionbar/actionbar'; import { Action, Separator } from 'vs/base/common/actions'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('Actionbar', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + test('prepareActions()', function () { const a1 = new Separator(); const a2 = new Separator(); - const a3 = new Action('a3'); + const a3 = store.add(new Action('a3')); const a4 = new Separator(); const a5 = new Separator(); - const a6 = new Action('a6'); + const a6 = store.add(new Action('a6')); const a7 = new Separator(); const actions = prepareActions([a1, a2, a3, a4, a5, a6, a7]); @@ -27,10 +30,10 @@ suite('Actionbar', () => { test('hasAction()', function () { const container = document.createElement('div'); - const actionbar = new ActionBar(container); + const actionbar = store.add(new ActionBar(container)); - const a1 = new Action('a1'); - const a2 = new Action('a2'); + const a1 = store.add(new Action('a1')); + const a2 = store.add(new Action('a2')); actionbar.push(a1); assert.strictEqual(actionbar.hasAction(a1), true); diff --git a/src/vs/base/test/browser/highlightedLabel.test.ts b/src/vs/base/test/browser/highlightedLabel.test.ts index 1bff745515b..4f5eb5ca015 100644 --- a/src/vs/base/test/browser/highlightedLabel.test.ts +++ b/src/vs/base/test/browser/highlightedLabel.test.ts @@ -5,6 +5,7 @@ import * as assert from 'assert'; import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('HighlightedLabel', () => { let label: HighlightedLabel; @@ -58,6 +59,7 @@ suite('HighlightedLabel', () => { escaped = HighlightedLabel.escapeNewLines('ACTION\r\n_TYPE2', highlights); assert.strictEqual(escaped, 'ACTION\u23CE_TYPE2'); assert.deepStrictEqual(highlights, [{ start: 5, end: 8 }, { start: 10, end: 11 }]); - }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/base/test/browser/progressBar.test.ts b/src/vs/base/test/browser/progressBar.test.ts index f43082a0bc6..eb9790cd6ce 100644 --- a/src/vs/base/test/browser/progressBar.test.ts +++ b/src/vs/base/test/browser/progressBar.test.ts @@ -5,6 +5,7 @@ import * as assert from 'assert'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('ProgressBar', () => { let fixture: HTMLElement; @@ -29,4 +30,6 @@ suite('ProgressBar', () => { bar.dispose(); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/base/test/browser/ui/grid/grid.test.ts b/src/vs/base/test/browser/ui/grid/grid.test.ts index 8dcbb829d7b..b4e5e4d6c16 100644 --- a/src/vs/base/test/browser/ui/grid/grid.test.ts +++ b/src/vs/base/test/browser/ui/grid/grid.test.ts @@ -8,6 +8,8 @@ import { createSerializedGrid, Direction, getRelativeLocation, Grid, GridNode, G import { Event } from 'vs/base/common/event'; import { deepClone } from 'vs/base/common/objects'; import { nodesToArrays, TestView } from './util'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { DisposableStore } from 'vs/base/common/lifecycle'; // Simple example: // @@ -30,6 +32,8 @@ import { nodesToArrays, TestView } from './util'; // +-3 suite('Grid', function () { + + const store = ensureNoDisposablesAreLeakedInTestSuite(); let container: HTMLElement; setup(function () { @@ -72,8 +76,8 @@ suite('Grid', function () { }); test('empty', () => { - const view1 = new TestView(100, Number.MAX_VALUE, 100, Number.MAX_VALUE); - const gridview = new Grid(view1); + const view1 = store.add(new TestView(100, Number.MAX_VALUE, 100, Number.MAX_VALUE)); + const gridview = store.add(new Grid(view1)); container.appendChild(gridview.element); gridview.layout(800, 600); @@ -81,59 +85,59 @@ suite('Grid', function () { }); test('two views vertically', function () { - const view1 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); - const grid = new Grid(view1); + const view1 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); + const grid = store.add(new Grid(view1)); container.appendChild(grid.element); grid.layout(800, 600); assert.deepStrictEqual(view1.size, [800, 600]); - const view2 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view2 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view2, 200, view1, Direction.Up); assert.deepStrictEqual(view1.size, [800, 400]); assert.deepStrictEqual(view2.size, [800, 200]); }); test('two views horizontally', function () { - const view1 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); - const grid = new Grid(view1); + const view1 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); + const grid = store.add(new Grid(view1)); container.appendChild(grid.element); grid.layout(800, 600); assert.deepStrictEqual(view1.size, [800, 600]); - const view2 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view2 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view2, 300, view1, Direction.Right); assert.deepStrictEqual(view1.size, [500, 600]); assert.deepStrictEqual(view2.size, [300, 600]); }); test('simple layout', function () { - const view1 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); - const grid = new Grid(view1); + const view1 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); + const grid = store.add(new Grid(view1)); container.appendChild(grid.element); grid.layout(800, 600); assert.deepStrictEqual(view1.size, [800, 600]); - const view2 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view2 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view2, 200, view1, Direction.Up); assert.deepStrictEqual(view1.size, [800, 400]); assert.deepStrictEqual(view2.size, [800, 200]); - const view3 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view3 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view3, 200, view1, Direction.Right); assert.deepStrictEqual(view1.size, [600, 400]); assert.deepStrictEqual(view2.size, [800, 200]); assert.deepStrictEqual(view3.size, [200, 400]); - const view4 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view4 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view4, 200, view2, Direction.Left); assert.deepStrictEqual(view1.size, [600, 400]); assert.deepStrictEqual(view2.size, [600, 200]); assert.deepStrictEqual(view3.size, [200, 400]); assert.deepStrictEqual(view4.size, [200, 200]); - const view5 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view5 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view5, 100, view1, Direction.Down); assert.deepStrictEqual(view1.size, [600, 300]); assert.deepStrictEqual(view2.size, [600, 200]); @@ -143,32 +147,32 @@ suite('Grid', function () { }); test('another simple layout with automatic size distribution', function () { - const view1 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); - const grid = new Grid(view1); + const view1 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); + const grid = store.add(new Grid(view1)); container.appendChild(grid.element); grid.layout(800, 600); assert.deepStrictEqual(view1.size, [800, 600]); - const view2 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view2 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view2, Sizing.Distribute, view1, Direction.Left); assert.deepStrictEqual(view1.size, [400, 600]); assert.deepStrictEqual(view2.size, [400, 600]); - const view3 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view3 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view3, Sizing.Distribute, view1, Direction.Right); assert.deepStrictEqual(view1.size, [266, 600]); assert.deepStrictEqual(view2.size, [266, 600]); assert.deepStrictEqual(view3.size, [268, 600]); - const view4 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view4 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view4, Sizing.Distribute, view2, Direction.Down); assert.deepStrictEqual(view1.size, [266, 600]); assert.deepStrictEqual(view2.size, [266, 300]); assert.deepStrictEqual(view3.size, [268, 600]); assert.deepStrictEqual(view4.size, [266, 300]); - const view5 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view5 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view5, Sizing.Distribute, view3, Direction.Up); assert.deepStrictEqual(view1.size, [266, 600]); assert.deepStrictEqual(view2.size, [266, 300]); @@ -176,7 +180,7 @@ suite('Grid', function () { assert.deepStrictEqual(view4.size, [266, 300]); assert.deepStrictEqual(view5.size, [268, 300]); - const view6 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view6 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view6, Sizing.Distribute, view3, Direction.Down); assert.deepStrictEqual(view1.size, [266, 600]); assert.deepStrictEqual(view2.size, [266, 300]); @@ -187,32 +191,32 @@ suite('Grid', function () { }); test('another simple layout with split size distribution', function () { - const view1 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); - const grid = new Grid(view1); + const view1 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); + const grid = store.add(new Grid(view1)); container.appendChild(grid.element); grid.layout(800, 600); assert.deepStrictEqual(view1.size, [800, 600]); - const view2 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view2 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view2, Sizing.Split, view1, Direction.Left); assert.deepStrictEqual(view1.size, [400, 600]); assert.deepStrictEqual(view2.size, [400, 600]); - const view3 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view3 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view3, Sizing.Split, view1, Direction.Right); assert.deepStrictEqual(view1.size, [200, 600]); assert.deepStrictEqual(view2.size, [400, 600]); assert.deepStrictEqual(view3.size, [200, 600]); - const view4 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view4 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view4, Sizing.Split, view2, Direction.Down); assert.deepStrictEqual(view1.size, [200, 600]); assert.deepStrictEqual(view2.size, [400, 300]); assert.deepStrictEqual(view3.size, [200, 600]); assert.deepStrictEqual(view4.size, [400, 300]); - const view5 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view5 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view5, Sizing.Split, view3, Direction.Up); assert.deepStrictEqual(view1.size, [200, 600]); assert.deepStrictEqual(view2.size, [400, 300]); @@ -220,7 +224,7 @@ suite('Grid', function () { assert.deepStrictEqual(view4.size, [400, 300]); assert.deepStrictEqual(view5.size, [200, 300]); - const view6 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view6 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view6, Sizing.Split, view3, Direction.Down); assert.deepStrictEqual(view1.size, [200, 600]); assert.deepStrictEqual(view2.size, [400, 300]); @@ -231,32 +235,32 @@ suite('Grid', function () { }); test('3/2 layout with split', function () { - const view1 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); - const grid = new Grid(view1); + const view1 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); + const grid = store.add(new Grid(view1)); container.appendChild(grid.element); grid.layout(800, 600); assert.deepStrictEqual(view1.size, [800, 600]); - const view2 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view2 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view2, Sizing.Split, view1, Direction.Down); assert.deepStrictEqual(view1.size, [800, 300]); assert.deepStrictEqual(view2.size, [800, 300]); - const view3 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view3 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view3, Sizing.Split, view2, Direction.Right); assert.deepStrictEqual(view1.size, [800, 300]); assert.deepStrictEqual(view2.size, [400, 300]); assert.deepStrictEqual(view3.size, [400, 300]); - const view4 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view4 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view4, Sizing.Split, view1, Direction.Right); assert.deepStrictEqual(view1.size, [400, 300]); assert.deepStrictEqual(view2.size, [400, 300]); assert.deepStrictEqual(view3.size, [400, 300]); assert.deepStrictEqual(view4.size, [400, 300]); - const view5 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view5 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view5, Sizing.Split, view1, Direction.Right); assert.deepStrictEqual(view1.size, [200, 300]); assert.deepStrictEqual(view2.size, [400, 300]); @@ -266,19 +270,19 @@ suite('Grid', function () { }); test('sizing should be correct after branch demotion #50564', function () { - const view1 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); - const grid = new Grid(view1); + const view1 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); + const grid = store.add(new Grid(view1)); container.appendChild(grid.element); grid.layout(800, 600); - const view2 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view2 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view2, Sizing.Split, view1, Direction.Right); - const view3 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view3 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view3, Sizing.Split, view2, Direction.Down); - const view4 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view4 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view4, Sizing.Split, view2, Direction.Right); assert.deepStrictEqual(view1.size, [400, 600]); assert.deepStrictEqual(view2.size, [200, 300]); @@ -292,19 +296,19 @@ suite('Grid', function () { }); test('sizing should be correct after branch demotion #50675', function () { - const view1 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); - const grid = new Grid(view1); + const view1 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); + const grid = store.add(new Grid(view1)); container.appendChild(grid.element); grid.layout(800, 600); - const view2 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view2 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view2, Sizing.Distribute, view1, Direction.Down); - const view3 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view3 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view3, Sizing.Distribute, view2, Direction.Down); - const view4 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view4 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view4, Sizing.Distribute, view3, Direction.Right); assert.deepStrictEqual(view1.size, [800, 200]); assert.deepStrictEqual(view2.size, [800, 200]); @@ -318,8 +322,8 @@ suite('Grid', function () { }); test('getNeighborViews should work on single view layout', function () { - const view1 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); - const grid = new Grid(view1); + const view1 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); + const grid = store.add(new Grid(view1)); container.appendChild(grid.element); grid.layout(800, 600); @@ -336,16 +340,16 @@ suite('Grid', function () { }); test('getNeighborViews should work on simple layout', function () { - const view1 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); - const grid = new Grid(view1); + const view1 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); + const grid = store.add(new Grid(view1)); container.appendChild(grid.element); grid.layout(800, 600); - const view2 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view2 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view2, Sizing.Distribute, view1, Direction.Down); - const view3 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view3 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view3, Sizing.Distribute, view2, Direction.Down); assert.deepStrictEqual(grid.getNeighborViews(view1, Direction.Up), []); @@ -380,22 +384,22 @@ suite('Grid', function () { }); test('getNeighborViews should work on a complex layout', function () { - const view1 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); - const grid = new Grid(view1); + const view1 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); + const grid = store.add(new Grid(view1)); container.appendChild(grid.element); grid.layout(800, 600); - const view2 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view2 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view2, Sizing.Distribute, view1, Direction.Down); - const view3 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view3 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view3, Sizing.Distribute, view2, Direction.Down); - const view4 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view4 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view4, Sizing.Distribute, view2, Direction.Right); - const view5 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view5 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view5, Sizing.Distribute, view4, Direction.Down); assert.deepStrictEqual(grid.getNeighborViews(view1, Direction.Up), []); @@ -421,19 +425,19 @@ suite('Grid', function () { }); test('getNeighborViews should work on another simple layout', function () { - const view1 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); - const grid = new Grid(view1); + const view1 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); + const grid = store.add(new Grid(view1)); container.appendChild(grid.element); grid.layout(800, 600); - const view2 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view2 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view2, Sizing.Distribute, view1, Direction.Right); - const view3 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view3 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view3, Sizing.Distribute, view2, Direction.Down); - const view4 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view4 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view4, Sizing.Distribute, view2, Direction.Right); assert.deepStrictEqual(grid.getNeighborViews(view4, Direction.Up), []); @@ -443,19 +447,19 @@ suite('Grid', function () { }); test('getNeighborViews should only return immediate neighbors', function () { - const view1 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); - const grid = new Grid(view1); + const view1 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); + const grid = store.add(new Grid(view1)); container.appendChild(grid.element); grid.layout(800, 600); - const view2 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view2 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view2, Sizing.Distribute, view1, Direction.Right); - const view3 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view3 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view3, Sizing.Distribute, view2, Direction.Down); - const view4 = new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view4 = store.add(new TestView(50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view4, Sizing.Distribute, view2, Direction.Right); assert.deepStrictEqual(grid.getNeighborViews(view1, Direction.Right), [view2, view3]); @@ -483,8 +487,10 @@ class TestViewDeserializer implements IViewDeserializer { private views = new Map(); + constructor(private readonly store: Pick) { } + fromJSON(json: any): TestSerializableView { - const view = new TestSerializableView(json.name, 50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view = this.store.add(new TestSerializableView(json.name, 50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); this.views.set(json.name, view); return view; } @@ -508,6 +514,7 @@ function nodesToNames(node: GridNode): any { suite('SerializableGrid', function () { + const store = ensureNoDisposablesAreLeakedInTestSuite(); let container: HTMLElement; setup(function () { @@ -518,8 +525,8 @@ suite('SerializableGrid', function () { }); test('serialize empty', function () { - const view1 = new TestSerializableView('view1', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE); - const grid = new SerializableGrid(view1); + const view1 = store.add(new TestSerializableView('view1', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); + const grid = store.add(new SerializableGrid(view1)); container.appendChild(grid.element); grid.layout(800, 600); @@ -545,21 +552,21 @@ suite('SerializableGrid', function () { }); test('serialize simple layout', function () { - const view1 = new TestSerializableView('view1', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE); - const grid = new SerializableGrid(view1); + const view1 = store.add(new TestSerializableView('view1', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); + const grid = store.add(new SerializableGrid(view1)); container.appendChild(grid.element); grid.layout(800, 600); - const view2 = new TestSerializableView('view2', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view2 = store.add(new TestSerializableView('view2', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view2, 200, view1, Direction.Up); - const view3 = new TestSerializableView('view3', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view3 = store.add(new TestSerializableView('view3', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view3, 200, view1, Direction.Right); - const view4 = new TestSerializableView('view4', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view4 = store.add(new TestSerializableView('view4', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view4, 200, view2, Direction.Left); - const view5 = new TestSerializableView('view5', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view5 = store.add(new TestSerializableView('view5', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view5, 100, view1, Direction.Down); assert.deepStrictEqual(grid.serialize(), { @@ -599,45 +606,45 @@ suite('SerializableGrid', function () { }); test('deserialize empty', function () { - const view1 = new TestSerializableView('view1', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE); - const grid = new SerializableGrid(view1); + const view1 = store.add(new TestSerializableView('view1', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); + const grid = store.add(new SerializableGrid(view1)); container.appendChild(grid.element); grid.layout(800, 600); const json = grid.serialize(); grid.dispose(); - const deserializer = new TestViewDeserializer(); - const grid2 = SerializableGrid.deserialize(json, deserializer); + const deserializer = new TestViewDeserializer(store); + const grid2 = store.add(SerializableGrid.deserialize(json, deserializer)); grid2.layout(800, 600); assert.deepStrictEqual(nodesToNames(grid2.getViews()), ['view1']); }); test('deserialize simple layout', function () { - const view1 = new TestSerializableView('view1', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE); - const grid = new SerializableGrid(view1); + const view1 = store.add(new TestSerializableView('view1', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); + const grid = store.add(new SerializableGrid(view1)); container.appendChild(grid.element); grid.layout(800, 600); - const view2 = new TestSerializableView('view2', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view2 = store.add(new TestSerializableView('view2', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view2, 200, view1, Direction.Up); - const view3 = new TestSerializableView('view3', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view3 = store.add(new TestSerializableView('view3', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view3, 200, view1, Direction.Right); - const view4 = new TestSerializableView('view4', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view4 = store.add(new TestSerializableView('view4', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view4, 200, view2, Direction.Left); - const view5 = new TestSerializableView('view5', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view5 = store.add(new TestSerializableView('view5', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view5, 100, view1, Direction.Down); const json = grid.serialize(); grid.dispose(); - const deserializer = new TestViewDeserializer(); - const grid2 = SerializableGrid.deserialize(json, deserializer); + const deserializer = new TestViewDeserializer(store); + const grid2 = store.add(SerializableGrid.deserialize(json, deserializer)); const view1Copy = deserializer.getView('view1'); const view2Copy = deserializer.getView('view2'); @@ -657,29 +664,29 @@ suite('SerializableGrid', function () { }); test('deserialize simple layout with scaling', function () { - const view1 = new TestSerializableView('view1', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE); - const grid = new SerializableGrid(view1); + const view1 = store.add(new TestSerializableView('view1', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); + const grid = store.add(new SerializableGrid(view1)); container.appendChild(grid.element); grid.layout(800, 600); - const view2 = new TestSerializableView('view2', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view2 = store.add(new TestSerializableView('view2', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view2, 200, view1, Direction.Up); - const view3 = new TestSerializableView('view3', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view3 = store.add(new TestSerializableView('view3', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view3, 200, view1, Direction.Right); - const view4 = new TestSerializableView('view4', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view4 = store.add(new TestSerializableView('view4', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view4, 200, view2, Direction.Left); - const view5 = new TestSerializableView('view5', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view5 = store.add(new TestSerializableView('view5', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view5, 100, view1, Direction.Down); const json = grid.serialize(); grid.dispose(); - const deserializer = new TestViewDeserializer(); - const grid2 = SerializableGrid.deserialize(json, deserializer); + const deserializer = new TestViewDeserializer(store); + const grid2 = store.add(SerializableGrid.deserialize(json, deserializer)); const view1Copy = deserializer.getView('view1'); const view2Copy = deserializer.getView('view2'); @@ -696,25 +703,25 @@ suite('SerializableGrid', function () { }); test('deserialize 4 view layout (ben issue #2)', function () { - const view1 = new TestSerializableView('view1', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE); - const grid = new SerializableGrid(view1); + const view1 = store.add(new TestSerializableView('view1', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); + const grid = store.add(new SerializableGrid(view1)); container.appendChild(grid.element); grid.layout(800, 600); - const view2 = new TestSerializableView('view2', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view2 = store.add(new TestSerializableView('view2', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view2, Sizing.Split, view1, Direction.Down); - const view3 = new TestSerializableView('view3', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view3 = store.add(new TestSerializableView('view3', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view3, Sizing.Split, view2, Direction.Down); - const view4 = new TestSerializableView('view4', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view4 = store.add(new TestSerializableView('view4', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view4, Sizing.Split, view3, Direction.Right); const json = grid.serialize(); grid.dispose(); - const deserializer = new TestViewDeserializer(); - const grid2 = SerializableGrid.deserialize(json, deserializer); + const deserializer = new TestViewDeserializer(store); + const grid2 = store.add(SerializableGrid.deserialize(json, deserializer)); const view1Copy = deserializer.getView('view1'); const view2Copy = deserializer.getView('view2'); @@ -730,20 +737,20 @@ suite('SerializableGrid', function () { }); test('deserialize 2 view layout (ben issue #3)', function () { - const view1 = new TestSerializableView('view1', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE); - const grid = new SerializableGrid(view1); + const view1 = store.add(new TestSerializableView('view1', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); + const grid = store.add(new SerializableGrid(view1)); container.appendChild(grid.element); grid.layout(800, 600); - const view2 = new TestSerializableView('view2', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view2 = store.add(new TestSerializableView('view2', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view2, Sizing.Split, view1, Direction.Right); const json = grid.serialize(); grid.dispose(); - const deserializer = new TestViewDeserializer(); - const grid2 = SerializableGrid.deserialize(json, deserializer); + const deserializer = new TestViewDeserializer(store); + const grid2 = store.add(SerializableGrid.deserialize(json, deserializer)); const view1Copy = deserializer.getView('view1'); const view2Copy = deserializer.getView('view2'); @@ -755,16 +762,16 @@ suite('SerializableGrid', function () { }); test('deserialize simple view layout #50609', function () { - const view1 = new TestSerializableView('view1', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE); - const grid = new SerializableGrid(view1); + const view1 = store.add(new TestSerializableView('view1', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); + const grid = store.add(new SerializableGrid(view1)); container.appendChild(grid.element); grid.layout(800, 600); - const view2 = new TestSerializableView('view2', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view2 = store.add(new TestSerializableView('view2', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view2, Sizing.Split, view1, Direction.Right); - const view3 = new TestSerializableView('view3', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view3 = store.add(new TestSerializableView('view3', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view3, Sizing.Split, view2, Direction.Down); grid.removeView(view1, Sizing.Split); @@ -772,8 +779,8 @@ suite('SerializableGrid', function () { const json = grid.serialize(); grid.dispose(); - const deserializer = new TestViewDeserializer(); - const grid2 = SerializableGrid.deserialize(json, deserializer); + const deserializer = new TestViewDeserializer(store); + const grid2 = store.add(SerializableGrid.deserialize(json, deserializer)); const view2Copy = deserializer.getView('view2'); const view3Copy = deserializer.getView('view3'); @@ -835,7 +842,7 @@ suite('SerializableGrid', function () { } }; - const grid = SerializableGrid.deserialize(serializedGrid, deserializer); + const grid = store.add(SerializableGrid.deserialize(serializedGrid, deserializer)); assert.strictEqual(views.length, 3); // should not throw @@ -867,21 +874,21 @@ suite('SerializableGrid', function () { }); test('serialize should store visibility and previous size', function () { - const view1 = new TestSerializableView('view1', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE); - const grid = new SerializableGrid(view1); + const view1 = store.add(new TestSerializableView('view1', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); + const grid = store.add(new SerializableGrid(view1)); container.appendChild(grid.element); grid.layout(800, 600); - const view2 = new TestSerializableView('view2', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view2 = store.add(new TestSerializableView('view2', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view2, 200, view1, Direction.Up); - const view3 = new TestSerializableView('view3', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view3 = store.add(new TestSerializableView('view3', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view3, 200, view1, Direction.Right); - const view4 = new TestSerializableView('view4', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view4 = store.add(new TestSerializableView('view4', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view4, 200, view2, Direction.Left); - const view5 = new TestSerializableView('view5', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view5 = store.add(new TestSerializableView('view5', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view5, 100, view1, Direction.Down); assert.deepStrictEqual(view1.size, [600, 300]); @@ -954,8 +961,8 @@ suite('SerializableGrid', function () { grid.dispose(); - const deserializer = new TestViewDeserializer(); - const grid2 = SerializableGrid.deserialize(json, deserializer); + const deserializer = new TestViewDeserializer(store); + const grid2 = store.add(SerializableGrid.deserialize(json, deserializer)); const view1Copy = deserializer.getView('view1'); const view2Copy = deserializer.getView('view2'); @@ -994,21 +1001,21 @@ suite('SerializableGrid', function () { }); test('serialize should store visibility and previous size even for first leaf', function () { - const view1 = new TestSerializableView('view1', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE); - const grid = new SerializableGrid(view1); + const view1 = store.add(new TestSerializableView('view1', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); + const grid = store.add(new SerializableGrid(view1)); container.appendChild(grid.element); grid.layout(800, 600); - const view2 = new TestSerializableView('view2', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view2 = store.add(new TestSerializableView('view2', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view2, 200, view1, Direction.Up); - const view3 = new TestSerializableView('view3', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view3 = store.add(new TestSerializableView('view3', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view3, 200, view1, Direction.Right); - const view4 = new TestSerializableView('view4', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view4 = store.add(new TestSerializableView('view4', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view4, 200, view2, Direction.Left); - const view5 = new TestSerializableView('view5', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE); + const view5 = store.add(new TestSerializableView('view5', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE)); grid.addView(view5, 100, view1, Direction.Down); assert.deepStrictEqual(view1.size, [600, 300]); @@ -1063,8 +1070,8 @@ suite('SerializableGrid', function () { grid.dispose(); - const deserializer = new TestViewDeserializer(); - const grid2 = SerializableGrid.deserialize(json, deserializer); + const deserializer = new TestViewDeserializer(store); + const grid2 = store.add(SerializableGrid.deserialize(json, deserializer)); const view1Copy = deserializer.getView('view1'); const view2Copy = deserializer.getView('view2'); diff --git a/src/vs/base/test/browser/ui/grid/gridview.test.ts b/src/vs/base/test/browser/ui/grid/gridview.test.ts index fbe6fd47b58..e9be1b00e16 100644 --- a/src/vs/base/test/browser/ui/grid/gridview.test.ts +++ b/src/vs/base/test/browser/ui/grid/gridview.test.ts @@ -7,36 +7,41 @@ import * as assert from 'assert'; import { $ } from 'vs/base/browser/dom'; import { GridView, IView, Orientation, Sizing } from 'vs/base/browser/ui/grid/gridview'; import { nodesToArrays, TestView } from './util'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('Gridview', function () { - let gridview: GridView; - setup(function () { - gridview = new GridView(); + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + function createGridView(): GridView { + const gridview = store.add(new GridView()); const container = $('.container'); container.style.position = 'absolute'; container.style.width = `${200}px`; container.style.height = `${200}px`; container.appendChild(gridview.element); - }); + + return gridview; + } test('empty gridview is empty', function () { + const gridview = createGridView(); assert.deepStrictEqual(nodesToArrays(gridview.getView()), []); - gridview.dispose(); }); test('gridview addView', function () { + const gridview = createGridView(); - const view = new TestView(20, 20, 20, 20); + const view = store.add(new TestView(20, 20, 20, 20)); assert.throws(() => gridview.addView(view, 200, []), 'empty location'); assert.throws(() => gridview.addView(view, 200, [1]), 'index overflow'); assert.throws(() => gridview.addView(view, 200, [0, 0]), 'hierarchy overflow'); const views = [ - new TestView(20, 20, 20, 20), - new TestView(20, 20, 20, 20), - new TestView(20, 20, 20, 20) + store.add(new TestView(20, 20, 20, 20)), + store.add(new TestView(20, 20, 20, 20)), + store.add(new TestView(20, 20, 20, 20)) ]; gridview.addView(views[0], 200, [0]); @@ -44,17 +49,16 @@ suite('Gridview', function () { gridview.addView(views[2], 200, [2]); assert.deepStrictEqual(nodesToArrays(gridview.getView()), views); - - gridview.dispose(); }); test('gridview addView nested', function () { + const gridview = createGridView(); const views = [ - new TestView(20, 20, 20, 20), + store.add(new TestView(20, 20, 20, 20)), [ - new TestView(20, 20, 20, 20), - new TestView(20, 20, 20, 20) + store.add(new TestView(20, 20, 20, 20)), + store.add(new TestView(20, 20, 20, 20)) ] ]; @@ -63,63 +67,61 @@ suite('Gridview', function () { gridview.addView((views[1] as TestView[])[1] as IView, 200, [1, 1]); assert.deepStrictEqual(nodesToArrays(gridview.getView()), views); - - gridview.dispose(); }); test('gridview addView deep nested', function () { + const gridview = createGridView(); - const view1 = new TestView(20, 20, 20, 20); + const view1 = store.add(new TestView(20, 20, 20, 20)); gridview.addView(view1 as IView, 200, [0]); assert.deepStrictEqual(nodesToArrays(gridview.getView()), [view1]); - const view2 = new TestView(20, 20, 20, 20); + const view2 = store.add(new TestView(20, 20, 20, 20)); gridview.addView(view2 as IView, 200, [1]); assert.deepStrictEqual(nodesToArrays(gridview.getView()), [view1, view2]); - const view3 = new TestView(20, 20, 20, 20); + const view3 = store.add(new TestView(20, 20, 20, 20)); gridview.addView(view3 as IView, 200, [1, 0]); assert.deepStrictEqual(nodesToArrays(gridview.getView()), [view1, [view3, view2]]); - const view4 = new TestView(20, 20, 20, 20); + const view4 = store.add(new TestView(20, 20, 20, 20)); gridview.addView(view4 as IView, 200, [1, 0, 0]); assert.deepStrictEqual(nodesToArrays(gridview.getView()), [view1, [[view4, view3], view2]]); - const view5 = new TestView(20, 20, 20, 20); + const view5 = store.add(new TestView(20, 20, 20, 20)); gridview.addView(view5 as IView, 200, [1, 0]); assert.deepStrictEqual(nodesToArrays(gridview.getView()), [view1, [view5, [view4, view3], view2]]); - const view6 = new TestView(20, 20, 20, 20); + const view6 = store.add(new TestView(20, 20, 20, 20)); gridview.addView(view6 as IView, 200, [2]); assert.deepStrictEqual(nodesToArrays(gridview.getView()), [view1, [view5, [view4, view3], view2], view6]); - const view7 = new TestView(20, 20, 20, 20); + const view7 = store.add(new TestView(20, 20, 20, 20)); gridview.addView(view7 as IView, 200, [1, 1]); assert.deepStrictEqual(nodesToArrays(gridview.getView()), [view1, [view5, view7, [view4, view3], view2], view6]); - const view8 = new TestView(20, 20, 20, 20); + const view8 = store.add(new TestView(20, 20, 20, 20)); gridview.addView(view8 as IView, 200, [1, 1, 0]); assert.deepStrictEqual(nodesToArrays(gridview.getView()), [view1, [view5, [view8, view7], [view4, view3], view2], view6]); - - gridview.dispose(); }); test('simple layout', function () { + const gridview = createGridView(); gridview.layout(800, 600); - const view1 = new TestView(50, Number.POSITIVE_INFINITY, 50, Number.POSITIVE_INFINITY); + const view1 = store.add(new TestView(50, Number.POSITIVE_INFINITY, 50, Number.POSITIVE_INFINITY)); gridview.addView(view1, 200, [0]); assert.deepStrictEqual(view1.size, [800, 600]); assert.deepStrictEqual(gridview.getViewSize([0]), { width: 800, height: 600 }); - const view2 = new TestView(50, Number.POSITIVE_INFINITY, 50, Number.POSITIVE_INFINITY); + const view2 = store.add(new TestView(50, Number.POSITIVE_INFINITY, 50, Number.POSITIVE_INFINITY)); gridview.addView(view2, 200, [0]); assert.deepStrictEqual(view1.size, [800, 400]); assert.deepStrictEqual(gridview.getViewSize([1]), { width: 800, height: 400 }); assert.deepStrictEqual(view2.size, [800, 200]); assert.deepStrictEqual(gridview.getViewSize([0]), { width: 800, height: 200 }); - const view3 = new TestView(50, Number.POSITIVE_INFINITY, 50, Number.POSITIVE_INFINITY); + const view3 = store.add(new TestView(50, Number.POSITIVE_INFINITY, 50, Number.POSITIVE_INFINITY)); gridview.addView(view3, 200, [1, 1]); assert.deepStrictEqual(view1.size, [600, 400]); assert.deepStrictEqual(gridview.getViewSize([1, 0]), { width: 600, height: 400 }); @@ -128,7 +130,7 @@ suite('Gridview', function () { assert.deepStrictEqual(view3.size, [200, 400]); assert.deepStrictEqual(gridview.getViewSize([1, 1]), { width: 200, height: 400 }); - const view4 = new TestView(50, Number.POSITIVE_INFINITY, 50, Number.POSITIVE_INFINITY); + const view4 = store.add(new TestView(50, Number.POSITIVE_INFINITY, 50, Number.POSITIVE_INFINITY)); gridview.addView(view4, 200, [0, 0]); assert.deepStrictEqual(view1.size, [600, 400]); assert.deepStrictEqual(gridview.getViewSize([1, 0]), { width: 600, height: 400 }); @@ -139,7 +141,7 @@ suite('Gridview', function () { assert.deepStrictEqual(view4.size, [200, 200]); assert.deepStrictEqual(gridview.getViewSize([0, 0]), { width: 200, height: 200 }); - const view5 = new TestView(50, Number.POSITIVE_INFINITY, 50, Number.POSITIVE_INFINITY); + const view5 = store.add(new TestView(50, Number.POSITIVE_INFINITY, 50, Number.POSITIVE_INFINITY)); gridview.addView(view5, 100, [1, 0, 1]); assert.deepStrictEqual(view1.size, [600, 300]); assert.deepStrictEqual(gridview.getViewSize([1, 0, 0]), { width: 600, height: 300 }); @@ -154,32 +156,33 @@ suite('Gridview', function () { }); test('simple layout with automatic size distribution', function () { + const gridview = createGridView(); gridview.layout(800, 600); - const view1 = new TestView(50, Number.POSITIVE_INFINITY, 50, Number.POSITIVE_INFINITY); + const view1 = store.add(new TestView(50, Number.POSITIVE_INFINITY, 50, Number.POSITIVE_INFINITY)); gridview.addView(view1, Sizing.Distribute, [0]); assert.deepStrictEqual(view1.size, [800, 600]); assert.deepStrictEqual(gridview.getViewSize([0]), { width: 800, height: 600 }); - const view2 = new TestView(50, Number.POSITIVE_INFINITY, 50, Number.POSITIVE_INFINITY); + const view2 = store.add(new TestView(50, Number.POSITIVE_INFINITY, 50, Number.POSITIVE_INFINITY)); gridview.addView(view2, Sizing.Distribute, [0]); assert.deepStrictEqual(view1.size, [800, 300]); assert.deepStrictEqual(view2.size, [800, 300]); - const view3 = new TestView(50, Number.POSITIVE_INFINITY, 50, Number.POSITIVE_INFINITY); + const view3 = store.add(new TestView(50, Number.POSITIVE_INFINITY, 50, Number.POSITIVE_INFINITY)); gridview.addView(view3, Sizing.Distribute, [1, 1]); assert.deepStrictEqual(view1.size, [400, 300]); assert.deepStrictEqual(view2.size, [800, 300]); assert.deepStrictEqual(view3.size, [400, 300]); - const view4 = new TestView(50, Number.POSITIVE_INFINITY, 50, Number.POSITIVE_INFINITY); + const view4 = store.add(new TestView(50, Number.POSITIVE_INFINITY, 50, Number.POSITIVE_INFINITY)); gridview.addView(view4, Sizing.Distribute, [0, 0]); assert.deepStrictEqual(view1.size, [400, 300]); assert.deepStrictEqual(view2.size, [400, 300]); assert.deepStrictEqual(view3.size, [400, 300]); assert.deepStrictEqual(view4.size, [400, 300]); - const view5 = new TestView(50, Number.POSITIVE_INFINITY, 50, Number.POSITIVE_INFINITY); + const view5 = store.add(new TestView(50, Number.POSITIVE_INFINITY, 50, Number.POSITIVE_INFINITY)); gridview.addView(view5, Sizing.Distribute, [1, 0, 1]); assert.deepStrictEqual(view1.size, [400, 150]); assert.deepStrictEqual(view2.size, [400, 300]); @@ -189,14 +192,15 @@ suite('Gridview', function () { }); test('addviews before layout call 1', function () { + const gridview = createGridView(); - const view1 = new TestView(50, Number.POSITIVE_INFINITY, 50, Number.POSITIVE_INFINITY); + const view1 = store.add(new TestView(50, Number.POSITIVE_INFINITY, 50, Number.POSITIVE_INFINITY)); gridview.addView(view1, 200, [0]); - const view2 = new TestView(50, Number.POSITIVE_INFINITY, 50, Number.POSITIVE_INFINITY); + const view2 = store.add(new TestView(50, Number.POSITIVE_INFINITY, 50, Number.POSITIVE_INFINITY)); gridview.addView(view2, 200, [0]); - const view3 = new TestView(50, Number.POSITIVE_INFINITY, 50, Number.POSITIVE_INFINITY); + const view3 = store.add(new TestView(50, Number.POSITIVE_INFINITY, 50, Number.POSITIVE_INFINITY)); gridview.addView(view3, 200, [1, 1]); gridview.layout(800, 600); @@ -207,13 +211,14 @@ suite('Gridview', function () { }); test('addviews before layout call 2', function () { - const view1 = new TestView(50, Number.POSITIVE_INFINITY, 50, Number.POSITIVE_INFINITY); + const gridview = createGridView(); + const view1 = store.add(new TestView(50, Number.POSITIVE_INFINITY, 50, Number.POSITIVE_INFINITY)); gridview.addView(view1, 200, [0]); - const view2 = new TestView(50, Number.POSITIVE_INFINITY, 50, Number.POSITIVE_INFINITY); + const view2 = store.add(new TestView(50, Number.POSITIVE_INFINITY, 50, Number.POSITIVE_INFINITY)); gridview.addView(view2, 200, [0]); - const view3 = new TestView(50, Number.POSITIVE_INFINITY, 50, Number.POSITIVE_INFINITY); + const view3 = store.add(new TestView(50, Number.POSITIVE_INFINITY, 50, Number.POSITIVE_INFINITY)); gridview.addView(view3, 200, [0, 0]); gridview.layout(800, 600); @@ -224,10 +229,11 @@ suite('Gridview', function () { }); test('flipping orientation should preserve absolute offsets', function () { - const view1 = new TestView(50, Number.POSITIVE_INFINITY, 50, Number.POSITIVE_INFINITY); + const gridview = createGridView(); + const view1 = store.add(new TestView(50, Number.POSITIVE_INFINITY, 50, Number.POSITIVE_INFINITY)); gridview.addView(view1, 200, [0]); - const view2 = new TestView(50, Number.POSITIVE_INFINITY, 50, Number.POSITIVE_INFINITY); + const view2 = store.add(new TestView(50, Number.POSITIVE_INFINITY, 50, Number.POSITIVE_INFINITY)); gridview.addView(view2, 200, [1]); gridview.layout(800, 600, 100, 200); diff --git a/src/vs/base/test/browser/ui/list/listView.test.ts b/src/vs/base/test/browser/ui/list/listView.test.ts index da3ee3e7913..7dcab8e7d25 100644 --- a/src/vs/base/test/browser/ui/list/listView.test.ts +++ b/src/vs/base/test/browser/ui/list/listView.test.ts @@ -7,8 +7,11 @@ import * as assert from 'assert'; import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { ListView } from 'vs/base/browser/ui/list/listView'; import { range } from 'vs/base/common/arrays'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('ListView', function () { + ensureNoDisposablesAreLeakedInTestSuite(); + test('all rows get disposed', function () { const element = document.createElement('div'); element.style.height = '200px'; diff --git a/src/vs/base/test/browser/ui/list/listWidget.test.ts b/src/vs/base/test/browser/ui/list/listWidget.test.ts index bb533961b2d..28fd9524341 100644 --- a/src/vs/base/test/browser/ui/list/listWidget.test.ts +++ b/src/vs/base/test/browser/ui/list/listWidget.test.ts @@ -8,8 +8,11 @@ import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/lis import { List } from 'vs/base/browser/ui/list/listWidget'; import { range } from 'vs/base/common/arrays'; import { timeout } from 'vs/base/common/async'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('ListWidget', function () { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + test('Page up and down', async function () { const element = document.createElement('div'); element.style.height = '200px'; @@ -29,7 +32,7 @@ suite('ListWidget', function () { disposeTemplate() { templatesCount--; } }; - const listWidget = new List('test', element, delegate, [renderer]); + const listWidget = store.add(new List('test', element, delegate, [renderer])); listWidget.layout(200); assert.strictEqual(templatesCount, 0, 'no templates have been allocated'); @@ -51,8 +54,6 @@ suite('ListWidget', function () { listWidget.focusPreviousPage(); await timeout(0); assert.strictEqual(listWidget.getFocus()[0], 0, 'page down to previous page'); - - listWidget.dispose(); }); test('Page up and down with item taller than viewport #149502', async function () { @@ -74,7 +75,7 @@ suite('ListWidget', function () { disposeTemplate() { templatesCount--; } }; - const listWidget = new List('test', element, delegate, [renderer]); + const listWidget = store.add(new List('test', element, delegate, [renderer])); listWidget.layout(200); assert.strictEqual(templatesCount, 0, 'no templates have been allocated'); @@ -91,7 +92,5 @@ suite('ListWidget', function () { listWidget.focusPreviousPage(); await timeout(0); assert.strictEqual(listWidget.getFocus()[0], 0, 'page up to next page'); - - listWidget.dispose(); }); }); diff --git a/src/vs/base/test/browser/ui/list/rangeMap.test.ts b/src/vs/base/test/browser/ui/list/rangeMap.test.ts index 518b250a943..0171250324b 100644 --- a/src/vs/base/test/browser/ui/list/rangeMap.test.ts +++ b/src/vs/base/test/browser/ui/list/rangeMap.test.ts @@ -6,13 +6,11 @@ import * as assert from 'assert'; import { consolidate, groupIntersect, RangeMap } from 'vs/base/browser/ui/list/rangeMap'; import { Range } from 'vs/base/common/range'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('RangeMap', () => { - let rangeMap: RangeMap; - setup(() => { - rangeMap = new RangeMap(); - }); + ensureNoDisposablesAreLeakedInTestSuite(); test('intersection', () => { assert.deepStrictEqual(Range.intersect({ start: 0, end: 0 }, { start: 0, end: 0 }), { start: 0, end: 0 }); @@ -141,6 +139,7 @@ suite('RangeMap', () => { }); test('empty', () => { + const rangeMap = new RangeMap(); assert.strictEqual(rangeMap.size, 0); assert.strictEqual(rangeMap.count, 0); }); @@ -152,30 +151,35 @@ suite('RangeMap', () => { const ten = { size: 10 }; test('length & count', () => { + const rangeMap = new RangeMap(); rangeMap.splice(0, 0, [one]); assert.strictEqual(rangeMap.size, 1); assert.strictEqual(rangeMap.count, 1); }); test('length & count #2', () => { + const rangeMap = new RangeMap(); rangeMap.splice(0, 0, [one, one, one, one, one]); assert.strictEqual(rangeMap.size, 5); assert.strictEqual(rangeMap.count, 5); }); test('length & count #3', () => { + const rangeMap = new RangeMap(); rangeMap.splice(0, 0, [five]); assert.strictEqual(rangeMap.size, 5); assert.strictEqual(rangeMap.count, 1); }); test('length & count #4', () => { + const rangeMap = new RangeMap(); rangeMap.splice(0, 0, [five, five, five, five, five]); assert.strictEqual(rangeMap.size, 25); assert.strictEqual(rangeMap.count, 5); }); test('insert', () => { + const rangeMap = new RangeMap(); rangeMap.splice(0, 0, [five, five, five, five, five]); assert.strictEqual(rangeMap.size, 25); assert.strictEqual(rangeMap.count, 5); @@ -194,6 +198,7 @@ suite('RangeMap', () => { }); test('delete', () => { + const rangeMap = new RangeMap(); rangeMap.splice(0, 0, [five, five, five, five, five, five, five, five, five, five, five, five, five, five, five, @@ -219,6 +224,7 @@ suite('RangeMap', () => { }); test('insert & delete', () => { + const rangeMap = new RangeMap(); assert.strictEqual(rangeMap.size, 0); assert.strictEqual(rangeMap.count, 0); @@ -232,6 +238,7 @@ suite('RangeMap', () => { }); test('insert & delete #2', () => { + const rangeMap = new RangeMap(); rangeMap.splice(0, 0, [one, one, one, one, one, one, one, one, one, one]); rangeMap.splice(2, 6); @@ -240,6 +247,7 @@ suite('RangeMap', () => { }); test('insert & delete #3', () => { + const rangeMap = new RangeMap(); rangeMap.splice(0, 0, [one, one, one, one, one, one, one, one, one, one, two, two, two, two, two, @@ -249,7 +257,8 @@ suite('RangeMap', () => { assert.strictEqual(rangeMap.size, 24); }); - test('insert & delete #3', () => { + test('insert & delete #4', () => { + const rangeMap = new RangeMap(); rangeMap.splice(0, 0, [one, one, one, one, one, one, one, one, one, one, two, two, two, two, two, @@ -265,6 +274,7 @@ suite('RangeMap', () => { suite('indexAt, positionAt', () => { test('empty', () => { + const rangeMap = new RangeMap(); assert.strictEqual(rangeMap.indexAt(0), 0); assert.strictEqual(rangeMap.indexAt(10), 0); assert.strictEqual(rangeMap.indexAt(-1), -1); @@ -274,6 +284,7 @@ suite('RangeMap', () => { }); test('simple', () => { + const rangeMap = new RangeMap(); rangeMap.splice(0, 0, [one]); assert.strictEqual(rangeMap.indexAt(0), 0); assert.strictEqual(rangeMap.indexAt(1), 1); @@ -282,6 +293,7 @@ suite('RangeMap', () => { }); test('simple #2', () => { + const rangeMap = new RangeMap(); rangeMap.splice(0, 0, [ten]); assert.strictEqual(rangeMap.indexAt(0), 0); assert.strictEqual(rangeMap.indexAt(5), 0); @@ -292,6 +304,7 @@ suite('RangeMap', () => { }); test('insert', () => { + const rangeMap = new RangeMap(); rangeMap.splice(0, 0, [one, one, one, one, one, one, one, one, one, one]); assert.strictEqual(rangeMap.indexAt(0), 0); assert.strictEqual(rangeMap.indexAt(1), 1); @@ -312,6 +325,7 @@ suite('RangeMap', () => { }); test('delete', () => { + const rangeMap = new RangeMap(); rangeMap.splice(0, 0, [one, one, one, one, one, one, one, one, one, one]); rangeMap.splice(2, 6); @@ -327,6 +341,7 @@ suite('RangeMap', () => { }); test('delete #2', () => { + const rangeMap = new RangeMap(); rangeMap.splice(0, 0, [ten, ten, ten, ten, ten, ten, ten, ten, ten, ten]); rangeMap.splice(2, 6); @@ -345,13 +360,9 @@ suite('RangeMap', () => { }); suite('RangeMap with top padding', () => { - let rangeMap: RangeMap; - - setup(() => { - rangeMap = new RangeMap(10); - }); test('empty', () => { + const rangeMap = new RangeMap(10); assert.strictEqual(rangeMap.size, 10); assert.strictEqual(rangeMap.count, 0); }); @@ -361,30 +372,35 @@ suite('RangeMap with top padding', () => { const ten = { size: 10 }; test('length & count', () => { + const rangeMap = new RangeMap(10); rangeMap.splice(0, 0, [one]); assert.strictEqual(rangeMap.size, 11); assert.strictEqual(rangeMap.count, 1); }); test('length & count #2', () => { + const rangeMap = new RangeMap(10); rangeMap.splice(0, 0, [one, one, one, one, one]); assert.strictEqual(rangeMap.size, 15); assert.strictEqual(rangeMap.count, 5); }); test('length & count #3', () => { + const rangeMap = new RangeMap(10); rangeMap.splice(0, 0, [five]); assert.strictEqual(rangeMap.size, 15); assert.strictEqual(rangeMap.count, 1); }); test('length & count #4', () => { + const rangeMap = new RangeMap(10); rangeMap.splice(0, 0, [five, five, five, five, five]); assert.strictEqual(rangeMap.size, 35); assert.strictEqual(rangeMap.count, 5); }); test('insert', () => { + const rangeMap = new RangeMap(10); rangeMap.splice(0, 0, [five, five, five, five, five]); assert.strictEqual(rangeMap.size, 35); assert.strictEqual(rangeMap.count, 5); @@ -404,6 +420,7 @@ suite('RangeMap with top padding', () => { suite('indexAt, positionAt', () => { test('empty', () => { + const rangeMap = new RangeMap(10); assert.strictEqual(rangeMap.indexAt(0), 0); assert.strictEqual(rangeMap.indexAt(10), 0); assert.strictEqual(rangeMap.indexAt(-1), -1); @@ -413,6 +430,7 @@ suite('RangeMap with top padding', () => { }); test('simple', () => { + const rangeMap = new RangeMap(10); rangeMap.splice(0, 0, [one]); assert.strictEqual(rangeMap.indexAt(0), 0); assert.strictEqual(rangeMap.indexAt(1), 0); diff --git a/src/vs/base/test/browser/ui/splitview/splitview.test.ts b/src/vs/base/test/browser/ui/splitview/splitview.test.ts index 0f23533882c..48ee43fe1b3 100644 --- a/src/vs/base/test/browser/ui/splitview/splitview.test.ts +++ b/src/vs/base/test/browser/ui/splitview/splitview.test.ts @@ -7,6 +7,7 @@ import * as assert from 'assert'; import { Sash, SashState } from 'vs/base/browser/ui/sash/sash'; import { IView, LayoutPriority, Sizing, SplitView } from 'vs/base/browser/ui/splitview/splitview'; import { Emitter } from 'vs/base/common/event'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; class TestView implements IView { @@ -66,6 +67,9 @@ function getSashes(splitview: SplitView): Sash[] { } suite('Splitview', () => { + + const store = ensureNoDisposablesAreLeakedInTestSuite(); + let container: HTMLElement; setup(() => { @@ -76,16 +80,15 @@ suite('Splitview', () => { }); test('empty splitview has empty DOM', () => { - const splitview = new SplitView(container); + store.add(new SplitView(container)); assert.strictEqual(container.firstElementChild!.firstElementChild!.childElementCount, 0, 'split view should be empty'); - splitview.dispose(); }); test('has views and sashes as children', () => { - const view1 = new TestView(20, 20); - const view2 = new TestView(20, 20); - const view3 = new TestView(20, 20); - const splitview = new SplitView(container); + const view1 = store.add(new TestView(20, 20)); + const view2 = store.add(new TestView(20, 20)); + const view3 = store.add(new TestView(20, 20)); + const splitview = store.add(new SplitView(container)); splitview.addView(view1, 20); splitview.addView(view2, 20); @@ -120,37 +123,26 @@ suite('Splitview', () => { sashQuery = container.querySelectorAll('.monaco-split-view2 > .sash-container > .monaco-sash'); assert.strictEqual(sashQuery.length, 0, 'split view should have no sashes'); - - splitview.dispose(); - view1.dispose(); - view2.dispose(); - view3.dispose(); }); test('calls view methods on addView and removeView', () => { - const view = new TestView(20, 20); - const splitview = new SplitView(container); + const view = store.add(new TestView(20, 20)); + const splitview = store.add(new SplitView(container)); let didLayout = false; - const layoutDisposable = view.onDidLayout(() => didLayout = true); - - const renderDisposable = view.onDidGetElement(() => undefined); + store.add(view.onDidLayout(() => didLayout = true)); + store.add(view.onDidGetElement(() => undefined)); splitview.addView(view, 20); assert.strictEqual(view.size, 20, 'view has right size'); assert(didLayout, 'layout is called'); assert(didLayout, 'render is called'); - - splitview.dispose(); - layoutDisposable.dispose(); - renderDisposable.dispose(); - view.dispose(); }); test('stretches view to viewport', () => { - const view = new TestView(20, Number.POSITIVE_INFINITY); - const splitview = new SplitView(container); + const view = store.add(new TestView(20, Number.POSITIVE_INFINITY)); + const splitview = store.add(new SplitView(container)); splitview.layout(200); splitview.addView(view, 20); @@ -170,16 +162,13 @@ suite('Splitview', () => { splitview.layout(200); assert.strictEqual(view.size, 200, 'view is stretched'); - - splitview.dispose(); - view.dispose(); }); test('can resize views', () => { - const view1 = new TestView(20, Number.POSITIVE_INFINITY); - const view2 = new TestView(20, Number.POSITIVE_INFINITY); - const view3 = new TestView(20, Number.POSITIVE_INFINITY); - const splitview = new SplitView(container); + const view1 = store.add(new TestView(20, Number.POSITIVE_INFINITY)); + const view2 = store.add(new TestView(20, Number.POSITIVE_INFINITY)); + const view3 = store.add(new TestView(20, Number.POSITIVE_INFINITY)); + const splitview = store.add(new SplitView(container)); splitview.layout(200); splitview.addView(view1, 20); @@ -207,18 +196,13 @@ suite('Splitview', () => { assert.strictEqual(view1.size, 70, 'view1 stays the same'); assert.strictEqual(view2.size, 90, 'view2 is collapsed'); assert.strictEqual(view3.size, 40, 'view3 is stretched'); - - splitview.dispose(); - view3.dispose(); - view2.dispose(); - view1.dispose(); }); test('reacts to view changes', () => { - const view1 = new TestView(20, Number.POSITIVE_INFINITY); - const view2 = new TestView(20, Number.POSITIVE_INFINITY); - const view3 = new TestView(20, Number.POSITIVE_INFINITY); - const splitview = new SplitView(container); + const view1 = store.add(new TestView(20, Number.POSITIVE_INFINITY)); + const view2 = store.add(new TestView(20, Number.POSITIVE_INFINITY)); + const view3 = store.add(new TestView(20, Number.POSITIVE_INFINITY)); + const splitview = store.add(new SplitView(container)); splitview.layout(200); splitview.addView(view1, 20); @@ -253,18 +237,13 @@ suite('Splitview', () => { assert.strictEqual(view1.size, 20, 'view1 is collapsed'); assert.strictEqual(view2.size, 80, 'view2 is collapsed'); assert.strictEqual(view3.size, 100, 'view3 is stretched'); - - splitview.dispose(); - view3.dispose(); - view2.dispose(); - view1.dispose(); }); test('sashes are properly enabled/disabled', () => { - const view1 = new TestView(20, Number.POSITIVE_INFINITY); - const view2 = new TestView(20, Number.POSITIVE_INFINITY); - const view3 = new TestView(20, Number.POSITIVE_INFINITY); - const splitview = new SplitView(container); + const view1 = store.add(new TestView(20, Number.POSITIVE_INFINITY)); + const view2 = store.add(new TestView(20, Number.POSITIVE_INFINITY)); + const view3 = store.add(new TestView(20, Number.POSITIVE_INFINITY)); + const splitview = store.add(new SplitView(container)); splitview.layout(200); splitview.addView(view1, Sizing.Distribute); @@ -307,27 +286,22 @@ suite('Splitview', () => { splitview.resizeView(0, 40); assert.strictEqual(sashes[0].state, SashState.Enabled, 'first sash is enabled'); assert.strictEqual(sashes[1].state, SashState.Enabled, 'second sash is enabled'); - - splitview.dispose(); - view3.dispose(); - view2.dispose(); - view1.dispose(); }); test('issue #35497', () => { - const view1 = new TestView(160, Number.POSITIVE_INFINITY); - const view2 = new TestView(66, 66); + const view1 = store.add(new TestView(160, Number.POSITIVE_INFINITY)); + const view2 = store.add(new TestView(66, 66)); - const splitview = new SplitView(container); + const splitview = store.add(new SplitView(container)); splitview.layout(986); splitview.addView(view1, 142, 0); assert.strictEqual(view1.size, 986, 'first view is stretched'); - view2.onDidGetElement(() => { + store.add(view2.onDidGetElement(() => { assert.throws(() => splitview.resizeView(1, 922)); assert.throws(() => splitview.resizeView(1, 922)); - }); + })); splitview.addView(view2, 66, 0); assert.strictEqual(view2.size, 66, 'second view is fixed'); @@ -337,17 +311,13 @@ suite('Splitview', () => { assert.strictEqual(viewContainers.length, 2, 'there are two view containers'); assert.strictEqual((viewContainers.item(0) as HTMLElement).style.height, '66px', 'second view container is 66px'); assert.strictEqual((viewContainers.item(1) as HTMLElement).style.height, `${986 - 66}px`, 'first view container is 66px'); - - splitview.dispose(); - view2.dispose(); - view1.dispose(); }); test('automatic size distribution', () => { - const view1 = new TestView(20, Number.POSITIVE_INFINITY); - const view2 = new TestView(20, Number.POSITIVE_INFINITY); - const view3 = new TestView(20, Number.POSITIVE_INFINITY); - const splitview = new SplitView(container); + const view1 = store.add(new TestView(20, Number.POSITIVE_INFINITY)); + const view2 = store.add(new TestView(20, Number.POSITIVE_INFINITY)); + const view3 = store.add(new TestView(20, Number.POSITIVE_INFINITY)); + const splitview = store.add(new SplitView(container)); splitview.layout(200); splitview.addView(view1, Sizing.Distribute); @@ -361,18 +331,13 @@ suite('Splitview', () => { splitview.removeView(1, Sizing.Distribute); assert.deepStrictEqual([view1.size, view3.size], [100, 100]); - - splitview.dispose(); - view3.dispose(); - view2.dispose(); - view1.dispose(); }); test('add views before layout', () => { - const view1 = new TestView(20, Number.POSITIVE_INFINITY); - const view2 = new TestView(20, Number.POSITIVE_INFINITY); - const view3 = new TestView(20, Number.POSITIVE_INFINITY); - const splitview = new SplitView(container); + const view1 = store.add(new TestView(20, Number.POSITIVE_INFINITY)); + const view2 = store.add(new TestView(20, Number.POSITIVE_INFINITY)); + const view3 = store.add(new TestView(20, Number.POSITIVE_INFINITY)); + const splitview = store.add(new SplitView(container)); splitview.addView(view1, 100); splitview.addView(view2, 75); @@ -380,18 +345,13 @@ suite('Splitview', () => { splitview.layout(200); assert.deepStrictEqual([view1.size, view2.size, view3.size], [67, 67, 66]); - - splitview.dispose(); - view3.dispose(); - view2.dispose(); - view1.dispose(); }); test('split sizing', () => { - const view1 = new TestView(20, Number.POSITIVE_INFINITY); - const view2 = new TestView(20, Number.POSITIVE_INFINITY); - const view3 = new TestView(20, Number.POSITIVE_INFINITY); - const splitview = new SplitView(container); + const view1 = store.add(new TestView(20, Number.POSITIVE_INFINITY)); + const view2 = store.add(new TestView(20, Number.POSITIVE_INFINITY)); + const view3 = store.add(new TestView(20, Number.POSITIVE_INFINITY)); + const splitview = store.add(new SplitView(container)); splitview.layout(200); splitview.addView(view1, Sizing.Distribute); @@ -402,18 +362,13 @@ suite('Splitview', () => { splitview.addView(view3, Sizing.Split(1)); assert.deepStrictEqual([view1.size, view2.size, view3.size], [100, 50, 50]); - - splitview.dispose(); - view3.dispose(); - view2.dispose(); - view1.dispose(); }); test('split sizing 2', () => { - const view1 = new TestView(20, Number.POSITIVE_INFINITY); - const view2 = new TestView(20, Number.POSITIVE_INFINITY); - const view3 = new TestView(20, Number.POSITIVE_INFINITY); - const splitview = new SplitView(container); + const view1 = store.add(new TestView(20, Number.POSITIVE_INFINITY)); + const view2 = store.add(new TestView(20, Number.POSITIVE_INFINITY)); + const view3 = store.add(new TestView(20, Number.POSITIVE_INFINITY)); + const splitview = store.add(new SplitView(container)); splitview.layout(200); splitview.addView(view1, Sizing.Distribute); @@ -424,17 +379,12 @@ suite('Splitview', () => { splitview.addView(view3, Sizing.Split(0)); assert.deepStrictEqual([view1.size, view2.size, view3.size], [50, 100, 50]); - - splitview.dispose(); - view3.dispose(); - view2.dispose(); - view1.dispose(); }); test('proportional layout', () => { - const view1 = new TestView(20, Number.POSITIVE_INFINITY); - const view2 = new TestView(20, Number.POSITIVE_INFINITY); - const splitview = new SplitView(container); + const view1 = store.add(new TestView(20, Number.POSITIVE_INFINITY)); + const view2 = store.add(new TestView(20, Number.POSITIVE_INFINITY)); + const splitview = store.add(new SplitView(container)); splitview.layout(200); splitview.addView(view1, Sizing.Distribute); @@ -443,16 +393,12 @@ suite('Splitview', () => { splitview.layout(100); assert.deepStrictEqual([view1.size, view2.size], [50, 50]); - - splitview.dispose(); - view2.dispose(); - view1.dispose(); }); test('disable proportional layout', () => { - const view1 = new TestView(20, Number.POSITIVE_INFINITY); - const view2 = new TestView(20, Number.POSITIVE_INFINITY); - const splitview = new SplitView(container, { proportionalLayout: false }); + const view1 = store.add(new TestView(20, Number.POSITIVE_INFINITY)); + const view2 = store.add(new TestView(20, Number.POSITIVE_INFINITY)); + const splitview = store.add(new SplitView(container, { proportionalLayout: false })); splitview.layout(200); splitview.addView(view1, Sizing.Distribute); @@ -461,17 +407,13 @@ suite('Splitview', () => { splitview.layout(100); assert.deepStrictEqual([view1.size, view2.size], [80, 20]); - - splitview.dispose(); - view2.dispose(); - view1.dispose(); }); test('high layout priority', () => { - const view1 = new TestView(20, Number.POSITIVE_INFINITY); - const view2 = new TestView(20, Number.POSITIVE_INFINITY, LayoutPriority.High); - const view3 = new TestView(20, Number.POSITIVE_INFINITY); - const splitview = new SplitView(container, { proportionalLayout: false }); + const view1 = store.add(new TestView(20, Number.POSITIVE_INFINITY)); + const view2 = store.add(new TestView(20, Number.POSITIVE_INFINITY, LayoutPriority.High)); + const view3 = store.add(new TestView(20, Number.POSITIVE_INFINITY)); + const splitview = store.add(new SplitView(container, { proportionalLayout: false })); splitview.layout(200); splitview.addView(view1, Sizing.Distribute); @@ -490,18 +432,13 @@ suite('Splitview', () => { splitview.layout(200); assert.deepStrictEqual([view1.size, view2.size, view3.size], [20, 160, 20]); - - splitview.dispose(); - view3.dispose(); - view2.dispose(); - view1.dispose(); }); test('low layout priority', () => { - const view1 = new TestView(20, Number.POSITIVE_INFINITY); - const view2 = new TestView(20, Number.POSITIVE_INFINITY); - const view3 = new TestView(20, Number.POSITIVE_INFINITY, LayoutPriority.Low); - const splitview = new SplitView(container, { proportionalLayout: false }); + const view1 = store.add(new TestView(20, Number.POSITIVE_INFINITY)); + const view2 = store.add(new TestView(20, Number.POSITIVE_INFINITY)); + const view3 = store.add(new TestView(20, Number.POSITIVE_INFINITY, LayoutPriority.Low)); + const splitview = store.add(new SplitView(container, { proportionalLayout: false })); splitview.layout(200); splitview.addView(view1, Sizing.Distribute); @@ -520,18 +457,13 @@ suite('Splitview', () => { splitview.layout(200); assert.deepStrictEqual([view1.size, view2.size, view3.size], [20, 160, 20]); - - splitview.dispose(); - view3.dispose(); - view2.dispose(); - view1.dispose(); }); test('context propagates to views', () => { - const view1 = new TestView(20, Number.POSITIVE_INFINITY); - const view2 = new TestView(20, Number.POSITIVE_INFINITY); - const view3 = new TestView(20, Number.POSITIVE_INFINITY, LayoutPriority.Low); - const splitview = new SplitView(container, { proportionalLayout: false }); + const view1 = store.add(new TestView(20, Number.POSITIVE_INFINITY)); + const view2 = store.add(new TestView(20, Number.POSITIVE_INFINITY)); + const view3 = store.add(new TestView(20, Number.POSITIVE_INFINITY, LayoutPriority.Low)); + const splitview = store.add(new SplitView(container, { proportionalLayout: false })); splitview.layout(200); splitview.addView(view1, Sizing.Distribute); @@ -540,10 +472,5 @@ suite('Splitview', () => { splitview.layout(200, 100); assert.deepStrictEqual([view1.orthogonalSize, view2.orthogonalSize, view3.orthogonalSize], [100, 100, 100]); - - splitview.dispose(); - view3.dispose(); - view2.dispose(); - view1.dispose(); }); }); diff --git a/src/vs/base/test/browser/ui/tree/asyncDataTree.test.ts b/src/vs/base/test/browser/ui/tree/asyncDataTree.test.ts index d10d76b0b85..9a552a91b83 100644 --- a/src/vs/base/test/browser/ui/tree/asyncDataTree.test.ts +++ b/src/vs/base/test/browser/ui/tree/asyncDataTree.test.ts @@ -9,6 +9,7 @@ import { AsyncDataTree } from 'vs/base/browser/ui/tree/asyncDataTree'; import { IAsyncDataSource, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; import { timeout } from 'vs/base/common/async'; import { Iterable } from 'vs/base/common/iterator'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; interface Element { id: string; @@ -86,6 +87,8 @@ class Model { suite('AsyncDataTree', function () { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + test('Collapse state should be preserved across refresh calls', async () => { const container = document.createElement('div'); @@ -96,7 +99,7 @@ suite('AsyncDataTree', function () { }] }); - const tree = new AsyncDataTree('test', container, new VirtualDelegate(), [new Renderer()], new DataSource(), { identityProvider: new IdentityProvider() }); + const tree = store.add(new AsyncDataTree('test', container, new VirtualDelegate(), [new Renderer()], new DataSource(), { identityProvider: new IdentityProvider() })); tree.layout(200); assert.strictEqual(container.querySelectorAll('.monaco-list-row').length, 0); @@ -144,7 +147,7 @@ suite('AsyncDataTree', function () { }] }); - const tree = new AsyncDataTree('test', container, new VirtualDelegate(), [new Renderer()], dataSource, { identityProvider: new IdentityProvider() }); + const tree = store.add(new AsyncDataTree('test', container, new VirtualDelegate(), [new Renderer()], dataSource, { identityProvider: new IdentityProvider() })); tree.layout(200); await tree.setInput(model.root); @@ -204,7 +207,7 @@ suite('AsyncDataTree', function () { }] }); - const tree = new AsyncDataTree('test', container, new VirtualDelegate(), [new Renderer()], dataSource, { identityProvider: new IdentityProvider() }); + const tree = store.add(new AsyncDataTree('test', container, new VirtualDelegate(), [new Renderer()], dataSource, { identityProvider: new IdentityProvider() })); tree.layout(200); await tree.setInput(model.root); @@ -234,7 +237,7 @@ suite('AsyncDataTree', function () { }] }); - const tree = new AsyncDataTree('test', container, new VirtualDelegate(), [new Renderer()], new DataSource(), { identityProvider: new IdentityProvider() }); + const tree = store.add(new AsyncDataTree('test', container, new VirtualDelegate(), [new Renderer()], new DataSource(), { identityProvider: new IdentityProvider() })); tree.layout(200); await tree.setInput(model.root); @@ -276,9 +279,9 @@ suite('AsyncDataTree', function () { }] }); - const tree = new AsyncDataTree('test', container, new VirtualDelegate(), [new Renderer()], dataSource, { + const tree = store.add(new AsyncDataTree('test', container, new VirtualDelegate(), [new Renderer()], dataSource, { collapseByDefault: el => el.id !== 'a' - }); + })); tree.layout(200); await tree.setInput(model.root); @@ -308,7 +311,7 @@ suite('AsyncDataTree', function () { }] }); - const tree = new AsyncDataTree('test', container, new VirtualDelegate(), [new Renderer()], dataSource, { identityProvider: new IdentityProvider() }); + const tree = store.add(new AsyncDataTree('test', container, new VirtualDelegate(), [new Renderer()], dataSource, { identityProvider: new IdentityProvider() })); tree.layout(200); const pSetInput = tree.setInput(model.root); @@ -351,7 +354,7 @@ suite('AsyncDataTree', function () { }] }); - const tree = new AsyncDataTree('test', container, new VirtualDelegate(), [new Renderer()], dataSource, { identityProvider: new IdentityProvider() }); + const tree = store.add(new AsyncDataTree('test', container, new VirtualDelegate(), [new Renderer()], dataSource, { identityProvider: new IdentityProvider() })); tree.layout(200); const pSetInput = tree.setInput(model.root); @@ -380,7 +383,7 @@ suite('AsyncDataTree', function () { }] }); - const tree = new AsyncDataTree('test', container, new VirtualDelegate(), [new Renderer()], new DataSource(), { identityProvider: new IdentityProvider() }); + const tree = store.add(new AsyncDataTree('test', container, new VirtualDelegate(), [new Renderer()], new DataSource(), { identityProvider: new IdentityProvider() })); tree.layout(200); await tree.setInput(model.root); @@ -418,7 +421,7 @@ suite('AsyncDataTree', function () { }] }); - const tree = new AsyncDataTree('test', container, new VirtualDelegate(), [new Renderer()], new DataSource(), { identityProvider: new IdentityProvider() }); + const tree = store.add(new AsyncDataTree('test', container, new VirtualDelegate(), [new Renderer()], new DataSource(), { identityProvider: new IdentityProvider() })); tree.layout(200); await tree.setInput(model.root); @@ -461,7 +464,7 @@ suite('AsyncDataTree', function () { }); const a = model.get('a'); - const tree = new AsyncDataTree('test', container, new VirtualDelegate(), [new Renderer()], dataSource, { identityProvider: new IdentityProvider() }); + const tree = store.add(new AsyncDataTree('test', container, new VirtualDelegate(), [new Renderer()], dataSource, { identityProvider: new IdentityProvider() })); tree.layout(200); await tree.setInput(model.root); diff --git a/src/vs/base/test/browser/ui/tree/compressedObjectTreeModel.test.ts b/src/vs/base/test/browser/ui/tree/compressedObjectTreeModel.test.ts index beee230cbad..79de7324a24 100644 --- a/src/vs/base/test/browser/ui/tree/compressedObjectTreeModel.test.ts +++ b/src/vs/base/test/browser/ui/tree/compressedObjectTreeModel.test.ts @@ -9,6 +9,7 @@ import { IList } from 'vs/base/browser/ui/tree/indexTreeModel'; import { IObjectTreeModelSetChildrenOptions } from 'vs/base/browser/ui/tree/objectTreeModel'; import { ITreeNode } from 'vs/base/browser/ui/tree/tree'; import { Iterable } from 'vs/base/common/iterator'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; interface IResolvedCompressedTreeElement extends ICompressedTreeElement { readonly element: T; @@ -32,6 +33,8 @@ function resolve(treeElement: ICompressedTreeElement): IResolvedCompressed suite('CompressedObjectTree', function () { + ensureNoDisposablesAreLeakedInTestSuite(); + suite('compress & decompress', function () { test('small', function () { diff --git a/src/vs/base/test/browser/ui/tree/dataTree.test.ts b/src/vs/base/test/browser/ui/tree/dataTree.test.ts index ab450c682e7..fb821d3b662 100644 --- a/src/vs/base/test/browser/ui/tree/dataTree.test.ts +++ b/src/vs/base/test/browser/ui/tree/dataTree.test.ts @@ -7,6 +7,7 @@ import * as assert from 'assert'; import { IIdentityProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { DataTree } from 'vs/base/browser/ui/tree/dataTree'; import { IDataSource, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; interface E { value: number; @@ -30,6 +31,10 @@ suite('DataTree', function () { children: [] }; + teardown(() => tree.dispose()); + + ensureNoDisposablesAreLeakedInTestSuite(); + setup(() => { const container = document.createElement('div'); container.style.width = '200px'; @@ -67,10 +72,6 @@ suite('DataTree', function () { tree.layout(200); }); - teardown(() => { - tree.dispose(); - }); - test('view state is lost implicitly', () => { tree.setInput(root); diff --git a/src/vs/base/test/browser/ui/tree/indexTreeModel.test.ts b/src/vs/base/test/browser/ui/tree/indexTreeModel.test.ts index 9ae0e08b0f1..008da2a3ea0 100644 --- a/src/vs/base/test/browser/ui/tree/indexTreeModel.test.ts +++ b/src/vs/base/test/browser/ui/tree/indexTreeModel.test.ts @@ -7,6 +7,7 @@ import * as assert from 'assert'; import { IIndexTreeModelSpliceOptions, IIndexTreeNode, IList, IndexTreeModel } from 'vs/base/browser/ui/tree/indexTreeModel'; import { ITreeElement, ITreeFilter, ITreeNode, TreeVisibility } from 'vs/base/browser/ui/tree/tree'; import { timeout } from 'vs/base/common/async'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; function toList(arr: T[]): IList { return { @@ -39,6 +40,8 @@ function withSmartSplice(fn: (options: IIndexTreeModelSpliceOptions suite('IndexTreeModel', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + test('ctor', () => { const list: ITreeNode[] = []; const model = new IndexTreeModel('test', toList(list), -1); diff --git a/src/vs/base/test/browser/ui/tree/objectTree.test.ts b/src/vs/base/test/browser/ui/tree/objectTree.test.ts index f758b089e46..a3e39846a5b 100644 --- a/src/vs/base/test/browser/ui/tree/objectTree.test.ts +++ b/src/vs/base/test/browser/ui/tree/objectTree.test.ts @@ -8,12 +8,21 @@ import { IIdentityProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; import { CompressibleObjectTree, ICompressibleTreeRenderer, ObjectTree } from 'vs/base/browser/ui/tree/objectTree'; import { ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('ObjectTree', function () { + suite('TreeNavigator', function () { let tree: ObjectTree; let filter = (_: number) => true; + teardown(() => { + tree.dispose(); + filter = (_: number) => true; + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + setup(() => { const container = document.createElement('div'); container.style.width = '200px'; @@ -39,11 +48,6 @@ suite('ObjectTree', function () { tree.layout(200); }); - teardown(() => { - tree.dispose(); - filter = (_: number) => true; - }); - test('should be able to navigate', () => { tree.setChildren(null, [ { diff --git a/src/vs/base/test/browser/ui/tree/objectTreeModel.test.ts b/src/vs/base/test/browser/ui/tree/objectTreeModel.test.ts index 2b9917e4ef9..4c895d94130 100644 --- a/src/vs/base/test/browser/ui/tree/objectTreeModel.test.ts +++ b/src/vs/base/test/browser/ui/tree/objectTreeModel.test.ts @@ -8,6 +8,7 @@ import { IList } from 'vs/base/browser/ui/tree/indexTreeModel'; import { ObjectTreeModel } from 'vs/base/browser/ui/tree/objectTreeModel'; import { ITreeFilter, ITreeNode, ObjectTreeElementCollapseState, TreeVisibility } from 'vs/base/browser/ui/tree/tree'; import { timeout } from 'vs/base/common/async'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; function toList(arr: T[]): IList { return { @@ -25,6 +26,8 @@ function toArray(list: ITreeNode[]): T[] { suite('ObjectTreeModel', function () { + ensureNoDisposablesAreLeakedInTestSuite(); + test('ctor', () => { const list: ITreeNode[] = []; const model = new ObjectTreeModel('test', toList(list)); diff --git a/src/vs/base/test/common/async.test.ts b/src/vs/base/test/common/async.test.ts index 467708aa2c2..4cb9eceb360 100644 --- a/src/vs/base/test/common/async.test.ts +++ b/src/vs/base/test/common/async.test.ts @@ -11,14 +11,17 @@ import { isCancellationError } from 'vs/base/common/errors'; import { Event } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('Async', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + suite('cancelablePromise', function () { test('set token, don\'t wait for inner promise', function () { let canceled = 0; const promise = async.createCancelablePromise(token => { - token.onCancellationRequested(_ => { canceled += 1; }); + store.add(token.onCancellationRequested(_ => { canceled += 1; })); return new Promise(resolve => { /*never*/ }); }); const result = promise.then(_ => assert.ok(false), err => { @@ -33,7 +36,7 @@ suite('Async', () => { test('cancel despite inner promise being resolved', function () { let canceled = 0; const promise = async.createCancelablePromise(token => { - token.onCancellationRequested(_ => { canceled += 1; }); + store.add(token.onCancellationRequested(_ => { canceled += 1; })); return Promise.resolve(1234); }); const result = promise.then(_ => assert.ok(false), err => { @@ -51,7 +54,7 @@ suite('Async', () => { const cancellablePromise = async.createCancelablePromise(token => { order.push('in callback'); - token.onCancellationRequested(_ => order.push('cancelled')); + store.add(token.onCancellationRequested(_ => order.push('cancelled'))); return Promise.resolve(1234); }); @@ -73,7 +76,7 @@ suite('Async', () => { const cancellablePromise = async.createCancelablePromise(token => { order.push('in callback'); - token.onCancellationRequested(_ => order.push('cancelled')); + store.add(token.onCancellationRequested(_ => order.push('cancelled'))); return new Promise(c => setTimeout(c.bind(1234), 0)); }); @@ -185,9 +188,14 @@ suite('Async', () => { const promises: Promise[] = []; throttler.dispose(); - assert.throws(() => promises.push(throttler.queue(factory))); - assert.strictEqual(factoryCalls, 0); - await Promise.all(promises); + promises.push(throttler.queue(factory)); + + try { + await Promise.all(promises); + assert.fail('should fail'); + } catch (err) { + assert.strictEqual(factoryCalls, 0); + } }); }); @@ -787,52 +795,64 @@ suite('Async', () => { test('cancel executing', async function () { const sequentializer = new async.TaskSequentializer(); + const ctsTimeout = store.add(new CancellationTokenSource()); let pendingCancelled = false; - sequentializer.run(1, async.timeout(1), () => pendingCancelled = true); + const timeout = async.timeout(1, ctsTimeout.token); + sequentializer.run(1, timeout, () => pendingCancelled = true); sequentializer.cancelRunning(); assert.ok(pendingCancelled); + ctsTimeout.cancel(); }); }); test('raceCancellation', async () => { - const cts = new CancellationTokenSource(); + const cts = store.add(new CancellationTokenSource()); + const ctsTimeout = store.add(new CancellationTokenSource()); let triggered = false; - const p = async.raceCancellation(async.timeout(100).then(() => triggered = true), cts.token); + const timeout = async.timeout(100, ctsTimeout.token); + const p = async.raceCancellation(timeout.then(() => triggered = true), cts.token); cts.cancel(); await p; assert.ok(!triggered); + ctsTimeout.cancel(); }); test('raceTimeout', async () => { - const cts = new CancellationTokenSource(); + const cts = store.add(new CancellationTokenSource()); // timeout wins let timedout = false; let triggered = false; - const p1 = async.raceTimeout(async.timeout(100).then(() => triggered = true), 1, () => timedout = true); + const ctsTimeout1 = store.add(new CancellationTokenSource()); + const timeout1 = async.timeout(100, ctsTimeout1.token); + const p1 = async.raceTimeout(timeout1.then(() => triggered = true), 1, () => timedout = true); cts.cancel(); await p1; assert.ok(!triggered); assert.strictEqual(timedout, true); + ctsTimeout1.cancel(); // promise wins timedout = false; - const p2 = async.raceTimeout(async.timeout(1).then(() => triggered = true), 100, () => timedout = true); + const ctsTimeout2 = store.add(new CancellationTokenSource()); + const timeout2 = async.timeout(1, ctsTimeout2.token); + const p2 = async.raceTimeout(timeout2.then(() => triggered = true), 100, () => timedout = true); cts.cancel(); await p2; assert.ok(triggered); assert.strictEqual(timedout, false); + ctsTimeout2.cancel(); }); test('SequencerByKey', async () => { @@ -1111,11 +1131,11 @@ suite('Async', () => { } }; - const worker = new async.ThrottledWorker({ + const worker = store.add(new async.ThrottledWorker({ maxWorkChunkSize: 5, maxBufferedWork: undefined, throttleDelay: 1 - }, handler); + }, handler)); // Work less than chunk size @@ -1223,11 +1243,11 @@ suite('Async', () => { const handled: number[] = []; const handler = (units: readonly number[]) => handled.push(...units); - const worker = new async.ThrottledWorker({ + const worker = store.add(new async.ThrottledWorker({ maxWorkChunkSize: 5, maxBufferedWork: 5, throttleDelay: 1 - }, handler); + }, handler)); let worked = worker.work([1, 2, 3]); assert.strictEqual(worked, true); @@ -1249,11 +1269,11 @@ suite('Async', () => { const handled: number[] = []; const handler = (units: readonly number[]) => handled.push(...units); - const worker = new async.ThrottledWorker({ + const worker = store.add(new async.ThrottledWorker({ maxWorkChunkSize: 5, maxBufferedWork: 5, throttleDelay: 1 - }, handler); + }, handler)); let worked = worker.work([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); assert.strictEqual(worked, false); @@ -1268,11 +1288,11 @@ suite('Async', () => { const handled: number[] = []; const handler = (units: readonly number[]) => handled.push(...units); - const worker = new async.ThrottledWorker({ + const worker = store.add(new async.ThrottledWorker({ maxWorkChunkSize: 5, maxBufferedWork: undefined, throttleDelay: 1 - }, handler); + }, handler)); worker.dispose(); const worked = worker.work([1, 2, 3]); diff --git a/src/vs/base/test/common/buffer.test.ts b/src/vs/base/test/common/buffer.test.ts index 5a37943b658..6c869a16b3f 100644 --- a/src/vs/base/test/common/buffer.test.ts +++ b/src/vs/base/test/common/buffer.test.ts @@ -7,9 +7,12 @@ import * as assert from 'assert'; import { timeout } from 'vs/base/common/async'; import { bufferedStreamToBuffer, bufferToReadable, bufferToStream, decodeBase64, encodeBase64, newWriteableBufferStream, readableToBuffer, streamToBuffer, VSBuffer } from 'vs/base/common/buffer'; import { peekStream } from 'vs/base/common/stream'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('Buffer', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + test('issue #71993 - VSBuffer#toString returns numbers', () => { const data = new Uint8Array([1, 2, 3, 'h'.charCodeAt(0), 'i'.charCodeAt(0), 4, 5]).buffer; const buffer = VSBuffer.wrap(new Uint8Array(data, 3, 2)); diff --git a/src/vs/base/test/common/cancellation.test.ts b/src/vs/base/test/common/cancellation.test.ts index 5f7206f65fa..2e184c42267 100644 --- a/src/vs/base/test/common/cancellation.test.ts +++ b/src/vs/base/test/common/cancellation.test.ts @@ -4,9 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('CancellationToken', function () { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + test('None', () => { assert.strictEqual(CancellationToken.None.isCancellationRequested, false); assert.strictEqual(typeof CancellationToken.None.onCancellationRequested, 'function'); @@ -35,7 +38,7 @@ suite('CancellationToken', function () { cancelCount += 1; } - source.token.onCancellationRequested(onCancel); + store.add(source.token.onCancellationRequested(onCancel)); source.cancel(); source.cancel(); @@ -48,15 +51,9 @@ suite('CancellationToken', function () { let count = 0; const source = new CancellationTokenSource(); - source.token.onCancellationRequested(function () { - count += 1; - }); - source.token.onCancellationRequested(function () { - count += 1; - }); - source.token.onCancellationRequested(function () { - count += 1; - }); + store.add(source.token.onCancellationRequested(() => count++)); + store.add(source.token.onCancellationRequested(() => count++)); + store.add(source.token.onCancellationRequested(() => count++)); source.cancel(); assert.strictEqual(count, 3); @@ -85,9 +82,7 @@ suite('CancellationToken', function () { let count = 0; const source = new CancellationTokenSource(); - source.token.onCancellationRequested(function () { - count += 1; - }); + store.add(source.token.onCancellationRequested(() => count++)); source.dispose(); source.cancel(); @@ -99,9 +94,7 @@ suite('CancellationToken', function () { let count = 0; const source = new CancellationTokenSource(); - source.token.onCancellationRequested(function () { - count += 1; - }); + store.add(source.token.onCancellationRequested(() => count++)); source.dispose(true); // source.cancel(); @@ -120,12 +113,15 @@ suite('CancellationToken', function () { const child = new CancellationTokenSource(parent.token); let count = 0; - child.token.onCancellationRequested(() => count += 1); + store.add(child.token.onCancellationRequested(() => count++)); parent.cancel(); assert.strictEqual(count, 1); assert.strictEqual(child.token.isCancellationRequested, true); assert.strictEqual(parent.token.isCancellationRequested, true); + + child.dispose(); + parent.dispose(); }); }); diff --git a/src/vs/base/test/common/date.test.ts b/src/vs/base/test/common/date.test.ts index 5826f6d7e55..331c7f0e001 100644 --- a/src/vs/base/test/common/date.test.ts +++ b/src/vs/base/test/common/date.test.ts @@ -5,8 +5,11 @@ import { strictEqual } from 'assert'; import { fromNow } from 'vs/base/common/date'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('Date', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + suite('fromNow', () => { test('appendAgoLabel', () => { strictEqual(fromNow(Date.now() - 35000), '35 secs'); diff --git a/src/vs/base/test/common/extpath.test.ts b/src/vs/base/test/common/extpath.test.ts index 1c37ca687b6..3c6a4e4979b 100644 --- a/src/vs/base/test/common/extpath.test.ts +++ b/src/vs/base/test/common/extpath.test.ts @@ -7,6 +7,7 @@ import * as assert from 'assert'; import { CharCode } from 'vs/base/common/charCode'; import * as extpath from 'vs/base/common/extpath'; import { isWindows } from 'vs/base/common/platform'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('Paths', () => { @@ -219,4 +220,6 @@ suite('Paths', () => { const r4 = extpath.randomPath(); assert.ok(r4); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/base/test/common/fuzzyScorer.test.ts b/src/vs/base/test/common/fuzzyScorer.test.ts index ac3f2aab30f..03417dea597 100644 --- a/src/vs/base/test/common/fuzzyScorer.test.ts +++ b/src/vs/base/test/common/fuzzyScorer.test.ts @@ -9,6 +9,7 @@ import { Schemas } from 'vs/base/common/network'; import { basename, dirname, posix, sep, win32 } from 'vs/base/common/path'; import { isWindows } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; class ResourceAccessorClass implements IItemAccessor { @@ -1250,4 +1251,6 @@ suite('Fuzzy Scorer', () => { assert.strictEqual(score[1][0], 7); assert.strictEqual(score[1][1], 8); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/base/test/common/glob.test.ts b/src/vs/base/test/common/glob.test.ts index bbabd7faf91..5bfb3dccfb3 100644 --- a/src/vs/base/test/common/glob.test.ts +++ b/src/vs/base/test/common/glob.test.ts @@ -8,6 +8,7 @@ import * as glob from 'vs/base/common/glob'; import { sep } from 'vs/base/common/path'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('Glob', () => { @@ -1156,4 +1157,6 @@ suite('Glob', () => { assert.ok(!glob.patternsEquals(undefined, ['b'])); assert.ok(!glob.patternsEquals(['a'], undefined)); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/base/test/common/labels.test.ts b/src/vs/base/test/common/labels.test.ts index f02cd55c523..8928b09c72d 100644 --- a/src/vs/base/test/common/labels.test.ts +++ b/src/vs/base/test/common/labels.test.ts @@ -7,6 +7,7 @@ import * as assert from 'assert'; import * as labels from 'vs/base/common/labels'; import { isMacintosh, isWindows, OperatingSystem } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('Labels', () => { (!isWindows ? test.skip : test)('shorten - windows', () => { @@ -231,4 +232,6 @@ suite('Labels', () => { assert.strictEqual(labels.getPathLabel(nixUntitledUri, { os: OperatingSystem.Macintosh, relative: nixRelativePathProvider }), 'folder/file.txt'); assert.strictEqual(labels.getPathLabel(nixUntitledUri, { os: OperatingSystem.Linux, relative: nixRelativePathProvider }), 'folder/file.txt'); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/base/test/common/lifecycle.test.ts b/src/vs/base/test/common/lifecycle.test.ts index 343167bd57d..ae8627eb117 100644 --- a/src/vs/base/test/common/lifecycle.test.ts +++ b/src/vs/base/test/common/lifecycle.test.ts @@ -231,7 +231,7 @@ suite('No Leakage Utilities', () => { eventEmitter.event(() => { // noop }); - }); + }, false); }, e => e.message.indexOf('undisposed disposables') !== -1); }); @@ -239,7 +239,7 @@ suite('No Leakage Utilities', () => { assertThrows(() => { throwIfDisposablesAreLeaked(() => { new DisposableStore(); - }); + }, false); }, e => e.message.indexOf('undisposed disposables') !== -1); }); diff --git a/src/vs/base/test/common/paging.test.ts b/src/vs/base/test/common/paging.test.ts index d85f04fe6ca..f0ff02a4ab5 100644 --- a/src/vs/base/test/common/paging.test.ts +++ b/src/vs/base/test/common/paging.test.ts @@ -4,13 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { disposableTimeout } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { canceled, isCancellationError } from 'vs/base/common/errors'; +import { CancellationError, isCancellationError } from 'vs/base/common/errors'; import { IPager, PagedModel } from 'vs/base/common/paging'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; function getPage(pageIndex: number, cancellationToken: CancellationToken): Promise { if (cancellationToken.isCancellationRequested) { - return Promise.reject(canceled()); + return Promise.reject(new CancellationError()); } return Promise.resolve([0, 1, 2, 3, 4].map(i => i + (pageIndex * 5))); @@ -30,6 +32,8 @@ class TestPager implements IPager { suite('PagedModel', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + test('isResolved', () => { const pager = new TestPager(); const model = new PagedModel(pager); @@ -116,11 +120,11 @@ suite('PagedModel', () => { test('cancellation works', function () { const pager = new TestPager((_, token) => new Promise((_, e) => { - token.onCancellationRequested(() => e(canceled())); + store.add(token.onCancellationRequested(() => e(new CancellationError()))); })); const model = new PagedModel(pager); - const tokenSource = new CancellationTokenSource(); + const tokenSource = store.add(new CancellationTokenSource()); const promise = model.resolve(5, tokenSource.token).then( () => assert(false), @@ -139,10 +143,10 @@ suite('PagedModel', () => { state = 'resolving'; return new Promise((_, e) => { - token.onCancellationRequested(() => { + store.add(token.onCancellationRequested(() => { state = 'idle'; - e(canceled()); - }); + e(new CancellationError()); + })); }); }); @@ -166,17 +170,17 @@ suite('PagedModel', () => { assert.strictEqual(state, 'resolving'); - setTimeout(() => { + store.add(disposableTimeout(() => { assert.strictEqual(state, 'resolving'); tokenSource1.cancel(); assert.strictEqual(state, 'resolving'); - setTimeout(() => { + store.add(disposableTimeout(() => { assert.strictEqual(state, 'resolving'); tokenSource2.cancel(); assert.strictEqual(state, 'idle'); - }, 10); - }, 10); + }, 10)); + }, 10)); return Promise.all([promise1, promise2]); }); diff --git a/src/vs/base/test/common/processes.test.ts b/src/vs/base/test/common/processes.test.ts index 12ade1a1458..d575590ab10 100644 --- a/src/vs/base/test/common/processes.test.ts +++ b/src/vs/base/test/common/processes.test.ts @@ -5,8 +5,11 @@ import * as assert from 'assert'; import * as processes from 'vs/base/common/processes'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('Processes', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + test('sanitizeProcessEnvironment', () => { const env = { FOO: 'bar', diff --git a/src/vs/base/test/common/stream.test.ts b/src/vs/base/test/common/stream.test.ts index 63d30530a95..5589dfdceaf 100644 --- a/src/vs/base/test/common/stream.test.ts +++ b/src/vs/base/test/common/stream.test.ts @@ -8,6 +8,7 @@ import { timeout } from 'vs/base/common/async'; import { bufferToReadable, VSBuffer } from 'vs/base/common/buffer'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { consumeReadable, consumeStream, isReadable, isReadableBufferedStream, isReadableStream, listenStream, newWriteableStream, peekReadable, peekStream, prefixedReadable, prefixedStream, Readable, ReadableStream, toReadable, toStream, transform } from 'vs/base/common/stream'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('Stream', () => { @@ -514,4 +515,6 @@ suite('Stream', () => { } assert.ok(error); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/base/test/common/utils.ts b/src/vs/base/test/common/utils.ts index 3301cdfe614..9c38a6a06df 100644 --- a/src/vs/base/test/common/utils.ts +++ b/src/vs/base/test/common/utils.ts @@ -49,15 +49,18 @@ interface DisposableInfo { source: string | null; parent: IDisposable | null; isSingleton: boolean; + idx: number; } export class DisposableTracker implements IDisposableTracker { + private static idx = 0; + private readonly livingDisposables = new Map(); private getDisposableData(d: IDisposable) { let val = this.livingDisposables.get(d); if (!val) { - val = { parent: null, source: null, isSingleton: false, value: d }; + val = { parent: null, source: null, isSingleton: false, value: d, idx: DisposableTracker.idx++ }; this.livingDisposables.set(d, val); } return val; @@ -106,7 +109,7 @@ export class DisposableTracker implements IDisposableTracker { return leaking; } - ensureNoLeakingDisposables() { + ensureNoLeakingDisposables(logToConsole = true) { const rootParentCache = new Map(); const leakingObjects = [...this.livingDisposables.values()] @@ -146,10 +149,13 @@ export class DisposableTracker implements IDisposableTracker { } } - uncoveredLeakingObjs.sort(compareBy(l => getStackTracePath(l).length, numberComparator)); + // Put earlier leaks first + uncoveredLeakingObjs.sort(compareBy(l => l.idx, numberComparator)); const maxReported = 10; + let message = ''; + let i = 0; for (const leaking of uncoveredLeakingObjs.slice(0, maxReported)) { i++; @@ -171,14 +177,18 @@ export class DisposableTracker implements IDisposableTracker { stackTraceFormattedLines.unshift(line); } - console.error(`\n\n\n==================== Leaking disposable ${i}/${uncoveredLeakingObjs.length}: ${leaking.value.constructor.name} ====================\n${stackTraceFormattedLines.join('\n')}\n============================================================\n\n`); + message += `\n\n\n==================== Leaking disposable ${i}/${uncoveredLeakingObjs.length}: ${leaking.value.constructor.name} ====================\n${stackTraceFormattedLines.join('\n')}\n============================================================\n\n`; } if (uncoveredLeakingObjs.length > maxReported) { - console.error(`\n\n\n... and ${uncoveredLeakingObjs.length - maxReported} more leaking disposables\n\n`); + message += `\n\n\n... and ${uncoveredLeakingObjs.length - maxReported} more leaking disposables\n\n`; } - throw new Error(`There are ${uncoveredLeakingObjs.length} undisposed disposables! (check test output)`); + if (logToConsole) { + console.error(message); + } + + throw new Error(`There are ${uncoveredLeakingObjs.length} undisposed disposables!${message}`); } } @@ -217,12 +227,12 @@ export function ensureNoDisposablesAreLeakedInTestSuite(): Pick void): void { +export function throwIfDisposablesAreLeaked(body: () => void, logToConsole = true): void { const tracker = new DisposableTracker(); setDisposableTracker(tracker); body(); setDisposableTracker(null); - tracker.ensureNoLeakingDisposables(); + tracker.ensureNoLeakingDisposables(logToConsole); } export async function throwIfDisposablesAreLeakedAsync(body: () => Promise): Promise { diff --git a/src/vs/base/test/node/extpath.test.ts b/src/vs/base/test/node/extpath.test.ts index c4e2fd831fb..04aa1873295 100644 --- a/src/vs/base/test/node/extpath.test.ts +++ b/src/vs/base/test/node/extpath.test.ts @@ -7,6 +7,7 @@ import * as assert from 'assert'; import { tmpdir } from 'os'; import { realcase, realcaseSync, realpath, realpathSync } from 'vs/base/node/extpath'; import { Promises } from 'vs/base/node/pfs'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; flakySuite('Extpath', () => { @@ -79,4 +80,6 @@ flakySuite('Extpath', () => { const realpath = realpathSync(testDir); assert.ok(realpath); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/base/test/node/pfs/pfs.test.ts b/src/vs/base/test/node/pfs/pfs.test.ts index 3a0fc605a71..b3ef62f232a 100644 --- a/src/vs/base/test/node/pfs/pfs.test.ts +++ b/src/vs/base/test/node/pfs/pfs.test.ts @@ -13,6 +13,7 @@ import { FileAccess } from 'vs/base/common/network'; import { basename, dirname, join, sep } from 'vs/base/common/path'; import { isWindows } from 'vs/base/common/platform'; import { configureFlushOnWrite, Promises, RimRafMode, rimrafSync, SymlinkSupport, writeFileSync } from 'vs/base/node/pfs'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; configureFlushOnWrite(false); // speed up all unit tests by disabling flush on write @@ -488,4 +489,6 @@ flakySuite('PFS', function () { writeFileSync(testFile, largeString); assert.strictEqual(fs.readFileSync(testFile).toString(), largeString); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/base/test/node/zip/zip.test.ts b/src/vs/base/test/node/zip/zip.test.ts index ef6797629ca..0a898e2c7bc 100644 --- a/src/vs/base/test/node/zip/zip.test.ts +++ b/src/vs/base/test/node/zip/zip.test.ts @@ -10,28 +10,24 @@ import { FileAccess } from 'vs/base/common/network'; import * as path from 'vs/base/common/path'; import { Promises } from 'vs/base/node/pfs'; import { extract } from 'vs/base/node/zip'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { getRandomTestPath } from 'vs/base/test/node/testUtils'; suite('Zip', () => { - let testDir: string; - - setup(() => { - testDir = getRandomTestPath(tmpdir(), 'vsctests', 'zip'); - - return Promises.mkdir(testDir, { recursive: true }); - }); - - teardown(() => { - return Promises.rm(testDir); - }); + ensureNoDisposablesAreLeakedInTestSuite(); test('extract should handle directories', async () => { + const testDir = getRandomTestPath(tmpdir(), 'vsctests', 'zip'); + await Promises.mkdir(testDir, { recursive: true }); + const fixtures = FileAccess.asFileUri('vs/base/test/node/zip/fixtures').fsPath; const fixture = path.join(fixtures, 'extract.zip'); await createCancelablePromise(token => extract(fixture, testDir, {}, token)); const doesExist = await Promises.exists(path.join(testDir, 'extension')); assert(doesExist); + + await Promises.rm(testDir); }); }); diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 9fcc3ce6f10..5307987e657 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -14,7 +14,7 @@ import { isEqualOrParent } from 'vs/base/common/extpath'; import { once } from 'vs/base/common/functional'; import { stripComments } from 'vs/base/common/json'; import { getPathLabel } from 'vs/base/common/labels'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { isAbsolute, join, posix } from 'vs/base/common/path'; import { IProcessEnvironment, isLinux, isLinuxSnap, isMacintosh, isWindows, OS } from 'vs/base/common/platform'; @@ -1051,10 +1051,12 @@ export class CodeApplication extends Disposable { // can talk to the first instance. Electron IPC does not work // across apps until `requestSingleInstance` APIs are adopted. - const launchChannel = ProxyChannel.fromService(accessor.get(ILaunchMainService), { disableMarshalling: true }); + const disposables = this._register(new DisposableStore()); + + const launchChannel = ProxyChannel.fromService(accessor.get(ILaunchMainService), disposables, { disableMarshalling: true }); this.mainProcessNodeIpcServer.registerChannel('launch', launchChannel); - const diagnosticsChannel = ProxyChannel.fromService(accessor.get(IDiagnosticsMainService), { disableMarshalling: true }); + const diagnosticsChannel = ProxyChannel.fromService(accessor.get(IDiagnosticsMainService), disposables, { disableMarshalling: true }); this.mainProcessNodeIpcServer.registerChannel('diagnostics', diagnosticsChannel); // Policies (main & shared process) @@ -1070,7 +1072,7 @@ export class CodeApplication extends Disposable { sharedProcessClient.then(client => client.registerChannel(LOCAL_FILE_SYSTEM_CHANNEL_NAME, fileSystemProviderChannel)); // User Data Profiles - const userDataProfilesService = ProxyChannel.fromService(accessor.get(IUserDataProfilesMainService)); + const userDataProfilesService = ProxyChannel.fromService(accessor.get(IUserDataProfilesMainService), disposables); mainProcessElectronServer.registerChannel('userDataProfiles', userDataProfilesService); sharedProcessClient.then(client => client.registerChannel('userDataProfiles', userDataProfilesService)); @@ -1083,45 +1085,45 @@ export class CodeApplication extends Disposable { mainProcessElectronServer.registerChannel('update', updateChannel); // Issues - const issueChannel = ProxyChannel.fromService(accessor.get(IIssueMainService)); + const issueChannel = ProxyChannel.fromService(accessor.get(IIssueMainService), disposables); mainProcessElectronServer.registerChannel('issue', issueChannel); // Encryption - const encryptionChannel = ProxyChannel.fromService(accessor.get(IEncryptionMainService)); + const encryptionChannel = ProxyChannel.fromService(accessor.get(IEncryptionMainService), disposables); mainProcessElectronServer.registerChannel('encryption', encryptionChannel); // Signing - const signChannel = ProxyChannel.fromService(accessor.get(ISignService)); + const signChannel = ProxyChannel.fromService(accessor.get(ISignService), disposables); mainProcessElectronServer.registerChannel('sign', signChannel); // Keyboard Layout - const keyboardLayoutChannel = ProxyChannel.fromService(accessor.get(IKeyboardLayoutMainService)); + const keyboardLayoutChannel = ProxyChannel.fromService(accessor.get(IKeyboardLayoutMainService), disposables); mainProcessElectronServer.registerChannel('keyboardLayout', keyboardLayoutChannel); // Native host (main & shared process) this.nativeHostMainService = accessor.get(INativeHostMainService); - const nativeHostChannel = ProxyChannel.fromService(this.nativeHostMainService); + const nativeHostChannel = ProxyChannel.fromService(this.nativeHostMainService, disposables); mainProcessElectronServer.registerChannel('nativeHost', nativeHostChannel); sharedProcessClient.then(client => client.registerChannel('nativeHost', nativeHostChannel)); // Workspaces - const workspacesChannel = ProxyChannel.fromService(accessor.get(IWorkspacesService)); + const workspacesChannel = ProxyChannel.fromService(accessor.get(IWorkspacesService), disposables); mainProcessElectronServer.registerChannel('workspaces', workspacesChannel); // Menubar - const menubarChannel = ProxyChannel.fromService(accessor.get(IMenubarMainService)); + const menubarChannel = ProxyChannel.fromService(accessor.get(IMenubarMainService), disposables); mainProcessElectronServer.registerChannel('menubar', menubarChannel); // URL handling - const urlChannel = ProxyChannel.fromService(accessor.get(IURLService)); + const urlChannel = ProxyChannel.fromService(accessor.get(IURLService), disposables); mainProcessElectronServer.registerChannel('url', urlChannel); // Extension URL Trust - const extensionUrlTrustChannel = ProxyChannel.fromService(accessor.get(IExtensionUrlTrustService)); + const extensionUrlTrustChannel = ProxyChannel.fromService(accessor.get(IExtensionUrlTrustService), disposables); mainProcessElectronServer.registerChannel('extensionUrlTrust', extensionUrlTrustChannel); // Webview Manager - const webviewChannel = ProxyChannel.fromService(accessor.get(IWebviewManagerService)); + const webviewChannel = ProxyChannel.fromService(accessor.get(IWebviewManagerService), disposables); mainProcessElectronServer.registerChannel('webview', webviewChannel); // Storage (main & shared process) @@ -1134,11 +1136,11 @@ export class CodeApplication extends Disposable { sharedProcessClient.then(client => client.registerChannel('profileStorageListener', profileStorageListener)); // Terminal - const ptyHostChannel = ProxyChannel.fromService(accessor.get(ILocalPtyService)); + const ptyHostChannel = ProxyChannel.fromService(accessor.get(ILocalPtyService), disposables); mainProcessElectronServer.registerChannel(TerminalIpcChannels.LocalPty, ptyHostChannel); // External Terminal - const externalTerminalChannel = ProxyChannel.fromService(accessor.get(IExternalTerminalMainService)); + const externalTerminalChannel = ProxyChannel.fromService(accessor.get(IExternalTerminalMainService), disposables); mainProcessElectronServer.registerChannel('externalTerminal', externalTerminalChannel); // Logger @@ -1151,11 +1153,11 @@ export class CodeApplication extends Disposable { mainProcessElectronServer.registerChannel('extensionhostdebugservice', electronExtensionHostDebugBroadcastChannel); // Extension Host Starter - const extensionHostStarterChannel = ProxyChannel.fromService(accessor.get(IExtensionHostStarter)); + const extensionHostStarterChannel = ProxyChannel.fromService(accessor.get(IExtensionHostStarter), disposables); mainProcessElectronServer.registerChannel(ipcExtensionHostStarterChannelName, extensionHostStarterChannel); // Utility Process Worker - const utilityProcessWorkerChannel = ProxyChannel.fromService(accessor.get(IUtilityProcessWorkerMainService)); + const utilityProcessWorkerChannel = ProxyChannel.fromService(accessor.get(IUtilityProcessWorkerMainService), disposables); mainProcessElectronServer.registerChannel(ipcUtilityProcessWorkerChannelName, utilityProcessWorkerChannel); } diff --git a/src/vs/code/node/sharedProcess/sharedProcessMain.ts b/src/vs/code/node/sharedProcess/sharedProcessMain.ts index a81bedb9ab4..2a46b4ddaf5 100644 --- a/src/vs/code/node/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/node/sharedProcess/sharedProcessMain.ts @@ -7,7 +7,7 @@ import { hostname, release } from 'os'; import { MessagePortMain, MessageEvent } from 'vs/base/parts/sandbox/node/electronTypes'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { onUnexpectedError, setUnexpectedErrorHandler } from 'vs/base/common/errors'; -import { combinedDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { combinedDisposable, Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { firstOrDefault } from 'vs/base/common/arrays'; @@ -367,16 +367,18 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter { private initChannels(accessor: ServicesAccessor): void { + const disposables = this._register(new DisposableStore()); + // Extensions Management const channel = new ExtensionManagementChannel(accessor.get(IExtensionManagementService), () => null); this.server.registerChannel('extensions', channel); // Language Packs - const languagePacksChannel = ProxyChannel.fromService(accessor.get(ILanguagePackService)); + const languagePacksChannel = ProxyChannel.fromService(accessor.get(ILanguagePackService), disposables); this.server.registerChannel('languagePacks', languagePacksChannel); // Diagnostics - const diagnosticsChannel = ProxyChannel.fromService(accessor.get(IDiagnosticsService)); + const diagnosticsChannel = ProxyChannel.fromService(accessor.get(IDiagnosticsService), disposables); this.server.registerChannel('diagnostics', diagnosticsChannel); // Extension Tips @@ -384,11 +386,11 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter { this.server.registerChannel('extensionTipsService', extensionTipsChannel); // Checksum - const checksumChannel = ProxyChannel.fromService(accessor.get(IChecksumService)); + const checksumChannel = ProxyChannel.fromService(accessor.get(IChecksumService), disposables); this.server.registerChannel('checksum', checksumChannel); // Profiling - const profilingChannel = ProxyChannel.fromService(accessor.get(IV8InspectProfilingService)); + const profilingChannel = ProxyChannel.fromService(accessor.get(IV8InspectProfilingService), disposables); this.server.registerChannel('v8InspectProfiling', profilingChannel); // Settings Sync @@ -396,7 +398,7 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter { this.server.registerChannel('userDataSyncMachines', userDataSyncMachineChannel); // Custom Endpoint Telemetry - const customEndpointTelemetryChannel = ProxyChannel.fromService(accessor.get(ICustomEndpointTelemetryService)); + const customEndpointTelemetryChannel = ProxyChannel.fromService(accessor.get(ICustomEndpointTelemetryService), disposables); this.server.registerChannel('customEndpointTelemetry', customEndpointTelemetryChannel); const userDataSyncAccountChannel = new UserDataSyncAccountServiceChannel(accessor.get(IUserDataSyncAccountService)); @@ -413,11 +415,11 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter { this.server.registerChannel('userDataAutoSync', userDataAutoSyncChannel); // Tunnel - const sharedProcessTunnelChannel = ProxyChannel.fromService(accessor.get(ISharedProcessTunnelService)); + const sharedProcessTunnelChannel = ProxyChannel.fromService(accessor.get(ISharedProcessTunnelService), disposables); this.server.registerChannel(ipcSharedProcessTunnelChannelName, sharedProcessTunnelChannel); // Remote Tunnel - const remoteTunnelChannel = ProxyChannel.fromService(accessor.get(IRemoteTunnelService)); + const remoteTunnelChannel = ProxyChannel.fromService(accessor.get(IRemoteTunnelService), disposables); this.server.registerChannel('remoteTunnel', remoteTunnelChannel); } diff --git a/src/vs/editor/browser/diff/testDiffProviderFactoryService.ts b/src/vs/editor/browser/diff/testDiffProviderFactoryService.ts new file mode 100644 index 00000000000..08ed249b8b9 --- /dev/null +++ b/src/vs/editor/browser/diff/testDiffProviderFactoryService.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from 'vs/base/common/cancellation'; +import { toDisposable } from 'vs/base/common/lifecycle'; +import { IDocumentDiff, IDocumentDiffProvider, IDocumentDiffProviderOptions } from 'vs/editor/common/diff/documentDiffProvider'; +import { linesDiffComputers } from 'vs/editor/common/diff/linesDiffComputers'; +import { ITextModel } from 'vs/editor/common/model'; +import { Event } from 'vs/base/common/event'; +import { IDiffProviderFactoryService } from 'vs/editor/browser/widget/diffEditor/diffProviderFactoryService'; + +export class TestDiffProviderFactoryService implements IDiffProviderFactoryService { + declare readonly _serviceBrand: undefined; + createDiffProvider(): IDocumentDiffProvider { + return new SyncDocumentDiffProvider(); + } +} + +export class SyncDocumentDiffProvider implements IDocumentDiffProvider { + computeDiff(original: ITextModel, modified: ITextModel, options: IDocumentDiffProviderOptions, cancellationToken: CancellationToken): Promise { + const result = linesDiffComputers.getDefault().computeDiff(original.getLinesContent(), modified.getLinesContent(), options); + return Promise.resolve({ + changes: result.changes, + quitEarly: result.hitTimeout, + identical: original.getValue() === modified.getValue(), + moves: result.moves, + }); + } + + onDidChange: Event = () => toDisposable(() => { }); +} diff --git a/src/vs/editor/browser/widget/diffEditor/diffEditor.contribution.ts b/src/vs/editor/browser/widget/diffEditor/diffEditor.contribution.ts index 7eb51c59d65..9ccc307b2cd 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditor.contribution.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffEditor.contribution.ts @@ -25,7 +25,9 @@ export class ToggleCollapseUnchangedRegions extends Action2 { title: { value: localize('toggleCollapseUnchangedRegions', "Toggle Collapse Unchanged Regions"), original: 'Toggle Collapse Unchanged Regions' }, icon: Codicon.map, toggled: ContextKeyExpr.has('config.diffEditor.hideUnchangedRegions.enabled'), + precondition: ContextKeyExpr.has('isInDiffEditor'), menu: { + when: ContextKeyExpr.has('isInDiffEditor'), id: MenuId.EditorTitle, order: 22, group: 'navigation', @@ -47,6 +49,7 @@ export class ToggleShowMovedCodeBlocks extends Action2 { super({ id: 'diffEditor.toggleShowMovedCodeBlocks', title: { value: localize('toggleShowMovedCodeBlocks', "Toggle Show Moved Code Blocks"), original: 'Toggle Show Moved Code Blocks' }, + precondition: ContextKeyExpr.has('isInDiffEditor'), }); } @@ -64,6 +67,7 @@ export class ToggleUseInlineViewWhenSpaceIsLimited extends Action2 { super({ id: 'diffEditor.toggleUseInlineViewWhenSpaceIsLimited', title: { value: localize('toggleUseInlineViewWhenSpaceIsLimited', "Toggle Use Inline View When Space Is Limited"), original: 'Toggle Use Inline View When Space Is Limited' }, + precondition: ContextKeyExpr.has('isInDiffEditor'), }); } @@ -81,10 +85,14 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { id: new ToggleUseInlineViewWhenSpaceIsLimited().desc.id, title: localize('useInlineViewWhenSpaceIsLimited', "Use Inline View When Space Is Limited"), toggled: ContextKeyExpr.has('config.diffEditor.useInlineViewWhenSpaceIsLimited'), + precondition: ContextKeyExpr.has('isInDiffEditor'), }, order: 11, group: '1_diff', - when: EditorContextKeys.diffEditorRenderSideBySideInlineBreakpointReached, + when: ContextKeyExpr.and( + EditorContextKeys.diffEditorRenderSideBySideInlineBreakpointReached, + ContextKeyExpr.has('isInDiffEditor'), + ), }); MenuRegistry.appendMenuItem(MenuId.EditorTitle, { @@ -93,9 +101,11 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { title: localize('showMoves', "Show Moved Code Blocks"), icon: Codicon.move, toggled: ContextKeyEqualsExpr.create('config.diffEditor.experimental.showMoves', true), + precondition: ContextKeyExpr.has('isInDiffEditor'), }, order: 10, group: '1_diff', + when: ContextKeyExpr.has('isInDiffEditor'), }); const diffEditorCategory: ILocalizedString = { @@ -232,6 +242,7 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: AccessibleDiffViewerNext.id, title: localize('Open Accessible Diff Viewer', "Open Accessible Diff Viewer"), + precondition: ContextKeyExpr.has('isInDiffEditor'), }, order: 10, group: '2_diff', diff --git a/src/vs/editor/browser/widget/diffEditor/movedBlocksLines.ts b/src/vs/editor/browser/widget/diffEditor/movedBlocksLines.ts index c67fd8822af..edaf0c6e099 100644 --- a/src/vs/editor/browser/widget/diffEditor/movedBlocksLines.ts +++ b/src/vs/editor/browser/widget/diffEditor/movedBlocksLines.ts @@ -316,24 +316,24 @@ class MovedBlockOverlayWidget extends ViewZoneOverlayWidget { 'codeMovedToWithChanges', 'Code moved with changes to line {0}-{1}', this._move.lineRangeMapping.modified.startLineNumber, - this._move.lineRangeMapping.modified.endLineNumberExclusive + this._move.lineRangeMapping.modified.endLineNumberExclusive - 1, ) : localize( 'codeMovedFromWithChanges', 'Code moved with changes from line {0}-{1}', this._move.lineRangeMapping.original.startLineNumber, - this._move.lineRangeMapping.original.endLineNumberExclusive + this._move.lineRangeMapping.original.endLineNumberExclusive - 1, ); } else { text = this._kind === 'original' ? localize( 'codeMovedTo', 'Code moved to line {0}-{1}', this._move.lineRangeMapping.modified.startLineNumber, - this._move.lineRangeMapping.modified.endLineNumberExclusive + this._move.lineRangeMapping.modified.endLineNumberExclusive - 1, ) : localize( 'codeMovedFrom', 'Code moved from line {0}-{1}', this._move.lineRangeMapping.original.startLineNumber, - this._move.lineRangeMapping.original.endLineNumberExclusive + this._move.lineRangeMapping.original.endLineNumberExclusive - 1, ); } diff --git a/src/vs/editor/browser/widget/diffEditor/style.css b/src/vs/editor/browser/widget/diffEditor/style.css index 6e52932ef8e..d81f5cf2ebe 100644 --- a/src/vs/editor/browser/widget/diffEditor/style.css +++ b/src/vs/editor/browser/widget/diffEditor/style.css @@ -286,3 +286,14 @@ .monaco-diff-editor .diffViewport:active { background: var(--vscode-scrollbarSlider-activeBackground); } + +.monaco-editor .diagonal-fill { + background-image: linear-gradient( + -45deg, + var(--vscode-diffEditor-diagonalFill) 12.5%, + #0000 12.5%, #0000 50%, + var(--vscode-diffEditor-diagonalFill) 50%, var(--vscode-diffEditor-diagonalFill) 62.5%, + #0000 62.5%, #0000 100% + ); + background-size: 8px 8px; +} diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 31fd45a4430..d5938f90014 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -459,6 +459,11 @@ export interface IEditorOptions { * Defaults to language defined behavior. */ autoClosingBrackets?: EditorAutoClosingStrategy; + /** + * Options for auto closing comments. + * Defaults to language defined behavior. + */ + autoClosingComments?: EditorAutoClosingStrategy; /** * Options for auto closing quotes. * Defaults to language defined behavior. @@ -2033,6 +2038,11 @@ export interface IEditorHoverOptions { * Defaults to true. */ sticky?: boolean; + /** + * Controls how long the hover is visible after you hovered out of it. + * Require sticky setting to be true. + */ + hidingDelay?: number; /** * Should the hover be shown above the line if possible? * Defaults to false. @@ -2051,6 +2061,7 @@ class EditorHover extends BaseEditorOption boolean; + + const chIsQuote = isQuote(ch); + if (chIsQuote) { + autoCloseConfig = config.autoClosingQuotes; + shouldAutoCloseBefore = config.shouldAutoCloseBefore.quote; + } else { + const pairIsForComments = config.blockCommentStartToken ? pair.open.includes(config.blockCommentStartToken) : false; + if (pairIsForComments) { + autoCloseConfig = config.autoClosingComments; + shouldAutoCloseBefore = config.shouldAutoCloseBefore.comment; + } else { + autoCloseConfig = config.autoClosingBrackets; + shouldAutoCloseBefore = config.shouldAutoCloseBefore.bracket; + } + } + + if (autoCloseConfig === 'never') { + return null; + } + // Sometimes, it is possible to have two auto-closing pairs that have a containment relationship // e.g. when having [(,)] and [(*,*)] // - when typing (, the resulting state is (|) diff --git a/src/vs/editor/common/cursorCommon.ts b/src/vs/editor/common/cursorCommon.ts index 9507d83056b..13b95ad1299 100644 --- a/src/vs/editor/common/cursorCommon.ts +++ b/src/vs/editor/common/cursorCommon.ts @@ -66,6 +66,7 @@ export class CursorConfiguration { public readonly multiCursorPaste: 'spread' | 'full'; public readonly multiCursorLimit: number; public readonly autoClosingBrackets: EditorAutoClosingStrategy; + public readonly autoClosingComments: EditorAutoClosingStrategy; public readonly autoClosingQuotes: EditorAutoClosingStrategy; public readonly autoClosingDelete: EditorAutoClosingEditStrategy; public readonly autoClosingOvertype: EditorAutoClosingEditStrategy; @@ -73,7 +74,8 @@ export class CursorConfiguration { public readonly autoIndent: EditorAutoIndentStrategy; public readonly autoClosingPairs: AutoClosingPairs; public readonly surroundingPairs: CharacterMap; - public readonly shouldAutoCloseBefore: { quote: (ch: string) => boolean; bracket: (ch: string) => boolean }; + public readonly blockCommentStartToken: string | null; + public readonly shouldAutoCloseBefore: { quote: (ch: string) => boolean; bracket: (ch: string) => boolean; comment: (ch: string) => boolean }; private readonly _languageId: string; private _electricChars: { [key: string]: boolean } | null; @@ -87,6 +89,7 @@ export class CursorConfiguration { || e.hasChanged(EditorOption.multiCursorPaste) || e.hasChanged(EditorOption.multiCursorLimit) || e.hasChanged(EditorOption.autoClosingBrackets) + || e.hasChanged(EditorOption.autoClosingComments) || e.hasChanged(EditorOption.autoClosingQuotes) || e.hasChanged(EditorOption.autoClosingDelete) || e.hasChanged(EditorOption.autoClosingOvertype) @@ -125,6 +128,7 @@ export class CursorConfiguration { this.multiCursorPaste = options.get(EditorOption.multiCursorPaste); this.multiCursorLimit = options.get(EditorOption.multiCursorLimit); this.autoClosingBrackets = options.get(EditorOption.autoClosingBrackets); + this.autoClosingComments = options.get(EditorOption.autoClosingComments); this.autoClosingQuotes = options.get(EditorOption.autoClosingQuotes); this.autoClosingDelete = options.get(EditorOption.autoClosingDelete); this.autoClosingOvertype = options.get(EditorOption.autoClosingOvertype); @@ -136,7 +140,8 @@ export class CursorConfiguration { this.shouldAutoCloseBefore = { quote: this._getShouldAutoClose(languageId, this.autoClosingQuotes, true), - bracket: this._getShouldAutoClose(languageId, this.autoClosingBrackets, false) + comment: this._getShouldAutoClose(languageId, this.autoClosingComments, false), + bracket: this._getShouldAutoClose(languageId, this.autoClosingBrackets, false), }; this.autoClosingPairs = this.languageConfigurationService.getLanguageConfiguration(languageId).getAutoClosingPairs(); @@ -147,6 +152,9 @@ export class CursorConfiguration { this.surroundingPairs[pair.open] = pair.close; } } + + const commentsConfiguration = this.languageConfigurationService.getLanguageConfiguration(languageId).comments; + this.blockCommentStartToken = commentsConfiguration?.blockCommentStartToken ?? null; } public get electricChars() { diff --git a/src/vs/editor/common/diff/defaultLinesDiffComputer/algorithms/myersDiffAlgorithm.ts b/src/vs/editor/common/diff/defaultLinesDiffComputer/algorithms/myersDiffAlgorithm.ts index 0f3b64004cd..c0843188927 100644 --- a/src/vs/editor/common/diff/defaultLinesDiffComputer/algorithms/myersDiffAlgorithm.ts +++ b/src/vs/editor/common/diff/defaultLinesDiffComputer/algorithms/myersDiffAlgorithm.ts @@ -17,8 +17,11 @@ export class MyersDiffAlgorithm implements IDiffAlgorithm { return DiffAlgorithmResult.trivial(seq1, seq2); } + const seqX = seq1; // Text on the x axis + const seqY = seq2; // Text on the y axis + function getXAfterSnake(x: number, y: number): number { - while (x < seq1.length && y < seq2.length && seq1.getElement(x) === seq2.getElement(y)) { + while (x < seqX.length && y < seqY.length && seqX.getElement(x) === seqY.getElement(y)) { x++; y++; } @@ -29,6 +32,7 @@ export class MyersDiffAlgorithm implements IDiffAlgorithm { // V[k]: X value of longest d-line that ends in diagonal k. // d-line: path from (0,0) to (x,y) that uses exactly d non-diagonals. // diagonal k: Set of points (x,y) with x-y = k. + // k=1 -> (1,0),(2,1) const V = new FastInt32Array(); V.set(0, getXAfterSnake(0, 0)); @@ -40,18 +44,21 @@ export class MyersDiffAlgorithm implements IDiffAlgorithm { loop: while (true) { d++; if (!timeout.isValid()) { - return DiffAlgorithmResult.trivialTimedOut(seq1, seq2); + return DiffAlgorithmResult.trivialTimedOut(seqX, seqY); } // The paper has `for (k = -d; k <= d; k += 2)`, but we can ignore diagonals that cannot influence the result. - const lowerBound = -Math.min(d, seq2.length + (d % 2)); - const upperBound = Math.min(d, seq1.length + (d % 2)); + const lowerBound = -Math.min(d, seqY.length + (d % 2)); + const upperBound = Math.min(d, seqX.length + (d % 2)); for (k = lowerBound; k <= upperBound; k += 2) { + let step = 0; // We can use the X values of (d-1)-lines to compute X value of the longest d-lines. - const maxXofDLineTop = k === upperBound ? -1 : V.get(k + 1); // We take a vertical non-diagonal (add a symbol in seq1) - const maxXofDLineLeft = k === lowerBound ? -1 : V.get(k - 1) + 1; // We take a horizontal non-diagonal (+1 x) (delete a symbol in seq1) - const x = Math.min(Math.max(maxXofDLineTop, maxXofDLineLeft), seq1.length); + const maxXofDLineTop = k === upperBound ? -1 : V.get(k + 1); // We take a vertical non-diagonal (add a symbol in seqX) + const maxXofDLineLeft = k === lowerBound ? -1 : V.get(k - 1) + 1; // We take a horizontal non-diagonal (+1 x) (delete a symbol in seqX) + step++; + const x = Math.min(Math.max(maxXofDLineTop, maxXofDLineLeft), seqX.length); const y = x - k; - if (x > seq1.length || y > seq2.length) { + step++; + if (x > seqX.length || y > seqY.length) { // This diagonal is irrelevant for the result. // TODO: Don't pay the cost for this in the next iteration. continue; @@ -61,7 +68,7 @@ export class MyersDiffAlgorithm implements IDiffAlgorithm { const lastPath = x === maxXofDLineTop ? paths.get(k + 1) : paths.get(k - 1); paths.set(k, newMaxX !== x ? new SnakePath(lastPath, x, y, newMaxX - x) : lastPath); - if (V.get(k) === seq1.length && V.get(k) - k === seq2.length) { + if (V.get(k) === seqX.length && V.get(k) - k === seqY.length) { break loop; } } @@ -69,8 +76,8 @@ export class MyersDiffAlgorithm implements IDiffAlgorithm { let path = paths.get(k); const result: SequenceDiff[] = []; - let lastAligningPosS1: number = seq1.length; - let lastAligningPosS2: number = seq2.length; + let lastAligningPosS1: number = seqX.length; + let lastAligningPosS2: number = seqY.length; while (true) { const endX = path ? path.x + path.length : 0; diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 15b63cf0386..82d7b1fc1e1 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -1280,10 +1280,6 @@ export interface FormattingOptions { * Prefer spaces over tabs. */ insertSpaces: boolean; - /** - * The list of multiple ranges to format at once, if the provider supports it. - */ - ranges?: Range[]; } /** * The document formatting provider interface defines the contract between extensions and diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index ea74d5c19b9..3d9f01f7dfa 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -1395,7 +1395,7 @@ export class SearchData { /** * @internal */ -export interface ITextBuffer extends IReadonlyTextBuffer { +export interface ITextBuffer extends IReadonlyTextBuffer, IDisposable { setEOL(newEOL: '\r\n' | '\n'): void; applyEdits(rawOperations: ValidAnnotatedEditOperation[], recordTrimAutoWhitespace: boolean, computeUndoEdits: boolean): ApplyEditsResult; } diff --git a/src/vs/editor/common/standalone/standaloneEnums.ts b/src/vs/editor/common/standalone/standaloneEnums.ts index 7cc3add3df1..ec6cf691492 100644 --- a/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/src/vs/editor/common/standalone/standaloneEnums.ts @@ -180,146 +180,147 @@ export enum EditorOption { ariaLabel = 4, ariaRequired = 5, autoClosingBrackets = 6, - screenReaderAnnounceInlineSuggestion = 7, - autoClosingDelete = 8, - autoClosingOvertype = 9, - autoClosingQuotes = 10, - autoIndent = 11, - automaticLayout = 12, - autoSurround = 13, - bracketPairColorization = 14, - guides = 15, - codeLens = 16, - codeLensFontFamily = 17, - codeLensFontSize = 18, - colorDecorators = 19, - colorDecoratorsLimit = 20, - columnSelection = 21, - comments = 22, - contextmenu = 23, - copyWithSyntaxHighlighting = 24, - cursorBlinking = 25, - cursorSmoothCaretAnimation = 26, - cursorStyle = 27, - cursorSurroundingLines = 28, - cursorSurroundingLinesStyle = 29, - cursorWidth = 30, - disableLayerHinting = 31, - disableMonospaceOptimizations = 32, - domReadOnly = 33, - dragAndDrop = 34, - dropIntoEditor = 35, - emptySelectionClipboard = 36, - experimentalWhitespaceRendering = 37, - extraEditorClassName = 38, - fastScrollSensitivity = 39, - find = 40, - fixedOverflowWidgets = 41, - folding = 42, - foldingStrategy = 43, - foldingHighlight = 44, - foldingImportsByDefault = 45, - foldingMaximumRegions = 46, - unfoldOnClickAfterEndOfLine = 47, - fontFamily = 48, - fontInfo = 49, - fontLigatures = 50, - fontSize = 51, - fontWeight = 52, - fontVariations = 53, - formatOnPaste = 54, - formatOnType = 55, - glyphMargin = 56, - gotoLocation = 57, - hideCursorInOverviewRuler = 58, - hover = 59, - inDiffEditor = 60, - inlineSuggest = 61, - letterSpacing = 62, - lightbulb = 63, - lineDecorationsWidth = 64, - lineHeight = 65, - lineNumbers = 66, - lineNumbersMinChars = 67, - linkedEditing = 68, - links = 69, - matchBrackets = 70, - minimap = 71, - mouseStyle = 72, - mouseWheelScrollSensitivity = 73, - mouseWheelZoom = 74, - multiCursorMergeOverlapping = 75, - multiCursorModifier = 76, - multiCursorPaste = 77, - multiCursorLimit = 78, - occurrencesHighlight = 79, - overviewRulerBorder = 80, - overviewRulerLanes = 81, - padding = 82, - pasteAs = 83, - parameterHints = 84, - peekWidgetDefaultFocus = 85, - definitionLinkOpensInPeek = 86, - quickSuggestions = 87, - quickSuggestionsDelay = 88, - readOnly = 89, - readOnlyMessage = 90, - renameOnType = 91, - renderControlCharacters = 92, - renderFinalNewline = 93, - renderLineHighlight = 94, - renderLineHighlightOnlyWhenFocus = 95, - renderValidationDecorations = 96, - renderWhitespace = 97, - revealHorizontalRightPadding = 98, - roundedSelection = 99, - rulers = 100, - scrollbar = 101, - scrollBeyondLastColumn = 102, - scrollBeyondLastLine = 103, - scrollPredominantAxis = 104, - selectionClipboard = 105, - selectionHighlight = 106, - selectOnLineNumbers = 107, - showFoldingControls = 108, - showUnused = 109, - snippetSuggestions = 110, - smartSelect = 111, - smoothScrolling = 112, - stickyScroll = 113, - stickyTabStops = 114, - stopRenderingLineAfter = 115, - suggest = 116, - suggestFontSize = 117, - suggestLineHeight = 118, - suggestOnTriggerCharacters = 119, - suggestSelection = 120, - tabCompletion = 121, - tabIndex = 122, - unicodeHighlighting = 123, - unusualLineTerminators = 124, - useShadowDOM = 125, - useTabStops = 126, - wordBreak = 127, - wordSeparators = 128, - wordWrap = 129, - wordWrapBreakAfterCharacters = 130, - wordWrapBreakBeforeCharacters = 131, - wordWrapColumn = 132, - wordWrapOverride1 = 133, - wordWrapOverride2 = 134, - wrappingIndent = 135, - wrappingStrategy = 136, - showDeprecated = 137, - inlayHints = 138, - editorClassName = 139, - pixelRatio = 140, - tabFocusMode = 141, - layoutInfo = 142, - wrappingInfo = 143, - defaultColorDecorators = 144, - colorDecoratorsActivatedOn = 145, - inlineCompletionsAccessibilityVerbose = 146 + autoClosingComments = 7, + screenReaderAnnounceInlineSuggestion = 8, + autoClosingDelete = 9, + autoClosingOvertype = 10, + autoClosingQuotes = 11, + autoIndent = 12, + automaticLayout = 13, + autoSurround = 14, + bracketPairColorization = 15, + guides = 16, + codeLens = 17, + codeLensFontFamily = 18, + codeLensFontSize = 19, + colorDecorators = 20, + colorDecoratorsLimit = 21, + columnSelection = 22, + comments = 23, + contextmenu = 24, + copyWithSyntaxHighlighting = 25, + cursorBlinking = 26, + cursorSmoothCaretAnimation = 27, + cursorStyle = 28, + cursorSurroundingLines = 29, + cursorSurroundingLinesStyle = 30, + cursorWidth = 31, + disableLayerHinting = 32, + disableMonospaceOptimizations = 33, + domReadOnly = 34, + dragAndDrop = 35, + dropIntoEditor = 36, + emptySelectionClipboard = 37, + experimentalWhitespaceRendering = 38, + extraEditorClassName = 39, + fastScrollSensitivity = 40, + find = 41, + fixedOverflowWidgets = 42, + folding = 43, + foldingStrategy = 44, + foldingHighlight = 45, + foldingImportsByDefault = 46, + foldingMaximumRegions = 47, + unfoldOnClickAfterEndOfLine = 48, + fontFamily = 49, + fontInfo = 50, + fontLigatures = 51, + fontSize = 52, + fontWeight = 53, + fontVariations = 54, + formatOnPaste = 55, + formatOnType = 56, + glyphMargin = 57, + gotoLocation = 58, + hideCursorInOverviewRuler = 59, + hover = 60, + inDiffEditor = 61, + inlineSuggest = 62, + letterSpacing = 63, + lightbulb = 64, + lineDecorationsWidth = 65, + lineHeight = 66, + lineNumbers = 67, + lineNumbersMinChars = 68, + linkedEditing = 69, + links = 70, + matchBrackets = 71, + minimap = 72, + mouseStyle = 73, + mouseWheelScrollSensitivity = 74, + mouseWheelZoom = 75, + multiCursorMergeOverlapping = 76, + multiCursorModifier = 77, + multiCursorPaste = 78, + multiCursorLimit = 79, + occurrencesHighlight = 80, + overviewRulerBorder = 81, + overviewRulerLanes = 82, + padding = 83, + pasteAs = 84, + parameterHints = 85, + peekWidgetDefaultFocus = 86, + definitionLinkOpensInPeek = 87, + quickSuggestions = 88, + quickSuggestionsDelay = 89, + readOnly = 90, + readOnlyMessage = 91, + renameOnType = 92, + renderControlCharacters = 93, + renderFinalNewline = 94, + renderLineHighlight = 95, + renderLineHighlightOnlyWhenFocus = 96, + renderValidationDecorations = 97, + renderWhitespace = 98, + revealHorizontalRightPadding = 99, + roundedSelection = 100, + rulers = 101, + scrollbar = 102, + scrollBeyondLastColumn = 103, + scrollBeyondLastLine = 104, + scrollPredominantAxis = 105, + selectionClipboard = 106, + selectionHighlight = 107, + selectOnLineNumbers = 108, + showFoldingControls = 109, + showUnused = 110, + snippetSuggestions = 111, + smartSelect = 112, + smoothScrolling = 113, + stickyScroll = 114, + stickyTabStops = 115, + stopRenderingLineAfter = 116, + suggest = 117, + suggestFontSize = 118, + suggestLineHeight = 119, + suggestOnTriggerCharacters = 120, + suggestSelection = 121, + tabCompletion = 122, + tabIndex = 123, + unicodeHighlighting = 124, + unusualLineTerminators = 125, + useShadowDOM = 126, + useTabStops = 127, + wordBreak = 128, + wordSeparators = 129, + wordWrap = 130, + wordWrapBreakAfterCharacters = 131, + wordWrapBreakBeforeCharacters = 132, + wordWrapColumn = 133, + wordWrapOverride1 = 134, + wordWrapOverride2 = 135, + wrappingIndent = 136, + wrappingStrategy = 137, + showDeprecated = 138, + inlayHints = 139, + editorClassName = 140, + pixelRatio = 141, + tabFocusMode = 142, + layoutInfo = 143, + wrappingInfo = 144, + defaultColorDecorators = 145, + colorDecoratorsActivatedOn = 146, + inlineCompletionsAccessibilityVerbose = 147 } /** diff --git a/src/vs/editor/contrib/codeAction/test/browser/codeAction.test.ts b/src/vs/editor/contrib/codeAction/test/browser/codeAction.test.ts index 09699fd6d12..ac919bb6192 100644 --- a/src/vs/editor/contrib/codeAction/test/browser/codeAction.test.ts +++ b/src/vs/editor/contrib/codeAction/test/browser/codeAction.test.ts @@ -6,6 +6,7 @@ import * as assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Range } from 'vs/editor/common/core/range'; import { LanguageFeatureRegistry } from 'vs/editor/common/languageFeatureRegistry'; import * as languages from 'vs/editor/common/languages'; @@ -105,6 +106,8 @@ suite('CodeAction', () => { disposables.clear(); }); + ensureNoDisposablesAreLeakedInTestSuite(); + test('CodeActions are sorted by type, #38623', async () => { const provider = staticCodeActionProvider( @@ -130,7 +133,7 @@ suite('CodeAction', () => { new CodeActionItem(testData.tsLint.abc, provider) ]; - const { validActions: actions } = await getCodeActions(registry, model, new Range(1, 1, 2, 1), { type: languages.CodeActionTriggerType.Invoke, triggerAction: CodeActionTriggerSource.Default }, Progress.None, CancellationToken.None); + const { validActions: actions } = disposables.add(await getCodeActions(registry, model, new Range(1, 1, 2, 1), { type: languages.CodeActionTriggerType.Invoke, triggerAction: CodeActionTriggerSource.Default }, Progress.None, CancellationToken.None)); assert.strictEqual(actions.length, 6); assert.deepStrictEqual(actions, expected); }); @@ -145,20 +148,20 @@ suite('CodeAction', () => { disposables.add(registry.register('fooLang', provider)); { - const { validActions: actions } = await getCodeActions(registry, model, new Range(1, 1, 2, 1), { type: languages.CodeActionTriggerType.Auto, triggerAction: CodeActionTriggerSource.Default, filter: { include: new CodeActionKind('a') } }, Progress.None, CancellationToken.None); + const { validActions: actions } = disposables.add(await getCodeActions(registry, model, new Range(1, 1, 2, 1), { type: languages.CodeActionTriggerType.Auto, triggerAction: CodeActionTriggerSource.Default, filter: { include: new CodeActionKind('a') } }, Progress.None, CancellationToken.None)); assert.strictEqual(actions.length, 2); assert.strictEqual(actions[0].action.title, 'a'); assert.strictEqual(actions[1].action.title, 'a.b'); } { - const { validActions: actions } = await getCodeActions(registry, model, new Range(1, 1, 2, 1), { type: languages.CodeActionTriggerType.Auto, triggerAction: CodeActionTriggerSource.Default, filter: { include: new CodeActionKind('a.b') } }, Progress.None, CancellationToken.None); + const { validActions: actions } = disposables.add(await getCodeActions(registry, model, new Range(1, 1, 2, 1), { type: languages.CodeActionTriggerType.Auto, triggerAction: CodeActionTriggerSource.Default, filter: { include: new CodeActionKind('a.b') } }, Progress.None, CancellationToken.None)); assert.strictEqual(actions.length, 1); assert.strictEqual(actions[0].action.title, 'a.b'); } { - const { validActions: actions } = await getCodeActions(registry, model, new Range(1, 1, 2, 1), { type: languages.CodeActionTriggerType.Auto, triggerAction: CodeActionTriggerSource.Default, filter: { include: new CodeActionKind('a.b.c') } }, Progress.None, CancellationToken.None); + const { validActions: actions } = disposables.add(await getCodeActions(registry, model, new Range(1, 1, 2, 1), { type: languages.CodeActionTriggerType.Auto, triggerAction: CodeActionTriggerSource.Default, filter: { include: new CodeActionKind('a.b.c') } }, Progress.None, CancellationToken.None)); assert.strictEqual(actions.length, 0); } }); @@ -177,7 +180,7 @@ suite('CodeAction', () => { disposables.add(registry.register('fooLang', provider)); - const { validActions: actions } = await getCodeActions(registry, model, new Range(1, 1, 2, 1), { type: languages.CodeActionTriggerType.Auto, triggerAction: CodeActionTriggerSource.Default, filter: { include: new CodeActionKind('a') } }, Progress.None, CancellationToken.None); + const { validActions: actions } = disposables.add(await getCodeActions(registry, model, new Range(1, 1, 2, 1), { type: languages.CodeActionTriggerType.Auto, triggerAction: CodeActionTriggerSource.Default, filter: { include: new CodeActionKind('a') } }, Progress.None, CancellationToken.None)); assert.strictEqual(actions.length, 1); assert.strictEqual(actions[0].action.title, 'a'); }); @@ -191,13 +194,13 @@ suite('CodeAction', () => { disposables.add(registry.register('fooLang', provider)); { - const { validActions: actions } = await getCodeActions(registry, model, new Range(1, 1, 2, 1), { type: languages.CodeActionTriggerType.Auto, triggerAction: CodeActionTriggerSource.SourceAction }, Progress.None, CancellationToken.None); + const { validActions: actions } = disposables.add(await getCodeActions(registry, model, new Range(1, 1, 2, 1), { type: languages.CodeActionTriggerType.Auto, triggerAction: CodeActionTriggerSource.SourceAction }, Progress.None, CancellationToken.None)); assert.strictEqual(actions.length, 1); assert.strictEqual(actions[0].action.title, 'b'); } { - const { validActions: actions } = await getCodeActions(registry, model, new Range(1, 1, 2, 1), { type: languages.CodeActionTriggerType.Auto, triggerAction: CodeActionTriggerSource.Default, filter: { include: CodeActionKind.Source, includeSourceActions: true } }, Progress.None, CancellationToken.None); + const { validActions: actions } = disposables.add(await getCodeActions(registry, model, new Range(1, 1, 2, 1), { type: languages.CodeActionTriggerType.Auto, triggerAction: CodeActionTriggerSource.Default, filter: { include: CodeActionKind.Source, includeSourceActions: true } }, Progress.None, CancellationToken.None)); assert.strictEqual(actions.length, 1); assert.strictEqual(actions[0].action.title, 'a'); } @@ -213,13 +216,13 @@ suite('CodeAction', () => { disposables.add(registry.register('fooLang', provider)); { - const { validActions: actions } = await getCodeActions(registry, model, new Range(1, 1, 2, 1), { + const { validActions: actions } = disposables.add(await getCodeActions(registry, model, new Range(1, 1, 2, 1), { type: languages.CodeActionTriggerType.Auto, triggerAction: CodeActionTriggerSource.SourceAction, filter: { include: CodeActionKind.Source.append('test'), excludes: [CodeActionKind.Source], includeSourceActions: true, } - }, Progress.None, CancellationToken.None); + }, Progress.None, CancellationToken.None)); assert.strictEqual(actions.length, 1); assert.strictEqual(actions[0].action.title, 'b'); } @@ -250,12 +253,12 @@ suite('CodeAction', () => { })); { - const { validActions: actions } = await getCodeActions(registry, model, new Range(1, 1, 2, 1), { + const { validActions: actions } = disposables.add(await getCodeActions(registry, model, new Range(1, 1, 2, 1), { type: languages.CodeActionTriggerType.Auto, triggerAction: CodeActionTriggerSource.Refactor, filter: { include: baseType, excludes: [subType], } - }, Progress.None, CancellationToken.None); + }, Progress.None, CancellationToken.None)); assert.strictEqual(didInvoke, false); assert.strictEqual(actions.length, 1); assert.strictEqual(actions[0].action.title, 'a'); @@ -275,12 +278,12 @@ suite('CodeAction', () => { disposables.add(registry.register('fooLang', provider)); - const { validActions: actions } = await getCodeActions(registry, model, new Range(1, 1, 2, 1), { + const { validActions: actions } = disposables.add(await getCodeActions(registry, model, new Range(1, 1, 2, 1), { type: languages.CodeActionTriggerType.Auto, triggerAction: CodeActionTriggerSource.Refactor, filter: { include: CodeActionKind.QuickFix } - }, Progress.None, CancellationToken.None); + }, Progress.None, CancellationToken.None)); assert.strictEqual(actions.length, 0); assert.strictEqual(wasInvoked, false); }); diff --git a/src/vs/editor/contrib/hover/browser/hover.ts b/src/vs/editor/contrib/hover/browser/hover.ts index 2c1e2b2611e..d6353807346 100644 --- a/src/vs/editor/contrib/hover/browser/hover.ts +++ b/src/vs/editor/contrib/hover/browser/hover.ts @@ -5,7 +5,7 @@ import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ICodeEditor, IEditorMouseEvent, IPartialEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { EditorAction, EditorContributionInstantiation, registerEditorAction, registerEditorContribution, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; @@ -31,18 +31,18 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ResultKind } from 'vs/platform/keybinding/common/keybindingResolver'; import * as nls from 'vs/nls'; import 'vs/css!./hover'; +import { RunOnceScheduler } from 'vs/base/common/async'; // sticky hover widget which doesn't disappear on focus out and such const _sticky = false // || Boolean("true") // done "weirdly" so that a lint warning prevents you from pushing this ; -export class ModesHoverController implements IEditorContribution { +export class ModesHoverController extends Disposable implements IEditorContribution { public static readonly ID = 'editor.contrib.hover'; private readonly _toUnhook = new DisposableStore(); - private readonly _didChangeConfigurationHandler: IDisposable; private _contentWidget: ContentHoverController | null; @@ -54,7 +54,10 @@ export class ModesHoverController implements IEditorContribution { private _hoverClicked: boolean; private _isHoverEnabled!: boolean; private _isHoverSticky!: boolean; + private _hidingDelay!: number; private _hoverActivatedByColorDecoratorClick: boolean = false; + private _reactToEditorMouseMoveRunner: RunOnceScheduler; + private _mouseMoveEvent: IEditorMouseEvent | undefined; static get(editor: ICodeEditor): ModesHoverController | null { return editor.getContribution(ModesHoverController.ID); @@ -66,19 +69,24 @@ export class ModesHoverController implements IEditorContribution { @ILanguageService private readonly _languageService: ILanguageService, @IKeybindingService private readonly _keybindingService: IKeybindingService ) { + super(); this._isMouseDown = false; this._hoverClicked = false; this._contentWidget = null; this._glyphWidget = null; + this._reactToEditorMouseMoveRunner = this._register(new RunOnceScheduler(() => this._reactToEditorMouseMove(this._mouseMoveEvent), 0)); this._hookEvents(); - - this._didChangeConfigurationHandler = this._editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => { + this._register(this._editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => { if (e.hasChanged(EditorOption.hover)) { this._unhookEvents(); this._hookEvents(); } - }); + })); + this._register(this._editor.onMouseLeave(() => { + this._mouseMoveEvent = undefined; + this._reactToEditorMouseMoveRunner.cancel(); + })); } private _hookEvents(): void { @@ -87,6 +95,7 @@ export class ModesHoverController implements IEditorContribution { const hoverOpts = this._editor.getOption(EditorOption.hover); this._isHoverEnabled = hoverOpts.enabled; this._isHoverSticky = hoverOpts.sticky; + this._hidingDelay = hoverOpts.hidingDelay; if (this._isHoverEnabled) { this._toUnhook.add(this._editor.onMouseDown((e: IEditorMouseEvent) => this._onEditorMouseDown(e))); this._toUnhook.add(this._editor.onMouseUp((e: IEditorMouseEvent) => this._onEditorMouseUp(e))); @@ -152,46 +161,83 @@ export class ModesHoverController implements IEditorContribution { } } - private _onEditorMouseMove(mouseEvent: IEditorMouseEvent): void { + private _isMouseOverWidget(mouseEvent: IEditorMouseEvent): boolean { const target = mouseEvent.target; - - if (this._contentWidget?.isFocused || this._contentWidget?.isResizing) { - return; - } - - if (this._isMouseDown && this._hoverClicked) { - return; - } - - if (this._isHoverSticky && target.type === MouseTargetType.CONTENT_WIDGET && target.detail === ContentHoverWidget.ID) { - // mouse moved on top of content hover widget - return; - } - - if (this._isHoverSticky && this._contentWidget?.containsNode(mouseEvent.event.browserEvent.view?.document.activeElement) && !mouseEvent.event.browserEvent.view?.getSelection()?.isCollapsed) { - // selected text within content hover widget - return; - } - if ( - !this._isHoverSticky && target.type === MouseTargetType.CONTENT_WIDGET && target.detail === ContentHoverWidget.ID + this._isHoverSticky + && target.type === MouseTargetType.CONTENT_WIDGET + && target.detail === ContentHoverWidget.ID + ) { + // mouse moved on top of content hover widget + return true; + } + if ( + this._isHoverSticky + && this._contentWidget?.containsNode(mouseEvent.event.browserEvent.view?.document.activeElement) + && !mouseEvent.event.browserEvent.view?.getSelection()?.isCollapsed + ) { + // selected text within content hover widget + return true; + } + if ( + !this._isHoverSticky + && target.type === MouseTargetType.CONTENT_WIDGET + && target.detail === ContentHoverWidget.ID && this._contentWidget?.isColorPickerVisible ) { // though the hover is not sticky, the color picker needs to. - return; + return true; } - - if (this._isHoverSticky && target.type === MouseTargetType.OVERLAY_WIDGET && target.detail === MarginHoverWidget.ID) { + if ( + this._isHoverSticky + && target.type === MouseTargetType.OVERLAY_WIDGET + && target.detail === MarginHoverWidget.ID + ) { // mouse moved on top of overlay hover widget + return true; + } + return false; + } + + private _onEditorMouseMove(mouseEvent: IEditorMouseEvent): void { + this._mouseMoveEvent = mouseEvent; + if (this._contentWidget?.isFocused || this._contentWidget?.isResizing) { + return; + } + if (this._isMouseDown && this._hoverClicked) { return; } - if (this._isHoverSticky && this._contentWidget?.isVisibleFromKeyboard) { // Sticky mode is on and the hover has been shown via keyboard // so moving the mouse has no effect return; } + const mouseIsOverWidget = this._isMouseOverWidget(mouseEvent); + // If the mouse is over the widget and the hiding timeout is defined, then cancel it + if (mouseIsOverWidget) { + this._reactToEditorMouseMoveRunner.cancel(); + return; + } + + // If the mouse is not over the widget, and if sticky is on, + // then give it a grace period before reacting to the mouse event + if (this._contentWidget?.isVisible && this._isHoverSticky && this._hidingDelay > 0) { + if (!this._reactToEditorMouseMoveRunner.isScheduled()) { + this._reactToEditorMouseMoveRunner.schedule(this._hidingDelay); + } + return; + } + this._reactToEditorMouseMove(mouseEvent); + } + + private _reactToEditorMouseMove(mouseEvent: IEditorMouseEvent | undefined): void { + if (!mouseEvent) { + return; + } + + const target = mouseEvent.target; + const mouseOnDecorator = target.element?.classList.contains('colorpicker-color-decoration'); const decoratorActivatedOn = this._editor.getOption(EditorOption.colorDecoratorsActivatedOn); @@ -311,10 +357,10 @@ export class ModesHoverController implements IEditorContribution { return this._contentWidget?.isVisible; } - public dispose(): void { + public override dispose(): void { + super.dispose(); this._unhookEvents(); this._toUnhook.dispose(); - this._didChangeConfigurationHandler.dispose(); this._glyphWidget?.dispose(); this._contentWidget?.dispose(); } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/ghostTextWidget.ts b/src/vs/editor/contrib/inlineCompletions/browser/ghostTextWidget.ts index 0f7a28ca902..f690a7142d9 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/ghostTextWidget.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/ghostTextWidget.ts @@ -25,6 +25,7 @@ import { InlineDecorationType } from 'vs/editor/common/viewModel'; import { GhostText, GhostTextReplacement } from 'vs/editor/contrib/inlineCompletions/browser/ghostText'; import { ColumnRange, applyObservableDecorations } from 'vs/editor/contrib/inlineCompletions/browser/utils'; +export const GHOST_TEXT_DESCRIPTION = 'ghost-text'; export interface IGhostTextWidgetModel { readonly targetTextModel: IObservable; readonly ghostText: IObservable; @@ -100,7 +101,7 @@ export class GhostTextWidget extends Disposable { } if (lines.length > 0) { - addToAdditionalLines(lines, 'ghost-text'); + addToAdditionalLines(lines, GHOST_TEXT_DESCRIPTION); if (hiddenTextStartColumn === undefined && part.column <= textBufferLine.length) { hiddenTextStartColumn = part.column; } @@ -151,7 +152,7 @@ export class GhostTextWidget extends Disposable { decorations.push({ range: Range.fromPositions(new Position(uiState.lineNumber, p.column)), options: { - description: 'ghost-text', + description: GHOST_TEXT_DESCRIPTION, after: { content: p.text, inlineClassName: p.preview ? 'ghost-text-decoration-preview' : 'ghost-text-decoration', cursorStops: InjectedTextCursorStops.Left }, showIfCollapsed: true, } diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css b/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css index b446742a2db..d763a13b280 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css @@ -33,7 +33,8 @@ background-color: inherit; } -.monaco-editor .sticky-line-number .codicon { +.monaco-editor .sticky-line-number .codicon-folding-expanded, +.monaco-editor .sticky-line-number .codicon-folding-collapsed { float: right; transition: var(--vscode-editorStickyScroll-foldingOpacityTransition); } @@ -45,7 +46,7 @@ } .monaco-editor .sticky-line-number-inner { - display: block; + display: inline-block; text-align: right; } diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts index cc340f143e9..1b2a3a7b305 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts @@ -27,6 +27,8 @@ import { ILanguageFeatureDebounceService } from 'vs/editor/common/services/langu import * as dom from 'vs/base/browser/dom'; import { StickyRange } from 'vs/editor/contrib/stickyScroll/browser/stickyScrollElement'; import { IMouseEvent, StandardMouseEvent } from 'vs/base/browser/mouseEvent'; +import { FoldingController } from 'vs/editor/contrib/folding/browser/folding'; +import { FoldingModel, toggleCollapseState } from 'vs/editor/contrib/folding/browser/foldingModel'; export interface IStickyScrollController { get stickyScrollCandidateProvider(): IStickyLineCandidateProvider; @@ -49,6 +51,7 @@ export class StickyScrollController extends Disposable implements IEditorContrib private readonly _sessionStore: DisposableStore = new DisposableStore(); private _widgetState: StickyScrollWidgetState; + private _foldingModel: FoldingModel | null = null; private _maxStickyLines: number = Number.MAX_SAFE_INTEGER; private _stickyRangeProjectedOnEditor: IRange | undefined; @@ -253,7 +256,7 @@ export class StickyScrollController extends Disposable implements IEditorContrib } if (mouseEvent.shiftKey) { // shift click - const lineIndex = this._stickyScrollWidget.getStickyLineIndexFromChildDomNode(mouseEvent.target); + const lineIndex = this._stickyScrollWidget.getLineIndexFromChildDomNode(mouseEvent.target); if (lineIndex === null) { return; } @@ -261,6 +264,17 @@ export class StickyScrollController extends Disposable implements IEditorContrib this._revealLineInCenterIfOutsideViewport(position); return; } + const isInFoldingIconDomNode = this._stickyScrollWidget.isInFoldingIconDomNode(mouseEvent.target); + if (isInFoldingIconDomNode) { + // clicked on folding icon + const lineNumber = this._stickyScrollWidget.getLineNumberFromChildDomNode(mouseEvent.target); + this._toggleFoldingRegionForLine(lineNumber); + return; + } + const isInStickyLine = this._stickyScrollWidget.isInStickyLine(mouseEvent.target); + if (!isInStickyLine) { + return; + } // normal click let position = this._stickyScrollWidget.getEditorPositionFromNode(mouseEvent.target); if (!position) { @@ -275,7 +289,7 @@ export class StickyScrollController extends Disposable implements IEditorContrib })); this._register(dom.addStandardDisposableListener(stickyScrollWidgetDomNode, dom.EventType.MOUSE_MOVE, (mouseEvent: IMouseEvent) => { if (mouseEvent.shiftKey) { - const currentEndForLineIndex = this._stickyScrollWidget.getStickyLineIndexFromChildDomNode(mouseEvent.target); + const currentEndForLineIndex = this._stickyScrollWidget.getLineIndexFromChildDomNode(mouseEvent.target); if (currentEndForLineIndex === null || this._showEndForLine !== null && this._showEndForLine === currentEndForLineIndex) { return; } @@ -373,6 +387,25 @@ export class StickyScrollController extends Disposable implements IEditorContrib }); } + private _toggleFoldingRegionForLine(line: number | null) { + if (!this._foldingModel || line === null) { + return; + } + const stickyLine = this._stickyScrollWidget.getStickyLineForLine(line); + const foldingIcon = stickyLine?.foldingIcon; + if (!foldingIcon) { + return; + } + toggleCollapseState(this._foldingModel, Number.MAX_VALUE, [line]); + foldingIcon.isCollapsed = !foldingIcon.isCollapsed; + const scrollTop = (foldingIcon.isCollapsed ? + this._editor.getTopForLineNumber(foldingIcon.foldingEndLine) + : this._editor.getTopForLineNumber(foldingIcon.foldingStartLine)) + - this._editor.getOption(EditorOption.lineHeight) * stickyLine.index + 1; + this._editor.setScrollTop(scrollTop); + this._renderStickyScroll(line); + } + private _readConfiguration() { const options = this._editor.getOption(EditorOption.stickyScroll); if (options.enabled === false) { @@ -421,7 +454,8 @@ export class StickyScrollController extends Disposable implements IEditorContrib private _onTokensChange(event: IModelTokensChangedEvent) { if (this._needsUpdate(event)) { - this._renderStickyScroll(); + // Rebuilding the whole widget from line -1 + this._renderStickyScroll(-1); } } @@ -432,30 +466,32 @@ export class StickyScrollController extends Disposable implements IEditorContrib this._maxStickyLines = Math.round(theoreticalLines * .25); } - private _renderStickyScroll() { + private async _renderStickyScroll(rebuildFromLine: number = Infinity) { const model = this._editor.getModel(); if (!model || model.isTooLargeForTokenization()) { - this._stickyScrollWidget.setState(undefined); + this._foldingModel = null; + this._stickyScrollWidget.setState(undefined, null, rebuildFromLine); return; } const stickyLineVersion = this._stickyLineCandidateProvider.getVersionId(); if (stickyLineVersion === undefined || stickyLineVersion === model.getVersionId()) { + this._foldingModel = await FoldingController.get(this._editor)?.getFoldingModel() ?? null; this._widgetState = this.findScrollWidgetState(); this._stickyScrollVisibleContextKey.set(!(this._widgetState.startLineNumbers.length === 0)); if (!this._focused) { - this._stickyScrollWidget.setState(this._widgetState); + this._stickyScrollWidget.setState(this._widgetState, this._foldingModel, rebuildFromLine); } else { // Suppose that previously the sticky scroll widget had height 0, then if there are visible lines, set the last line as focused if (this._focusedStickyElementIndex === -1) { - this._stickyScrollWidget.setState(this._widgetState); + this._stickyScrollWidget.setState(this._widgetState, this._foldingModel, rebuildFromLine); this._focusedStickyElementIndex = this._stickyScrollWidget.lineNumberCount - 1; if (this._focusedStickyElementIndex !== -1) { this._stickyScrollWidget.focusLineWithIndex(this._focusedStickyElementIndex); } } else { const focusedStickyElementLineNumber = this._stickyScrollWidget.lineNumbers[this._focusedStickyElementIndex]; - this._stickyScrollWidget.setState(this._widgetState); + this._stickyScrollWidget.setState(this._widgetState, this._foldingModel, rebuildFromLine); // Suppose that after setting the state, there are no sticky lines, set the focused index to -1 if (this._stickyScrollWidget.lineNumberCount === 0) { this._focusedStickyElementIndex = -1; diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts index 763c3c862db..46dd3875f89 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts @@ -5,6 +5,7 @@ import * as dom from 'vs/base/browser/dom'; import { createTrustedTypesPolicy } from 'vs/base/browser/trustedTypes'; +import { equals } from 'vs/base/common/arrays'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ThemeIcon } from 'vs/base/common/themables'; import 'vs/css!./stickyScroll'; @@ -16,9 +17,8 @@ import { Position } from 'vs/editor/common/core/position'; import { StringBuilder } from 'vs/editor/common/core/stringBuilder'; import { LineDecoration } from 'vs/editor/common/viewLayout/lineDecorations'; import { CharacterMapping, RenderLineInput, renderViewLine } from 'vs/editor/common/viewLayout/viewLineRenderer'; -import { FoldingController } from 'vs/editor/contrib/folding/browser/folding'; import { foldingCollapsedIcon, foldingExpandedIcon } from 'vs/editor/contrib/folding/browser/foldingDecorations'; -import { FoldingModel, toggleCollapseState } from 'vs/editor/contrib/folding/browser/foldingModel'; +import { FoldingModel } from 'vs/editor/contrib/folding/browser/foldingModel'; export class StickyScrollWidgetState { constructor( @@ -27,10 +27,21 @@ export class StickyScrollWidgetState { readonly lastLineRelativePosition: number, readonly showEndForLine: number | null = null ) { } + + equals(other: StickyScrollWidgetState | undefined): boolean { + return !!other + && this.lastLineRelativePosition === other.lastLineRelativePosition + && this.showEndForLine === other.showEndForLine + && equals(this.startLineNumbers, other.startLineNumbers) + && equals(this.endLineNumbers, other.endLineNumbers); + } } const _ttPolicy = createTrustedTypesPolicy('stickyScrollViewLayer', { createHTML: value => value }); -const STICKY_LINE_INDEX_ATTR = 'data-sticky-line-index'; +const STICKY_INDEX_ATTR = 'data-sticky-line-index'; +const STICKY_IS_LINE_ATTR = 'data-sticky-is-line'; +const STICKY_IS_LINE_NUMBER_ATTR = 'data-sticky-is-line-number'; +const STICKY_IS_FOLDING_ICON_ATTR = 'data-sticky-is-folding-icon'; export class StickyScrollWidget extends Disposable implements IOverlayWidget { @@ -40,6 +51,7 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { private readonly _linesDomNodeScrollable: HTMLElement = document.createElement('div'); private readonly _linesDomNode: HTMLElement = document.createElement('div'); + private _previousState: StickyScrollWidgetState | undefined; private _lineHeight: number = this._editor.getOption(EditorOption.lineHeight); private _stickyLines: RenderedStickyLine[] = []; private _lineNumbers: number[] = []; @@ -106,11 +118,21 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { return this._lineNumbers.length; } + getStickyLineForLine(lineNumber: number): RenderedStickyLine | undefined { + return this._stickyLines.find(stickyLine => stickyLine.lineNumber === lineNumber); + } + getCurrentLines(): readonly number[] { return this._lineNumbers; } - setState(state: StickyScrollWidgetState | undefined): void { + setState(state: StickyScrollWidgetState | undefined, foldingModel: FoldingModel | null, rebuildFromLine: number = Infinity): void { + if (((!this._previousState && !state) || (this._previousState && this._previousState.equals(state))) + && rebuildFromLine === Infinity) { + return; + } + this._previousState = state; + const previousStickyLines = this._stickyLines; this._clearStickyWidget(); if (!state || !this._editor._getViewModel()) { return; @@ -128,7 +150,7 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { this._lastLineRelativePosition = 0; this._lineNumbers = []; } - this._renderRootNode(); + this._renderRootNode(previousStickyLines, foldingModel, rebuildFromLine); } private _updateWidgetWidth(): void { @@ -161,18 +183,20 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { } } - private async _renderRootNode(): Promise { + private async _renderRootNode(previousStickyLines: RenderedStickyLine[], foldingModel: FoldingModel | null, rebuildFromLine: number = Infinity): Promise { - const foldingModel = await FoldingController.get(this._editor)?.getFoldingModel(); const layoutInfo = this._editor.getLayoutInfo(); for (const [index, line] of this._lineNumbers.entries()) { - const renderedStickyLine = this._renderChildNode(index, line, layoutInfo, foldingModel); - if (!renderedStickyLine) { + const previousStickyLine = previousStickyLines[index]; + const stickyLine = (line >= rebuildFromLine || previousStickyLine?.lineNumber !== line) + ? this._renderChildNode(index, line, foldingModel, layoutInfo) + : this._updateTopAndZIndexOfStickyLine(previousStickyLine); + if (!stickyLine) { continue; } - this._linesDomNode.appendChild(renderedStickyLine.lineDomNode); - this._lineNumbersDomNode.appendChild(renderedStickyLine.lineNumberDomNode); - this._stickyLines.push(renderedStickyLine); + this._linesDomNode.appendChild(stickyLine.lineDomNode); + this._lineNumbersDomNode.appendChild(stickyLine.lineNumberDomNode); + this._stickyLines.push(stickyLine); } if (foldingModel) { this._setFoldingHoverListeners(); @@ -211,7 +235,7 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { })); } - private _renderChildNode(index: number, line: number, layoutInfo: EditorLayoutInfo, foldingModel: FoldingModel | null | undefined): RenderedStickyLine | undefined { + private _renderChildNode(index: number, line: number, foldingModel: FoldingModel | null, layoutInfo: EditorLayoutInfo): RenderedStickyLine | undefined { const viewModel = this._editor._getViewModel(); if (!viewModel) { return; @@ -246,12 +270,18 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { } const lineHTMLNode = document.createElement('span'); + lineHTMLNode.setAttribute(STICKY_INDEX_ATTR, String(index)); + lineHTMLNode.setAttribute(STICKY_IS_LINE_ATTR, ''); + lineHTMLNode.setAttribute('role', 'listitem'); + lineHTMLNode.tabIndex = 0; lineHTMLNode.className = 'sticky-line-content'; lineHTMLNode.classList.add(`stickyLine${line}`); lineHTMLNode.style.lineHeight = `${this._lineHeight}px`; lineHTMLNode.innerHTML = newLine as string; const lineNumberHTMLNode = document.createElement('span'); + lineNumberHTMLNode.setAttribute(STICKY_INDEX_ATTR, String(index)); + lineNumberHTMLNode.setAttribute(STICKY_IS_LINE_NUMBER_ATTR, ''); lineNumberHTMLNode.className = 'sticky-line-number'; lineNumberHTMLNode.style.lineHeight = `${this._lineHeight}px`; const lineNumbersWidth = layoutInfo.contentLeft; @@ -269,21 +299,28 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { innerLineNumberHTML.style.paddingLeft = `${layoutInfo.lineNumbersLeft}px`; lineNumberHTMLNode.appendChild(innerLineNumberHTML); - const foldingIcon = this._renderFoldingIconForLine(lineNumberHTMLNode, foldingModel, index, line); + const foldingIcon = this._renderFoldingIconForLine(foldingModel, line); + if (foldingIcon) { + lineNumberHTMLNode.appendChild(foldingIcon.domNode); + } this._editor.applyFontInfo(lineHTMLNode); this._editor.applyFontInfo(innerLineNumberHTML); - lineHTMLNode.setAttribute('role', 'listitem'); - lineHTMLNode.setAttribute(STICKY_LINE_INDEX_ATTR, String(index)); - lineHTMLNode.tabIndex = 0; lineNumberHTMLNode.style.lineHeight = `${this._lineHeight}px`; lineHTMLNode.style.lineHeight = `${this._lineHeight}px`; lineNumberHTMLNode.style.height = `${this._lineHeight}px`; lineHTMLNode.style.height = `${this._lineHeight}px`; - // Special case for the last line of sticky scroll + const renderedLine = new RenderedStickyLine(index, line, lineHTMLNode, lineNumberHTMLNode, foldingIcon, renderOutput.characterMapping); + return this._updateTopAndZIndexOfStickyLine(renderedLine); + } + + private _updateTopAndZIndexOfStickyLine(stickyLine: RenderedStickyLine): RenderedStickyLine { + const index = stickyLine.index; + const lineHTMLNode = stickyLine.lineDomNode; + const lineNumberHTMLNode = stickyLine.lineNumberDomNode; const isLastLine = index === this._lineNumbers.length - 1; const lastLineZIndex = '0'; @@ -291,14 +328,14 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { lineHTMLNode.style.zIndex = isLastLine ? lastLineZIndex : intermediateLineZIndex; lineNumberHTMLNode.style.zIndex = isLastLine ? lastLineZIndex : intermediateLineZIndex; - const lastLineTop = `${index * this._lineHeight + this._lastLineRelativePosition + (foldingIcon?.isCollapsed ? 1 : 0)}px`; + const lastLineTop = `${index * this._lineHeight + this._lastLineRelativePosition + (stickyLine.foldingIcon?.isCollapsed ? 1 : 0)}px`; const intermediateLineTop = `${index * this._lineHeight}px`; lineHTMLNode.style.top = isLastLine ? lastLineTop : intermediateLineTop; lineNumberHTMLNode.style.top = isLastLine ? lastLineTop : intermediateLineTop; - return new RenderedStickyLine(line, lineHTMLNode, lineNumberHTMLNode, foldingIcon, renderOutput.characterMapping); + return stickyLine; } - private _renderFoldingIconForLine(container: HTMLSpanElement, foldingModel: FoldingModel | null | undefined, index: number, line: number): StickyFoldingIcon | undefined { + private _renderFoldingIconForLine(foldingModel: FoldingModel | null, line: number): StickyFoldingIcon | undefined { const showFoldingControls: 'mouseover' | 'always' | 'never' = this._editor.getOption(EditorOption.showFoldingControls); if (!foldingModel || showFoldingControls === 'never') { return; @@ -311,20 +348,9 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { return; } const isCollapsed = foldingRegions.isCollapsed(indexOfFoldingRegion); - const foldingIcon = new StickyFoldingIcon(isCollapsed, this._lineHeight); - container.append(foldingIcon.domNode); + const foldingIcon = new StickyFoldingIcon(isCollapsed, startLineNumber, foldingRegions.getEndLineNumber(indexOfFoldingRegion), this._lineHeight); foldingIcon.setVisible(this._isOnGlyphMargin ? true : (isCollapsed || showFoldingControls === 'always')); - - this._foldingIconStore.add(dom.addDisposableListener(foldingIcon.domNode, dom.EventType.CLICK, () => { - toggleCollapseState(foldingModel, Number.MAX_VALUE, [line]); - foldingIcon.isCollapsed = !isCollapsed; - const scrollTop = - (isCollapsed ? - this._editor.getTopForLineNumber(startLineNumber) - : this._editor.getTopForLineNumber(foldingRegions.getEndLineNumber(indexOfFoldingRegion))) - - this._lineHeight * index + 1; - this._editor.setScrollTop(scrollTop); - })); + foldingIcon.domNode.setAttribute(STICKY_IS_FOLDING_ICON_ATTR, ''); return foldingIcon; } @@ -383,7 +409,7 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { } private _getRenderedStickyLineFromChildDomNode(domNode: HTMLElement | null): RenderedStickyLine | null { - const index = this.getStickyLineIndexFromChildDomNode(domNode); + const index = this.getLineIndexFromChildDomNode(domNode); if (index === null || index < 0 || index >= this._stickyLines.length) { return null; } @@ -391,23 +417,51 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { } /** - * Given a child dom node, tries to find the line number attribute - * that was stored in the node. Returns null if none is found. + * Given a child dom node, tries to find the line number attribute that was stored in the node. + * @returns the attribute value or null if none is found. */ - getStickyLineIndexFromChildDomNode(domNode: HTMLElement | null): number | null { + getLineIndexFromChildDomNode(domNode: HTMLElement | null): number | null { + const lineIndex = this._getAttributeValue(domNode, STICKY_INDEX_ATTR); + return lineIndex ? parseInt(lineIndex, 10) : null; + } + + /** + * Given a child dom node, tries to find if it is (contained in) a sticky line. + * @returns a boolean. + */ + isInStickyLine(domNode: HTMLElement | null): boolean { + const isInLine = this._getAttributeValue(domNode, STICKY_IS_LINE_ATTR); + return isInLine !== undefined; + } + + /** + * Given a child dom node, tries to find if this dom node is (contained in) a sticky folding icon. + * @returns a boolean. + */ + isInFoldingIconDomNode(domNode: HTMLElement | null): boolean { + const isInFoldingIcon = this._getAttributeValue(domNode, STICKY_IS_FOLDING_ICON_ATTR); + return isInFoldingIcon !== undefined; + } + + /** + * Given the dom node, finds if it or its parent sequence contains the given attribute. + * @returns the attribute value or undefined. + */ + private _getAttributeValue(domNode: HTMLElement | null, attribute: string): string | undefined { while (domNode && domNode !== this._rootDomNode) { - const line = domNode.getAttribute(STICKY_LINE_INDEX_ATTR); - if (line) { - return parseInt(line, 10); + const line = domNode.getAttribute(attribute); + if (line !== null) { + return line; } domNode = domNode.parentElement; } - return null; + return; } } class RenderedStickyLine { constructor( + public readonly index: number, public readonly lineNumber: number, public readonly lineDomNode: HTMLElement, public readonly lineNumberDomNode: HTMLElement, @@ -422,6 +476,8 @@ class StickyFoldingIcon { constructor( public isCollapsed: boolean, + public foldingStartLine: number, + public foldingEndLine: number, public dimension: number ) { this.domNode = document.createElement('div'); diff --git a/src/vs/editor/contrib/suggest/test/browser/suggestInlineCompletions.test.ts b/src/vs/editor/contrib/suggest/test/browser/suggestInlineCompletions.test.ts index b8e16dc8ef3..c80ef6fed15 100644 --- a/src/vs/editor/contrib/suggest/test/browser/suggestInlineCompletions.test.ts +++ b/src/vs/editor/contrib/suggest/test/browser/suggestInlineCompletions.test.ts @@ -8,6 +8,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { CompletionContext, CompletionItem, CompletionItemKind, CompletionItemProvider, CompletionList, InlineCompletionTriggerKind, ProviderResult } from 'vs/editor/common/languages'; @@ -71,6 +72,8 @@ suite('Suggest Inline Completions', function () { }); + ensureNoDisposablesAreLeakedInTestSuite(); + test('Aggressive inline completions when typing within line #146948', async function () { const completions: SuggestInlineCompletions = insta.createInstance(SuggestInlineCompletions, (id) => editor.getOption(id)); @@ -79,6 +82,7 @@ suite('Suggest Inline Completions', function () { // (1,3), end of word -> suggestions const result = await completions.provideInlineCompletions(model, new Position(1, 3), { triggerKind: InlineCompletionTriggerKind.Explicit, selectedSuggestionInfo: undefined }, CancellationToken.None); assert.strictEqual(result?.items.length, 3); + completions.freeInlineCompletions(result); } { // (1,2), middle of word -> NO suggestions diff --git a/src/vs/editor/test/browser/controller/cursor.test.ts b/src/vs/editor/test/browser/controller/cursor.test.ts index 5254ef1add9..a2ec4986989 100644 --- a/src/vs/editor/test/browser/controller/cursor.test.ts +++ b/src/vs/editor/test/browser/controller/cursor.test.ts @@ -1429,6 +1429,9 @@ suite('Editor Controller', () => { function setupAutoClosingLanguage() { disposables.add(languageService.registerLanguage({ id: autoClosingLanguageId })); disposables.add(languageConfigurationService.register(autoClosingLanguageId, { + comments: { + blockComment: ['/*', '*/'] + }, autoClosingPairs: [ { open: '{', close: '}' }, { open: '[', close: ']' }, @@ -5343,6 +5346,24 @@ suite('Editor Controller', () => { }); }); + test('autoClosingPairs - doc comments can be turned off', () => { + usingCursor({ + text: [ + '', + ], + languageId: autoClosingLanguageId, + editorOpts: { + autoClosingComments: 'never' + } + }, (editor, model, viewModel) => { + + model.setValue('/*'); + viewModel.setSelections('test', [new Selection(1, 3, 1, 3)]); + viewModel.type('*', 'keyboard'); + assert.strictEqual(model.getLineContent(1), '/**'); + }); + }); + test('issue #72177: multi-character autoclose with conflicting patterns', () => { const languageId = 'autoClosingModeMultiChar'; diff --git a/src/vs/editor/test/browser/services/openerService.test.ts b/src/vs/editor/test/browser/services/openerService.test.ts index d823f89d285..4f1b579d5e7 100644 --- a/src/vs/editor/test/browser/services/openerService.test.ts +++ b/src/vs/editor/test/browser/services/openerService.test.ts @@ -5,6 +5,7 @@ import * as assert from 'assert'; import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { OpenerService } from 'vs/editor/browser/services/openerService'; import { TestCodeEditorService } from 'vs/editor/test/browser/editorTestServices'; import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; @@ -33,6 +34,8 @@ suite('OpenerService', function () { lastCommand = undefined; }); + const store = ensureNoDisposablesAreLeakedInTestSuite(); + test('delegate to editorService, scheme:///fff', async function () { const openerService = new OpenerService(editorService, NullCommandService); await openerService.open(URI.parse('another:///somepath')); @@ -83,7 +86,7 @@ suite('OpenerService', function () { const openerService = new OpenerService(editorService, commandService); const id = `aCommand${Math.random()}`; - CommandsRegistry.registerCommand(id, function () { }); + store.add(CommandsRegistry.registerCommand(id, function () { })); assert.strictEqual(lastCommand, undefined); await openerService.open(URI.parse('command:' + id)); @@ -91,11 +94,11 @@ suite('OpenerService', function () { }); - test('delegate to commandsService, command:someid', async function () { + test('delegate to commandsService, command:someid, 2', async function () { const openerService = new OpenerService(editorService, commandService); const id = `aCommand${Math.random()}`; - CommandsRegistry.registerCommand(id, function () { }); + store.add(CommandsRegistry.registerCommand(id, function () { })); await openerService.open(URI.parse('command:' + id).with({ query: '\"123\"' }), { allowCommands: true }); assert.strictEqual(lastCommand!.id, id); @@ -121,7 +124,7 @@ suite('OpenerService', function () { test('links are protected by validators', async function () { const openerService = new OpenerService(editorService, commandService); - openerService.registerValidator({ shouldOpen: () => Promise.resolve(false) }); + store.add(openerService.registerValidator({ shouldOpen: () => Promise.resolve(false) })); const httpResult = await openerService.open(URI.parse('https://www.microsoft.com')); const httpsResult = await openerService.open(URI.parse('https://www.microsoft.com')); @@ -132,15 +135,15 @@ suite('OpenerService', function () { test('links validated by validators go to openers', async function () { const openerService = new OpenerService(editorService, commandService); - openerService.registerValidator({ shouldOpen: () => Promise.resolve(true) }); + store.add(openerService.registerValidator({ shouldOpen: () => Promise.resolve(true) })); let openCount = 0; - openerService.registerOpener({ + store.add(openerService.registerOpener({ open: (resource: URI) => { openCount++; return Promise.resolve(true); } - }); + })); await openerService.open(URI.parse('http://microsoft.com')); assert.strictEqual(openCount, 1); @@ -151,13 +154,13 @@ suite('OpenerService', function () { test('links aren\'t manipulated before being passed to validator: PR #118226', async function () { const openerService = new OpenerService(editorService, commandService); - openerService.registerValidator({ + store.add(openerService.registerValidator({ shouldOpen: (resource) => { // We don't want it to convert strings into URIs assert.strictEqual(resource instanceof URI, false); return Promise.resolve(false); } - }); + })); await openerService.open('https://wwww.microsoft.com'); await openerService.open('https://www.microsoft.com??params=CountryCode%3DUSA%26Name%3Dvscode"'); }); diff --git a/src/vs/editor/test/browser/widget/diffEditorWidget2.test.ts b/src/vs/editor/test/browser/widget/diffEditorWidget.test.ts similarity index 100% rename from src/vs/editor/test/browser/widget/diffEditorWidget2.test.ts rename to src/vs/editor/test/browser/widget/diffEditorWidget.test.ts diff --git a/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts b/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts index f1d41730546..262032dd9e4 100644 --- a/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts +++ b/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts @@ -14,6 +14,7 @@ import { PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeText import { NodeColor, SENTINEL, TreeNode } from 'vs/editor/common/model/pieceTreeTextBuffer/rbTreeBase'; import { createTextModel } from 'vs/editor/test/common/testTextModel'; import { splitLines } from 'vs/base/common/strings'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; const alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\r\n'; @@ -151,13 +152,13 @@ function testLineStarts(str: string, pieceTable: PieceTreeBase) { } } -function createTextBuffer(val: string[], normalizeEOL: boolean = true): PieceTreeBase { +function createTextBuffer(val: string[], normalizeEOL: boolean = true): PieceTreeTextBuffer { const bufferBuilder = new PieceTreeTextBufferBuilder(); for (const chunk of val) { bufferBuilder.acceptChunk(chunk); } const factory = bufferBuilder.finish(normalizeEOL); - return (factory.create(DefaultEndOfLine.LF).textBuffer).getPieceTree(); + return (factory.create(DefaultEndOfLine.LF).textBuffer); } function assertTreeInvariants(T: PieceTreeBase): void { @@ -212,10 +213,14 @@ function assertValidTree(T: PieceTreeBase): void { //#endregion suite('inserts and deletes', () => { + const ds = ensureNoDisposablesAreLeakedInTestSuite(); + test('basic insert/delete', () => { - const pieceTable = createTextBuffer([ + const pieceTree = createTextBuffer([ 'This is a document with some text.' ]); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(34, 'This is some more text to insert at offset 34.'); assert.strictEqual( @@ -231,8 +236,9 @@ suite('inserts and deletes', () => { }); test('more inserts', () => { - const pt = createTextBuffer(['']); - + const pieceTree = createTextBuffer(['']); + ds.add(pieceTree); + const pt = pieceTree.getPieceTree(); pt.insert(0, 'AAA'); assert.strictEqual(pt.getLinesRawContent(), 'AAA'); pt.insert(0, 'BBB'); @@ -245,7 +251,10 @@ suite('inserts and deletes', () => { }); test('more deletes', () => { - const pt = createTextBuffer(['012345678']); + const pieceTree = createTextBuffer(['012345678']); + ds.add(pieceTree); + const pt = pieceTree.getPieceTree(); + pt.delete(8, 1); assert.strictEqual(pt.getLinesRawContent(), '01234567'); pt.delete(0, 1); @@ -261,7 +270,10 @@ suite('inserts and deletes', () => { test('random test 1', () => { let str = ''; - const pieceTable = createTextBuffer(['']); + const pieceTree = createTextBuffer(['']); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); + pieceTable.insert(0, 'ceLPHmFzvCtFeHkCBej '); str = str.substring(0, 0) + 'ceLPHmFzvCtFeHkCBej ' + str.substring(0); assert.strictEqual(pieceTable.getLinesRawContent(), str); @@ -280,7 +292,9 @@ suite('inserts and deletes', () => { test('random test 2', () => { let str = ''; - const pieceTable = createTextBuffer(['']); + const pieceTree = createTextBuffer(['']); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(0, 'VgPG '); str = str.substring(0, 0) + 'VgPG ' + str.substring(0); pieceTable.insert(2, 'DdWF '); @@ -298,7 +312,9 @@ suite('inserts and deletes', () => { test('random test 3', () => { let str = ''; - const pieceTable = createTextBuffer(['']); + const pieceTree = createTextBuffer(['']); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(0, 'gYSz'); str = str.substring(0, 0) + 'gYSz' + str.substring(0); pieceTable.insert(1, 'mDQe'); @@ -314,7 +330,9 @@ suite('inserts and deletes', () => { test('random delete 1', () => { let str = ''; - const pieceTable = createTextBuffer(['']); + const pieceTree = createTextBuffer(['']); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(0, 'vfb'); str = str.substring(0, 0) + 'vfb' + str.substring(0); @@ -347,7 +365,9 @@ suite('inserts and deletes', () => { test('random delete 2', () => { let str = ''; - const pieceTable = createTextBuffer(['']); + const pieceTree = createTextBuffer(['']); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(0, 'IDT'); str = str.substring(0, 0) + 'IDT' + str.substring(0); @@ -373,7 +393,9 @@ suite('inserts and deletes', () => { test('random delete 3', () => { let str = ''; - const pieceTable = createTextBuffer(['']); + const pieceTree = createTextBuffer(['']); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(0, 'PqM'); str = str.substring(0, 0) + 'PqM' + str.substring(0); pieceTable.delete(1, 2); @@ -406,7 +428,9 @@ suite('inserts and deletes', () => { test('random insert/delete \\r bug 1', () => { let str = 'a'; - const pieceTable = createTextBuffer(['a']); + const pieceTree = createTextBuffer(['a']); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.delete(0, 1); str = str.substring(0, 0) + str.substring(0 + 1); pieceTable.insert(0, '\r\r\n\n'); @@ -432,7 +456,9 @@ suite('inserts and deletes', () => { test('random insert/delete \\r bug 2', () => { let str = 'a'; - const pieceTable = createTextBuffer(['a']); + const pieceTree = createTextBuffer(['a']); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(1, '\naa\r'); str = str.substring(0, 1) + '\naa\r' + str.substring(1); pieceTable.delete(0, 4); @@ -460,7 +486,9 @@ suite('inserts and deletes', () => { test('random insert/delete \\r bug 3', () => { let str = 'a'; - const pieceTable = createTextBuffer(['a']); + const pieceTree = createTextBuffer(['a']); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(0, '\r\na\r'); str = str.substring(0, 0) + '\r\na\r' + str.substring(0); pieceTable.delete(2, 3); @@ -489,7 +517,9 @@ suite('inserts and deletes', () => { test('random insert/delete \\r bug 4s', () => { let str = 'a'; - const pieceTable = createTextBuffer(['a']); + const pieceTree = createTextBuffer(['a']); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.delete(0, 1); str = str.substring(0, 0) + str.substring(0 + 1); pieceTable.insert(0, '\naaa'); @@ -516,7 +546,9 @@ suite('inserts and deletes', () => { }); test('random insert/delete \\r bug 5', () => { let str = ''; - const pieceTable = createTextBuffer(['']); + const pieceTree = createTextBuffer(['']); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(0, '\n\n\n\r'); str = str.substring(0, 0) + '\n\n\n\r' + str.substring(0); pieceTable.insert(1, '\n\n\n\r'); @@ -544,8 +576,12 @@ suite('inserts and deletes', () => { }); suite('prefix sum for line feed', () => { + const ds = ensureNoDisposablesAreLeakedInTestSuite(); + test('basic', () => { - const pieceTable = createTextBuffer(['1\n2\n3\n4']); + const pieceTree = createTextBuffer(['1\n2\n3\n4']); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); assert.strictEqual(pieceTable.getLineCount(), 4); assert.deepStrictEqual(pieceTable.getPositionAt(0), new Position(1, 1)); @@ -567,7 +603,9 @@ suite('prefix sum for line feed', () => { }); test('append', () => { - const pieceTable = createTextBuffer(['a\nb\nc\nde']); + const pieceTree = createTextBuffer(['a\nb\nc\nde']); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(8, 'fh\ni\njk'); assert.strictEqual(pieceTable.getLineCount(), 6); @@ -577,7 +615,9 @@ suite('prefix sum for line feed', () => { }); test('insert', () => { - const pieceTable = createTextBuffer(['a\nb\nc\nde']); + const pieceTree = createTextBuffer(['a\nb\nc\nde']); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(7, 'fh\ni\njk'); assert.strictEqual(pieceTable.getLineCount(), 6); @@ -600,7 +640,10 @@ suite('prefix sum for line feed', () => { }); test('delete', () => { - const pieceTable = createTextBuffer(['a\nb\nc\ndefh\ni\njk']); + const pieceTree = createTextBuffer(['a\nb\nc\ndefh\ni\njk']); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); + pieceTable.delete(7, 2); assert.strictEqual(pieceTable.getLinesRawContent(), 'a\nb\nc\ndh\ni\njk'); @@ -624,7 +667,9 @@ suite('prefix sum for line feed', () => { }); test('add+delete 1', () => { - const pieceTable = createTextBuffer(['a\nb\nc\nde']); + const pieceTree = createTextBuffer(['a\nb\nc\nde']); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(8, 'fh\ni\njk'); pieceTable.delete(7, 2); @@ -650,7 +695,9 @@ suite('prefix sum for line feed', () => { test('insert random bug 1: prefixSumComputer.removeValues(start, cnt) cnt is 1 based.', () => { let str = ''; - const pieceTable = createTextBuffer(['']); + const pieceTree = createTextBuffer(['']); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(0, ' ZX \n Z\nZ\n YZ\nY\nZXX '); str = str.substring(0, 0) + @@ -667,7 +714,9 @@ suite('prefix sum for line feed', () => { test('insert random bug 2: prefixSumComputer initialize does not do deep copy of UInt32Array.', () => { let str = ''; - const pieceTable = createTextBuffer(['']); + const pieceTree = createTextBuffer(['']); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(0, 'ZYZ\nYY XY\nX \nZ Y \nZ '); str = str.substring(0, 0) + 'ZYZ\nYY XY\nX \nZ Y \nZ ' + str.substring(0); @@ -680,7 +729,9 @@ suite('prefix sum for line feed', () => { }); test('delete random bug 1: I forgot to update the lineFeedCnt when deletion is on one single piece.', () => { - const pieceTable = createTextBuffer(['']); + const pieceTree = createTextBuffer(['']); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(0, 'ba\na\nca\nba\ncbab\ncaa '); pieceTable.insert(13, 'cca\naabb\ncac\nccc\nab '); pieceTable.delete(5, 8); @@ -709,7 +760,9 @@ suite('prefix sum for line feed', () => { test('delete random bug rb tree 1', () => { let str = ''; - const pieceTable = createTextBuffer([str]); + const pieceTree = createTextBuffer([str]); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(0, 'YXXZ\n\nYY\n'); str = str.substring(0, 0) + 'YXXZ\n\nYY\n' + str.substring(0); pieceTable.delete(0, 5); @@ -724,7 +777,9 @@ suite('prefix sum for line feed', () => { test('delete random bug rb tree 2', () => { let str = ''; - const pieceTable = createTextBuffer([str]); + const pieceTree = createTextBuffer([str]); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(0, 'YXXZ\n\nYY\n'); str = str.substring(0, 0) + 'YXXZ\n\nYY\n' + str.substring(0); pieceTable.insert(0, 'ZXYY\nX\nZ\n'); @@ -744,7 +799,9 @@ suite('prefix sum for line feed', () => { test('delete random bug rb tree 3', () => { let str = ''; - const pieceTable = createTextBuffer([str]); + const pieceTree = createTextBuffer([str]); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(0, 'YXXZ\n\nYY\n'); str = str.substring(0, 0) + 'YXXZ\n\nYY\n' + str.substring(0); pieceTable.delete(7, 2); @@ -772,9 +829,13 @@ suite('prefix sum for line feed', () => { }); suite('offset 2 position', () => { + const ds = ensureNoDisposablesAreLeakedInTestSuite(); + test('random tests bug 1', () => { let str = ''; - const pieceTable = createTextBuffer(['']); + const pieceTree = createTextBuffer(['']); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(0, 'huuyYzUfKOENwGgZLqn '); str = str.substring(0, 0) + 'huuyYzUfKOENwGgZLqn ' + str.substring(0); pieceTable.delete(18, 2); @@ -796,8 +857,12 @@ suite('offset 2 position', () => { }); suite('get text in range', () => { + const ds = ensureNoDisposablesAreLeakedInTestSuite(); + test('getContentInRange', () => { - const pieceTable = createTextBuffer(['a\nb\nc\nde']); + const pieceTree = createTextBuffer(['a\nb\nc\nde']); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(8, 'fh\ni\njk'); pieceTable.delete(7, 2); // 'a\nb\nc\ndh\ni\njk' @@ -813,7 +878,9 @@ suite('get text in range', () => { test('random test value in range', () => { let str = ''; - const pieceTable = createTextBuffer([str]); + const pieceTree = createTextBuffer([str]); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(0, 'ZXXY'); str = str.substring(0, 0) + 'ZXXY' + str.substring(0); @@ -831,7 +898,9 @@ suite('get text in range', () => { }); test('random test value in range exception', () => { let str = ''; - const pieceTable = createTextBuffer([str]); + const pieceTree = createTextBuffer([str]); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(0, 'XZ\nZ'); str = str.substring(0, 0) + 'XZ\nZ' + str.substring(0); @@ -850,7 +919,9 @@ suite('get text in range', () => { test('random tests bug 1', () => { let str = ''; - const pieceTable = createTextBuffer(['']); + const pieceTree = createTextBuffer(['']); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(0, 'huuyYzUfKOENwGgZLqn '); str = str.substring(0, 0) + 'huuyYzUfKOENwGgZLqn ' + str.substring(0); pieceTable.delete(18, 2); @@ -871,7 +942,9 @@ suite('get text in range', () => { test('random tests bug 2', () => { let str = ''; - const pieceTable = createTextBuffer(['']); + const pieceTree = createTextBuffer(['']); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(0, 'xfouRDZwdAHjVXJAMV\n '); str = str.substring(0, 0) + 'xfouRDZwdAHjVXJAMV\n ' + str.substring(0); pieceTable.insert(16, 'dBGndxpFZBEAIKykYYx '); @@ -900,7 +973,10 @@ suite('get text in range', () => { }); test('get line content', () => { - const pieceTable = createTextBuffer(['1']); + const pieceTree = createTextBuffer(['1']); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); + assert.strictEqual(pieceTable.getLineRawContent(1), '1'); pieceTable.insert(1, '2'); assert.strictEqual(pieceTable.getLineRawContent(1), '12'); @@ -908,7 +984,10 @@ suite('get text in range', () => { }); test('get line content basic', () => { - const pieceTable = createTextBuffer(['1\n2\n3\n4']); + const pieceTree = createTextBuffer(['1\n2\n3\n4']); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); + assert.strictEqual(pieceTable.getLineRawContent(1), '1\n'); assert.strictEqual(pieceTable.getLineRawContent(2), '2\n'); assert.strictEqual(pieceTable.getLineRawContent(3), '3\n'); @@ -917,7 +996,9 @@ suite('get text in range', () => { }); test('get line content after inserts/deletes', () => { - const pieceTable = createTextBuffer(['a\nb\nc\nde']); + const pieceTree = createTextBuffer(['a\nb\nc\nde']); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(8, 'fh\ni\njk'); pieceTable.delete(7, 2); // 'a\nb\nc\ndh\ni\njk' @@ -933,7 +1014,9 @@ suite('get text in range', () => { test('random 1', () => { let str = ''; - const pieceTable = createTextBuffer(['']); + const pieceTree = createTextBuffer(['']); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(0, 'J eNnDzQpnlWyjmUu\ny '); str = str.substring(0, 0) + 'J eNnDzQpnlWyjmUu\ny ' + str.substring(0); @@ -948,7 +1031,9 @@ suite('get text in range', () => { test('random 2', () => { let str = ''; - const pieceTable = createTextBuffer(['']); + const pieceTree = createTextBuffer(['']); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(0, 'DZoQ tglPCRHMltejRI '); str = str.substring(0, 0) + 'DZoQ tglPCRHMltejRI ' + str.substring(0); pieceTable.insert(10, 'JRXiyYqJ qqdcmbfkKX '); @@ -967,8 +1052,12 @@ suite('get text in range', () => { }); suite('CRLF', () => { + const ds = ensureNoDisposablesAreLeakedInTestSuite(); + test('delete CR in CRLF 1', () => { - const pieceTable = createTextBuffer([''], false); + const pieceTree = createTextBuffer([''], false); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(0, 'a\r\nb'); pieceTable.delete(0, 2); @@ -977,7 +1066,9 @@ suite('CRLF', () => { }); test('delete CR in CRLF 2', () => { - const pieceTable = createTextBuffer([''], false); + const pieceTree = createTextBuffer([''], false); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(0, 'a\r\nb'); pieceTable.delete(2, 2); @@ -987,7 +1078,9 @@ suite('CRLF', () => { test('random bug 1', () => { let str = ''; - const pieceTable = createTextBuffer([''], false); + const pieceTree = createTextBuffer([''], false); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(0, '\n\n\r\r'); str = str.substring(0, 0) + '\n\n\r\r' + str.substring(0); pieceTable.insert(1, '\r\n\r\n'); @@ -1003,7 +1096,9 @@ suite('CRLF', () => { }); test('random bug 2', () => { let str = ''; - const pieceTable = createTextBuffer([''], false); + const pieceTree = createTextBuffer([''], false); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(0, '\n\r\n\r'); str = str.substring(0, 0) + '\n\r\n\r' + str.substring(0); @@ -1018,7 +1113,9 @@ suite('CRLF', () => { }); test('random bug 3', () => { let str = ''; - const pieceTable = createTextBuffer([''], false); + const pieceTree = createTextBuffer([''], false); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(0, '\n\n\n\r'); str = str.substring(0, 0) + '\n\n\n\r' + str.substring(0); @@ -1039,7 +1136,9 @@ suite('CRLF', () => { }); test('random bug 4', () => { let str = ''; - const pieceTable = createTextBuffer([''], false); + const pieceTree = createTextBuffer([''], false); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(0, '\n\n\n\n'); str = str.substring(0, 0) + '\n\n\n\n' + str.substring(0); @@ -1057,7 +1156,9 @@ suite('CRLF', () => { }); test('random bug 5', () => { let str = ''; - const pieceTable = createTextBuffer([''], false); + const pieceTree = createTextBuffer([''], false); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(0, '\n\n\n\n'); str = str.substring(0, 0) + '\n\n\n\n' + str.substring(0); @@ -1083,7 +1184,9 @@ suite('CRLF', () => { }); test('random bug 6', () => { let str = ''; - const pieceTable = createTextBuffer([''], false); + const pieceTree = createTextBuffer([''], false); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(0, '\n\r\r\n'); str = str.substring(0, 0) + '\n\r\r\n' + str.substring(0); @@ -1107,7 +1210,9 @@ suite('CRLF', () => { }); test('random bug 8', () => { let str = ''; - const pieceTable = createTextBuffer([''], false); + const pieceTree = createTextBuffer([''], false); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(0, '\r\n\n\r'); str = str.substring(0, 0) + '\r\n\n\r' + str.substring(0); @@ -1123,7 +1228,9 @@ suite('CRLF', () => { }); test('random bug 7', () => { let str = ''; - const pieceTable = createTextBuffer([''], false); + const pieceTree = createTextBuffer([''], false); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(0, '\r\r\n\n'); str = str.substring(0, 0) + '\r\r\n\n' + str.substring(0); @@ -1139,7 +1246,9 @@ suite('CRLF', () => { test('random bug 10', () => { let str = ''; - const pieceTable = createTextBuffer([''], false); + const pieceTree = createTextBuffer([''], false); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(0, 'qneW'); str = str.substring(0, 0) + 'qneW' + str.substring(0); @@ -1160,7 +1269,9 @@ suite('CRLF', () => { test('random bug 9', () => { let str = ''; - const pieceTable = createTextBuffer([''], false); + const pieceTree = createTextBuffer([''], false); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(0, '\n\n\n\n'); str = str.substring(0, 0) + '\n\n\n\n' + str.substring(0); @@ -1181,14 +1292,20 @@ suite('CRLF', () => { }); suite('centralized lineStarts with CRLF', () => { + const ds = ensureNoDisposablesAreLeakedInTestSuite(); + test('delete CR in CRLF 1', () => { - const pieceTable = createTextBuffer(['a\r\nb'], false); + const pieceTree = createTextBuffer(['a\r\nb'], false); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.delete(2, 2); assert.strictEqual(pieceTable.getLineCount(), 2); assertTreeInvariants(pieceTable); }); test('delete CR in CRLF 2', () => { - const pieceTable = createTextBuffer(['a\r\nb']); + const pieceTree = createTextBuffer(['a\r\nb']); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.delete(0, 2); assert.strictEqual(pieceTable.getLineCount(), 2); @@ -1197,7 +1314,10 @@ suite('centralized lineStarts with CRLF', () => { test('random bug 1', () => { let str = '\n\n\r\r'; - const pieceTable = createTextBuffer(['\n\n\r\r'], false); + const pieceTree = createTextBuffer(['\n\n\r\r'], false); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); + pieceTable.insert(1, '\r\n\r\n'); str = str.substring(0, 1) + '\r\n\r\n' + str.substring(1); pieceTable.delete(5, 3); @@ -1211,7 +1331,9 @@ suite('centralized lineStarts with CRLF', () => { }); test('random bug 2', () => { let str = '\n\r\n\r'; - const pieceTable = createTextBuffer(['\n\r\n\r'], false); + const pieceTree = createTextBuffer(['\n\r\n\r'], false); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(2, '\n\r\r\r'); str = str.substring(0, 2) + '\n\r\r\r' + str.substring(2); @@ -1225,7 +1347,9 @@ suite('centralized lineStarts with CRLF', () => { test('random bug 3', () => { let str = '\n\n\n\r'; - const pieceTable = createTextBuffer(['\n\n\n\r'], false); + const pieceTree = createTextBuffer(['\n\n\n\r'], false); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.delete(2, 2); str = str.substring(0, 2) + str.substring(2 + 2); @@ -1245,7 +1369,9 @@ suite('centralized lineStarts with CRLF', () => { test('random bug 4', () => { let str = '\n\n\n\n'; - const pieceTable = createTextBuffer(['\n\n\n\n'], false); + const pieceTree = createTextBuffer(['\n\n\n\n'], false); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.delete(3, 1); str = str.substring(0, 3) + str.substring(3 + 1); @@ -1262,7 +1388,9 @@ suite('centralized lineStarts with CRLF', () => { test('random bug 5', () => { let str = '\n\n\n\n'; - const pieceTable = createTextBuffer(['\n\n\n\n'], false); + const pieceTree = createTextBuffer(['\n\n\n\n'], false); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.delete(3, 1); str = str.substring(0, 3) + str.substring(3 + 1); @@ -1287,7 +1415,9 @@ suite('centralized lineStarts with CRLF', () => { test('random bug 6', () => { let str = '\n\r\r\n'; - const pieceTable = createTextBuffer(['\n\r\r\n'], false); + const pieceTree = createTextBuffer(['\n\r\r\n'], false); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(4, '\r\n\n\r'); str = str.substring(0, 4) + '\r\n\n\r' + str.substring(4); @@ -1310,7 +1440,9 @@ suite('centralized lineStarts with CRLF', () => { test('random bug 7', () => { let str = '\r\n\n\r'; - const pieceTable = createTextBuffer(['\r\n\n\r'], false); + const pieceTree = createTextBuffer(['\r\n\n\r'], false); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.delete(1, 0); str = str.substring(0, 1) + str.substring(1 + 0); @@ -1325,7 +1457,9 @@ suite('centralized lineStarts with CRLF', () => { test('random bug 8', () => { let str = '\r\r\n\n'; - const pieceTable = createTextBuffer(['\r\r\n\n'], false); + const pieceTree = createTextBuffer(['\r\r\n\n'], false); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(4, '\r\n\n\r'); str = str.substring(0, 4) + '\r\n\n\r' + str.substring(4); @@ -1339,7 +1473,9 @@ suite('centralized lineStarts with CRLF', () => { test('random bug 9', () => { let str = 'qneW'; - const pieceTable = createTextBuffer(['qneW'], false); + const pieceTree = createTextBuffer(['qneW'], false); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(0, 'YhIl'); str = str.substring(0, 0) + 'YhIl' + str.substring(0); @@ -1358,7 +1494,9 @@ suite('centralized lineStarts with CRLF', () => { test('random bug 10', () => { let str = '\n\n\n\n'; - const pieceTable = createTextBuffer(['\n\n\n\n'], false); + const pieceTree = createTextBuffer(['\n\n\n\n'], false); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); pieceTable.insert(3, '\n\r\n\r'); str = str.substring(0, 3) + '\n\r\n\r' + str.substring(3); @@ -1376,7 +1514,10 @@ suite('centralized lineStarts with CRLF', () => { }); test('random chunk bug 1', () => { - const pieceTable = createTextBuffer(['\n\r\r\n\n\n\r\n\r'], false); + const pieceTree = createTextBuffer(['\n\r\r\n\n\n\r\n\r'], false); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); + let str = '\n\r\r\n\n\n\r\n\r'; pieceTable.delete(0, 2); str = str.substring(0, 0) + str.substring(0 + 2); @@ -1391,9 +1532,11 @@ suite('centralized lineStarts with CRLF', () => { }); test('random chunk bug 2', () => { - const pieceTable = createTextBuffer([ + const pieceTree = createTextBuffer([ '\n\r\n\n\n\r\n\r\n\r\r\n\n\n\r\r\n\r\n' ], false); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); let str = '\n\r\n\n\n\r\n\r\n\r\r\n\n\n\r\r\n\r\n'; pieceTable.insert(16, '\r\n\r\r'); str = str.substring(0, 16) + '\r\n\r\r' + str.substring(16); @@ -1412,7 +1555,9 @@ suite('centralized lineStarts with CRLF', () => { }); test('random chunk bug 3', () => { - const pieceTable = createTextBuffer(['\r\n\n\n\n\n\n\r\n'], false); + const pieceTree = createTextBuffer(['\r\n\n\n\n\n\n\r\n'], false); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); let str = '\r\n\n\n\n\n\n\r\n'; pieceTable.insert(4, '\n\n\r\n\r\r\n\n\r'); str = str.substring(0, 4) + '\n\n\r\n\r\r\n\n\r' + str.substring(4); @@ -1429,7 +1574,9 @@ suite('centralized lineStarts with CRLF', () => { }); test('random chunk bug 4', () => { - const pieceTable = createTextBuffer(['\n\r\n\r'], false); + const pieceTree = createTextBuffer(['\n\r\n\r'], false); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); let str = '\n\r\n\r'; pieceTable.insert(4, '\n\n\r\n'); str = str.substring(0, 4) + '\n\n\r\n' + str.substring(4); @@ -1443,8 +1590,12 @@ suite('centralized lineStarts with CRLF', () => { }); suite('random is unsupervised', () => { + const ds = ensureNoDisposablesAreLeakedInTestSuite(); + test('splitting large change buffer', function () { - const pieceTable = createTextBuffer([''], false); + const pieceTree = createTextBuffer([''], false); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); let str = ''; pieceTable.insert(0, 'WUZ\nXVZY\n'); @@ -1478,8 +1629,9 @@ suite('random is unsupervised', () => { test('random insert delete', function () { this.timeout(500000); let str = ''; - const pieceTable = createTextBuffer([str], false); - + const pieceTree = createTextBuffer([str], false); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); // let output = ''; for (let i = 0; i < 1000; i++) { if (Math.random() < 0.6) { @@ -1520,7 +1672,9 @@ suite('random is unsupervised', () => { chunks.push(randomStr(1000)); } - const pieceTable = createTextBuffer(chunks, false); + const pieceTree = createTextBuffer(chunks, false); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); let str = chunks.join(''); for (let i = 0; i < 1000; i++) { @@ -1553,7 +1707,9 @@ suite('random is unsupervised', () => { const chunks: string[] = []; chunks.push(randomStr(1000)); - const pieceTable = createTextBuffer(chunks, false); + const pieceTree = createTextBuffer(chunks, false); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); let str = chunks.join(''); for (let i = 0; i < 50; i++) { @@ -1584,39 +1740,53 @@ suite('random is unsupervised', () => { }); suite('buffer api', () => { + const ds = ensureNoDisposablesAreLeakedInTestSuite(); + test('equal', () => { const a = createTextBuffer(['abc']); const b = createTextBuffer(['ab', 'c']); const c = createTextBuffer(['abd']); const d = createTextBuffer(['abcd']); + ds.add(a); + ds.add(b); + ds.add(c); + ds.add(d); - assert(a.equal(b)); - assert(!a.equal(c)); - assert(!a.equal(d)); + assert(a.getPieceTree().equal(b.getPieceTree())); + assert(!a.getPieceTree().equal(c.getPieceTree())); + assert(!a.getPieceTree().equal(d.getPieceTree())); }); test('equal with more chunks', () => { const a = createTextBuffer(['ab', 'cd', 'e']); const b = createTextBuffer(['ab', 'c', 'de']); - assert(a.equal(b)); + ds.add(a); + ds.add(b); + assert(a.getPieceTree().equal(b.getPieceTree())); }); test('equal 2, empty buffer', () => { const a = createTextBuffer(['']); const b = createTextBuffer(['']); + ds.add(a); + ds.add(b); - assert(a.equal(b)); + assert(a.getPieceTree().equal(b.getPieceTree())); }); test('equal 3, empty buffer', () => { const a = createTextBuffer(['a']); const b = createTextBuffer(['']); + ds.add(a); + ds.add(b); - assert(!a.equal(b)); + assert(!a.getPieceTree().equal(b.getPieceTree())); }); test('getLineCharCode - issue #45735', () => { - const pieceTable = createTextBuffer(['LINE1\nline2']); + const pieceTree = createTextBuffer(['LINE1\nline2']); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); assert.strictEqual(pieceTable.getLineCharCode(1, 0), 'L'.charCodeAt(0), 'L'); assert.strictEqual(pieceTable.getLineCharCode(1, 1), 'I'.charCodeAt(0), 'I'); assert.strictEqual(pieceTable.getLineCharCode(1, 2), 'N'.charCodeAt(0), 'N'); @@ -1632,7 +1802,9 @@ suite('buffer api', () => { test('getLineCharCode - issue #47733', () => { - const pieceTable = createTextBuffer(['', 'LINE1\n', 'line2']); + const pieceTree = createTextBuffer(['', 'LINE1\n', 'line2']); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); assert.strictEqual(pieceTable.getLineCharCode(1, 0), 'L'.charCodeAt(0), 'L'); assert.strictEqual(pieceTable.getLineCharCode(1, 1), 'I'.charCodeAt(0), 'I'); assert.strictEqual(pieceTable.getLineCharCode(1, 2), 'N'.charCodeAt(0), 'N'); @@ -1648,8 +1820,12 @@ suite('buffer api', () => { }); suite('search offset cache', () => { + const ds = ensureNoDisposablesAreLeakedInTestSuite(); + test('render white space exception', () => { - const pieceTable = createTextBuffer(['class Name{\n\t\n\t\t\tget() {\n\n\t\t\t}\n\t\t}']); + const pieceTree = createTextBuffer(['class Name{\n\t\n\t\t\tget() {\n\n\t\t\t}\n\t\t}']); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); let str = 'class Name{\n\t\n\t\t\tget() {\n\n\t\t\t}\n\t\t}'; pieceTable.insert(12, 's'); @@ -1705,7 +1881,9 @@ suite('search offset cache', () => { }); test('Line breaks replacement is not necessary when EOL is normalized', () => { - const pieceTable = createTextBuffer(['abc']); + const pieceTree = createTextBuffer(['abc']); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); let str = 'abc'; pieceTable.insert(3, 'def\nabc'); @@ -1717,7 +1895,9 @@ suite('search offset cache', () => { }); test('Line breaks replacement is not necessary when EOL is normalized 2', () => { - const pieceTable = createTextBuffer(['abc\n']); + const pieceTree = createTextBuffer(['abc\n']); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); let str = 'abc\n'; pieceTable.insert(4, 'def\nabc'); @@ -1729,7 +1909,9 @@ suite('search offset cache', () => { }); test('Line breaks replacement is not necessary when EOL is normalized 3', () => { - const pieceTable = createTextBuffer(['abc\n']); + const pieceTree = createTextBuffer(['abc\n']); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); let str = 'abc\n'; pieceTable.insert(2, 'def\nabc'); @@ -1741,7 +1923,9 @@ suite('search offset cache', () => { }); test('Line breaks replacement is not necessary when EOL is normalized 4', () => { - const pieceTable = createTextBuffer(['abc\n']); + const pieceTree = createTextBuffer(['abc\n']); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); let str = 'abc\n'; pieceTable.insert(3, 'def\nabc'); @@ -1766,6 +1950,8 @@ function getValueInSnapshot(snapshot: ITextSnapshot) { return ret; } suite('snapshot', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + test('bug #45564, piece tree pieces should be immutable', () => { const model = createTextModel('\n'); model.applyEdits([ @@ -1863,9 +2049,13 @@ suite('snapshot', () => { }); suite('chunk based search', () => { + const ds = ensureNoDisposablesAreLeakedInTestSuite(); + test('#45892. For some cases, the buffer is empty but we still try to search', () => { const pieceTree = createTextBuffer(['']); - pieceTree.delete(0, 1); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); + pieceTable.delete(0, 1); const ret = pieceTree.findMatchesLineByLine(new Range(1, 1, 1, 1), new SearchData(/abc/, new WordCharacterClassifier(',./'), 'abc'), true, 1000); assert.strictEqual(ret.length, 0); }); @@ -1881,11 +2071,14 @@ suite('chunk based search', () => { '* [ ] task 3' ].join('\n') ]); - pieceTree.delete(0, 62); - pieceTree.delete(16, 1); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); - pieceTree.insert(16, ' '); - const ret = pieceTree.findMatchesLineByLine(new Range(1, 1, 4, 13), new SearchData(/\[/gi, new WordCharacterClassifier(',./'), '['), true, 1000); + pieceTable.delete(0, 62); + pieceTable.delete(16, 1); + + pieceTable.insert(16, ' '); + const ret = pieceTable.findMatchesLineByLine(new Range(1, 1, 4, 13), new SearchData(/\[/gi, new WordCharacterClassifier(',./'), '['), true, 1000); assert.strictEqual(ret.length, 3); assert.deepStrictEqual(ret[0].range, new Range(2, 3, 2, 4)); @@ -1900,13 +2093,16 @@ suite('chunk based search', () => { 'dbcabc' ].join('\n') ]); - pieceTree.delete(4, 1); - let ret = pieceTree.findMatchesLineByLine(new Range(2, 3, 2, 6), new SearchData(/a/gi, null, 'a'), true, 1000); + ds.add(pieceTree); + const pieceTable = pieceTree.getPieceTree(); + + pieceTable.delete(4, 1); + let ret = pieceTable.findMatchesLineByLine(new Range(2, 3, 2, 6), new SearchData(/a/gi, null, 'a'), true, 1000); assert.strictEqual(ret.length, 1); assert.deepStrictEqual(ret[0].range, new Range(2, 3, 2, 4)); - pieceTree.delete(4, 1); - ret = pieceTree.findMatchesLineByLine(new Range(2, 2, 2, 5), new SearchData(/a/gi, null, 'a'), true, 1000); + pieceTable.delete(4, 1); + ret = pieceTable.findMatchesLineByLine(new Range(2, 2, 2, 5), new SearchData(/a/gi, null, 'a'), true, 1000); assert.strictEqual(ret.length, 1); assert.deepStrictEqual(ret[0].range, new Range(2, 2, 2, 3)); }); diff --git a/src/vs/editor/test/common/services/editorSimpleWorker.test.ts b/src/vs/editor/test/common/services/editorSimpleWorker.test.ts index d650c2d5357..af7e41cd60b 100644 --- a/src/vs/editor/test/common/services/editorSimpleWorker.test.ts +++ b/src/vs/editor/test/common/services/editorSimpleWorker.test.ts @@ -257,6 +257,15 @@ suite('EditorSimpleWorker', () => { ); }); + test.skip('[Bug] Getting Message "Overlapping ranges are not allowed" and nothing happens with Inline-Chat ', async function () { + await testEdits(("const API = require('../src/api');\n\ndescribe('API', () => {\n let api;\n let database;\n\n beforeAll(() => {\n database = {\n getAllBooks: jest.fn(),\n getBooksByAuthor: jest.fn(),\n getBooksByTitle: jest.fn(),\n };\n api = new API(database);\n });\n\n describe('GET /books', () => {\n it('should return all books', async () => {\n const mockBooks = [{ title: 'Book 1' }, { title: 'Book 2' }];\n database.getAllBooks.mockResolvedValue(mockBooks);\n\n const req = {};\n const res = {\n json: jest.fn(),\n };\n\n await api.register({\n get: (path, handler) => {\n if (path === '/books') {\n handler(req, res);\n }\n },\n });\n\n expect(database.getAllBooks).toHaveBeenCalled();\n expect(res.json).toHaveBeenCalledWith(mockBooks);\n });\n });\n\n describe('GET /books/author/:author', () => {\n it('should return books by author', async () => {\n const mockAuthor = 'John Doe';\n const mockBooks = [{ title: 'Book 1', author: mockAuthor }, { title: 'Book 2', author: mockAuthor }];\n database.getBooksByAuthor.mockResolvedValue(mockBooks);\n\n const req = {\n params: {\n author: mockAuthor,\n },\n };\n const res = {\n json: jest.fn(),\n };\n\n await api.register({\n get: (path, handler) => {\n if (path === `/books/author/${mockAuthor}`) {\n handler(req, res);\n }\n },\n });\n\n expect(database.getBooksByAuthor).toHaveBeenCalledWith(mockAuthor);\n expect(res.json).toHaveBeenCalledWith(mockBooks);\n });\n });\n\n describe('GET /books/title/:title', () => {\n it('should return books by title', async () => {\n const mockTitle = 'Book 1';\n const mockBooks = [{ title: mockTitle, author: 'John Doe' }];\n database.getBooksByTitle.mockResolvedValue(mockBooks);\n\n const req = {\n params: {\n title: mockTitle,\n },\n };\n const res = {\n json: jest.fn(),\n };\n\n await api.register({\n get: (path, handler) => {\n if (path === `/books/title/${mockTitle}`) {\n handler(req, res);\n }\n },\n });\n\n expect(database.getBooksByTitle).toHaveBeenCalledWith(mockTitle);\n expect(res.json).toHaveBeenCalledWith(mockBooks);\n });\n });\n});\n").split('\n'), + [{ + range: { startLineNumber: 1, startColumn: 1, endLineNumber: 96, endColumn: 1 }, + text: `const request = require('supertest');\nconst API = require('../src/api');\n\ndescribe('API', () => {\n let api;\n let database;\n\n beforeAll(() => {\n database = {\n getAllBooks: jest.fn(),\n getBooksByAuthor: jest.fn(),\n getBooksByTitle: jest.fn(),\n };\n api = new API(database);\n });\n\n describe('GET /books', () => {\n it('should return all books', async () => {\n const mockBooks = [{ title: 'Book 1' }, { title: 'Book 2' }];\n database.getAllBooks.mockResolvedValue(mockBooks);\n\n const response = await request(api.app).get('/books');\n\n expect(database.getAllBooks).toHaveBeenCalled();\n expect(response.status).toBe(200);\n expect(response.body).toEqual(mockBooks);\n });\n });\n\n describe('GET /books/author/:author', () => {\n it('should return books by author', async () => {\n const mockAuthor = 'John Doe';\n const mockBooks = [{ title: 'Book 1', author: mockAuthor }, { title: 'Book 2', author: mockAuthor }];\n database.getBooksByAuthor.mockResolvedValue(mockBooks);\n\n const response = await request(api.app).get(\`/books/author/\${mockAuthor}\`);\n\n expect(database.getBooksByAuthor).toHaveBeenCalledWith(mockAuthor);\n expect(response.status).toBe(200);\n expect(response.body).toEqual(mockBooks);\n });\n });\n\n describe('GET /books/title/:title', () => {\n it('should return books by title', async () => {\n const mockTitle = 'Book 1';\n const mockBooks = [{ title: mockTitle, author: 'John Doe' }];\n database.getBooksByTitle.mockResolvedValue(mockBooks);\n\n const response = await request(api.app).get(\`/books/title/\${mockTitle}\`);\n\n expect(database.getBooksByTitle).toHaveBeenCalledWith(mockTitle);\n expect(response.status).toBe(200);\n expect(response.body).toEqual(mockBooks);\n });\n });\n});\n`, + }] + ); + }); + test('ICommonModel#getValueInRange, issue #17424', function () { const model = worker.addModel([ diff --git a/src/vs/editor/test/common/services/textResourceConfigurationService.test.ts b/src/vs/editor/test/common/services/textResourceConfigurationService.test.ts index faaf6b520ec..561d6cb23fe 100644 --- a/src/vs/editor/test/common/services/textResourceConfigurationService.test.ts +++ b/src/vs/editor/test/common/services/textResourceConfigurationService.test.ts @@ -11,10 +11,13 @@ import { ILanguageService } from 'vs/editor/common/languages/language'; import { IConfigurationValue, IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { TextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; import { URI } from 'vs/base/common/uri'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('TextResourceConfigurationService - Update', () => { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + let instantiationService: TestInstantiationService; let configurationValue: IConfigurationValue = {}; let updateArgs: any[]; @@ -31,15 +34,11 @@ suite('TextResourceConfigurationService - Update', () => { let testObject: TextResourceConfigurationService; setup(() => { - instantiationService = new TestInstantiationService(); + instantiationService = disposables.add(new TestInstantiationService()); instantiationService.stub(IModelService, >{ getModel() { return null; } }); instantiationService.stub(ILanguageService, >{ guessLanguageIdByFilepathOrFirstLine() { return language; } }); instantiationService.stub(IConfigurationService, configurationService); - testObject = instantiationService.createInstance(TextResourceConfigurationService); - }); - - teardown(() => { - instantiationService.dispose(); + testObject = disposables.add(instantiationService.createInstance(TextResourceConfigurationService)); }); test('updateValue writes without target and overrides when no language is defined', async () => { diff --git a/src/vs/editor/test/node/diffing/advancedLinesDiffComputer.test.ts b/src/vs/editor/test/node/diffing/defaultLinesDiffComputer.test.ts similarity index 81% rename from src/vs/editor/test/node/diffing/advancedLinesDiffComputer.test.ts rename to src/vs/editor/test/node/diffing/defaultLinesDiffComputer.test.ts index 31fa3e3086e..664ef33cf4f 100644 --- a/src/vs/editor/test/node/diffing/advancedLinesDiffComputer.test.ts +++ b/src/vs/editor/test/node/diffing/defaultLinesDiffComputer.test.ts @@ -6,12 +6,24 @@ import * as assert from 'assert'; import { Range } from 'vs/editor/common/core/range'; import { RangeMapping } from 'vs/editor/common/diff/rangeMapping'; -import { getLineRangeMapping } from 'vs/editor/common/diff/defaultLinesDiffComputer/defaultLinesDiffComputer'; import { OffsetRange } from 'vs/editor/common/core/offsetRange'; +import { getLineRangeMapping } from 'vs/editor/common/diff/defaultLinesDiffComputer/defaultLinesDiffComputer'; import { LinesSliceCharSequence } from 'vs/editor/common/diff/defaultLinesDiffComputer/linesSliceCharSequence'; +import { MyersDiffAlgorithm } from 'vs/editor/common/diff/defaultLinesDiffComputer/algorithms/myersDiffAlgorithm'; +import { DynamicProgrammingDiffing } from 'vs/editor/common/diff/defaultLinesDiffComputer/algorithms/dynamicProgrammingDiffing'; + +suite('myers', () => { + test('1', () => { + const s1 = new LinesSliceCharSequence(['hello world'], new OffsetRange(0, 1), true); + const s2 = new LinesSliceCharSequence(['hallo welt'], new OffsetRange(0, 1), true); + + const a = true ? new MyersDiffAlgorithm() : new DynamicProgrammingDiffing(); + a.compute(s1, s2); + }); +}); suite('lineRangeMapping', () => { - test('1', () => { + test('Simple', () => { assert.deepStrictEqual( getLineRangeMapping( new RangeMapping( @@ -32,7 +44,7 @@ suite('lineRangeMapping', () => { ); }); - test('2', () => { + test('Empty Lines', () => { assert.deepStrictEqual( getLineRangeMapping( new RangeMapping( @@ -56,8 +68,6 @@ suite('lineRangeMapping', () => { }); suite('LinesSliceCharSequence', () => { - // Create tests for translateOffset - const sequence = new LinesSliceCharSequence( [ 'line1: foo', diff --git a/src/vs/editor/test/node/diffing/diffingFixture.test.ts b/src/vs/editor/test/node/diffing/fixtures.test.ts similarity index 99% rename from src/vs/editor/test/node/diffing/diffingFixture.test.ts rename to src/vs/editor/test/node/diffing/fixtures.test.ts index bb42c69d62c..a7ff5dbe5a1 100644 --- a/src/vs/editor/test/node/diffing/diffingFixture.test.ts +++ b/src/vs/editor/test/node/diffing/fixtures.test.ts @@ -12,7 +12,7 @@ import { DetailedLineRangeMapping } from 'vs/editor/common/diff/rangeMapping'; import { LegacyLinesDiffComputer } from 'vs/editor/common/diff/legacyLinesDiffComputer'; import { DefaultLinesDiffComputer } from 'vs/editor/common/diff/defaultLinesDiffComputer/defaultLinesDiffComputer'; -suite('diff fixtures', () => { +suite('diffing fixtures', () => { setup(() => { setUnexpectedErrorHandler(e => { throw e; diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 279a605ba52..b3b99ebc23c 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -3428,6 +3428,11 @@ declare namespace monaco.editor { * Defaults to language defined behavior. */ autoClosingBrackets?: EditorAutoClosingStrategy; + /** + * Options for auto closing comments. + * Defaults to language defined behavior. + */ + autoClosingComments?: EditorAutoClosingStrategy; /** * Options for auto closing quotes. * Defaults to language defined behavior. @@ -3975,6 +3980,11 @@ declare namespace monaco.editor { * Defaults to true. */ sticky?: boolean; + /** + * Controls how long the hover is visible after you hovered out of it. + * Require sticky setting to be true. + */ + hidingDelay?: number; /** * Should the hover be shown above the line if possible? * Defaults to false. @@ -4697,146 +4707,147 @@ declare namespace monaco.editor { ariaLabel = 4, ariaRequired = 5, autoClosingBrackets = 6, - screenReaderAnnounceInlineSuggestion = 7, - autoClosingDelete = 8, - autoClosingOvertype = 9, - autoClosingQuotes = 10, - autoIndent = 11, - automaticLayout = 12, - autoSurround = 13, - bracketPairColorization = 14, - guides = 15, - codeLens = 16, - codeLensFontFamily = 17, - codeLensFontSize = 18, - colorDecorators = 19, - colorDecoratorsLimit = 20, - columnSelection = 21, - comments = 22, - contextmenu = 23, - copyWithSyntaxHighlighting = 24, - cursorBlinking = 25, - cursorSmoothCaretAnimation = 26, - cursorStyle = 27, - cursorSurroundingLines = 28, - cursorSurroundingLinesStyle = 29, - cursorWidth = 30, - disableLayerHinting = 31, - disableMonospaceOptimizations = 32, - domReadOnly = 33, - dragAndDrop = 34, - dropIntoEditor = 35, - emptySelectionClipboard = 36, - experimentalWhitespaceRendering = 37, - extraEditorClassName = 38, - fastScrollSensitivity = 39, - find = 40, - fixedOverflowWidgets = 41, - folding = 42, - foldingStrategy = 43, - foldingHighlight = 44, - foldingImportsByDefault = 45, - foldingMaximumRegions = 46, - unfoldOnClickAfterEndOfLine = 47, - fontFamily = 48, - fontInfo = 49, - fontLigatures = 50, - fontSize = 51, - fontWeight = 52, - fontVariations = 53, - formatOnPaste = 54, - formatOnType = 55, - glyphMargin = 56, - gotoLocation = 57, - hideCursorInOverviewRuler = 58, - hover = 59, - inDiffEditor = 60, - inlineSuggest = 61, - letterSpacing = 62, - lightbulb = 63, - lineDecorationsWidth = 64, - lineHeight = 65, - lineNumbers = 66, - lineNumbersMinChars = 67, - linkedEditing = 68, - links = 69, - matchBrackets = 70, - minimap = 71, - mouseStyle = 72, - mouseWheelScrollSensitivity = 73, - mouseWheelZoom = 74, - multiCursorMergeOverlapping = 75, - multiCursorModifier = 76, - multiCursorPaste = 77, - multiCursorLimit = 78, - occurrencesHighlight = 79, - overviewRulerBorder = 80, - overviewRulerLanes = 81, - padding = 82, - pasteAs = 83, - parameterHints = 84, - peekWidgetDefaultFocus = 85, - definitionLinkOpensInPeek = 86, - quickSuggestions = 87, - quickSuggestionsDelay = 88, - readOnly = 89, - readOnlyMessage = 90, - renameOnType = 91, - renderControlCharacters = 92, - renderFinalNewline = 93, - renderLineHighlight = 94, - renderLineHighlightOnlyWhenFocus = 95, - renderValidationDecorations = 96, - renderWhitespace = 97, - revealHorizontalRightPadding = 98, - roundedSelection = 99, - rulers = 100, - scrollbar = 101, - scrollBeyondLastColumn = 102, - scrollBeyondLastLine = 103, - scrollPredominantAxis = 104, - selectionClipboard = 105, - selectionHighlight = 106, - selectOnLineNumbers = 107, - showFoldingControls = 108, - showUnused = 109, - snippetSuggestions = 110, - smartSelect = 111, - smoothScrolling = 112, - stickyScroll = 113, - stickyTabStops = 114, - stopRenderingLineAfter = 115, - suggest = 116, - suggestFontSize = 117, - suggestLineHeight = 118, - suggestOnTriggerCharacters = 119, - suggestSelection = 120, - tabCompletion = 121, - tabIndex = 122, - unicodeHighlighting = 123, - unusualLineTerminators = 124, - useShadowDOM = 125, - useTabStops = 126, - wordBreak = 127, - wordSeparators = 128, - wordWrap = 129, - wordWrapBreakAfterCharacters = 130, - wordWrapBreakBeforeCharacters = 131, - wordWrapColumn = 132, - wordWrapOverride1 = 133, - wordWrapOverride2 = 134, - wrappingIndent = 135, - wrappingStrategy = 136, - showDeprecated = 137, - inlayHints = 138, - editorClassName = 139, - pixelRatio = 140, - tabFocusMode = 141, - layoutInfo = 142, - wrappingInfo = 143, - defaultColorDecorators = 144, - colorDecoratorsActivatedOn = 145, - inlineCompletionsAccessibilityVerbose = 146 + autoClosingComments = 7, + screenReaderAnnounceInlineSuggestion = 8, + autoClosingDelete = 9, + autoClosingOvertype = 10, + autoClosingQuotes = 11, + autoIndent = 12, + automaticLayout = 13, + autoSurround = 14, + bracketPairColorization = 15, + guides = 16, + codeLens = 17, + codeLensFontFamily = 18, + codeLensFontSize = 19, + colorDecorators = 20, + colorDecoratorsLimit = 21, + columnSelection = 22, + comments = 23, + contextmenu = 24, + copyWithSyntaxHighlighting = 25, + cursorBlinking = 26, + cursorSmoothCaretAnimation = 27, + cursorStyle = 28, + cursorSurroundingLines = 29, + cursorSurroundingLinesStyle = 30, + cursorWidth = 31, + disableLayerHinting = 32, + disableMonospaceOptimizations = 33, + domReadOnly = 34, + dragAndDrop = 35, + dropIntoEditor = 36, + emptySelectionClipboard = 37, + experimentalWhitespaceRendering = 38, + extraEditorClassName = 39, + fastScrollSensitivity = 40, + find = 41, + fixedOverflowWidgets = 42, + folding = 43, + foldingStrategy = 44, + foldingHighlight = 45, + foldingImportsByDefault = 46, + foldingMaximumRegions = 47, + unfoldOnClickAfterEndOfLine = 48, + fontFamily = 49, + fontInfo = 50, + fontLigatures = 51, + fontSize = 52, + fontWeight = 53, + fontVariations = 54, + formatOnPaste = 55, + formatOnType = 56, + glyphMargin = 57, + gotoLocation = 58, + hideCursorInOverviewRuler = 59, + hover = 60, + inDiffEditor = 61, + inlineSuggest = 62, + letterSpacing = 63, + lightbulb = 64, + lineDecorationsWidth = 65, + lineHeight = 66, + lineNumbers = 67, + lineNumbersMinChars = 68, + linkedEditing = 69, + links = 70, + matchBrackets = 71, + minimap = 72, + mouseStyle = 73, + mouseWheelScrollSensitivity = 74, + mouseWheelZoom = 75, + multiCursorMergeOverlapping = 76, + multiCursorModifier = 77, + multiCursorPaste = 78, + multiCursorLimit = 79, + occurrencesHighlight = 80, + overviewRulerBorder = 81, + overviewRulerLanes = 82, + padding = 83, + pasteAs = 84, + parameterHints = 85, + peekWidgetDefaultFocus = 86, + definitionLinkOpensInPeek = 87, + quickSuggestions = 88, + quickSuggestionsDelay = 89, + readOnly = 90, + readOnlyMessage = 91, + renameOnType = 92, + renderControlCharacters = 93, + renderFinalNewline = 94, + renderLineHighlight = 95, + renderLineHighlightOnlyWhenFocus = 96, + renderValidationDecorations = 97, + renderWhitespace = 98, + revealHorizontalRightPadding = 99, + roundedSelection = 100, + rulers = 101, + scrollbar = 102, + scrollBeyondLastColumn = 103, + scrollBeyondLastLine = 104, + scrollPredominantAxis = 105, + selectionClipboard = 106, + selectionHighlight = 107, + selectOnLineNumbers = 108, + showFoldingControls = 109, + showUnused = 110, + snippetSuggestions = 111, + smartSelect = 112, + smoothScrolling = 113, + stickyScroll = 114, + stickyTabStops = 115, + stopRenderingLineAfter = 116, + suggest = 117, + suggestFontSize = 118, + suggestLineHeight = 119, + suggestOnTriggerCharacters = 120, + suggestSelection = 121, + tabCompletion = 122, + tabIndex = 123, + unicodeHighlighting = 124, + unusualLineTerminators = 125, + useShadowDOM = 126, + useTabStops = 127, + wordBreak = 128, + wordSeparators = 129, + wordWrap = 130, + wordWrapBreakAfterCharacters = 131, + wordWrapBreakBeforeCharacters = 132, + wordWrapColumn = 133, + wordWrapOverride1 = 134, + wordWrapOverride2 = 135, + wrappingIndent = 136, + wrappingStrategy = 137, + showDeprecated = 138, + inlayHints = 139, + editorClassName = 140, + pixelRatio = 141, + tabFocusMode = 142, + layoutInfo = 143, + wrappingInfo = 144, + defaultColorDecorators = 145, + colorDecoratorsActivatedOn = 146, + inlineCompletionsAccessibilityVerbose = 147 } export const EditorOptions: { @@ -4848,6 +4859,7 @@ declare namespace monaco.editor { ariaRequired: IEditorOption; screenReaderAnnounceInlineSuggestion: IEditorOption; autoClosingBrackets: IEditorOption; + autoClosingComments: IEditorOption; autoClosingDelete: IEditorOption; autoClosingOvertype: IEditorOption; autoClosingQuotes: IEditorOption; @@ -7336,10 +7348,6 @@ declare namespace monaco.languages { * Prefer spaces over tabs. */ insertSpaces: boolean; - /** - * The list of multiple ranges to format at once, if the provider supports it. - */ - ranges?: Range[]; } /** diff --git a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts index c6e7a926213..077393ebb51 100644 --- a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts +++ b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts @@ -39,7 +39,13 @@ export function createAndFillInContextMenuActions(menu: IMenu, options: IMenuAct fillInActions(groups, target, useAlternativeActions, primaryGroup ? actionGroup => actionGroup === primaryGroup : actionGroup => actionGroup === 'navigation'); } -export function createAndFillInActionBarActions(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[] }, primaryGroup?: string | ((actionGroup: string) => boolean), shouldInlineSubmenu?: (action: SubmenuAction, group: string, groupSize: number) => boolean, useSeparatorsInPrimaryActions?: boolean): void { +export function createAndFillInActionBarActions( + menu: IMenu, + options: IMenuActionOptions | undefined, + target: IAction[] | { primary: IAction[]; secondary: IAction[] }, + primaryGroup?: string | ((actionGroup: string) => boolean), + shouldInlineSubmenu?: (action: SubmenuAction, group: string, groupSize: number) => boolean, + useSeparatorsInPrimaryActions?: boolean): void { const groups = menu.getActions(options); const isPrimaryAction = typeof primaryGroup === 'string' ? (actionGroup: string) => actionGroup === primaryGroup : primaryGroup; @@ -102,7 +108,7 @@ function fillInActions( // inlining submenus with length 0 or 1 is easy, // larger submenus need to be checked with the overall limit const submenuActions = action.actions; - if (submenuActions.length <= 1 && shouldInlineSubmenu(action, group, target.length)) { + if (shouldInlineSubmenu(action, group, target.length)) { target.splice(index, 1, ...submenuActions); } } diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 0b6f1148464..62feb6c2496 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -74,6 +74,7 @@ export class MenuId { static readonly ExtensionContext = new MenuId('ExtensionContext'); static readonly GlobalActivity = new MenuId('GlobalActivity'); static readonly CommandCenter = new MenuId('CommandCenter'); + static readonly CommandCenterCenter = new MenuId('CommandCenterCenter'); static readonly LayoutControlMenuSubmenu = new MenuId('LayoutControlMenuSubmenu'); static readonly LayoutControlMenu = new MenuId('LayoutControlMenu'); static readonly MenubarMainMenu = new MenuId('MenubarMainMenu'); diff --git a/src/vs/platform/backup/test/electron-main/backupMainService.test.ts b/src/vs/platform/backup/test/electron-main/backupMainService.test.ts index 928d90f7a40..8f3048c267a 100644 --- a/src/vs/platform/backup/test/electron-main/backupMainService.test.ts +++ b/src/vs/platform/backup/test/electron-main/backupMainService.test.ts @@ -26,6 +26,7 @@ import { IFolderBackupInfo, isFolderBackupInfo, IWorkspaceBackupInfo } from 'vs/ import { IWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; import { InMemoryTestStateMainService } from 'vs/platform/test/electron-main/workbenchTestServices'; import { LogService } from 'vs/platform/log/common/logService'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; flakySuite('BackupMainService', () => { @@ -613,4 +614,6 @@ flakySuite('BackupMainService', () => { assert.strictEqual(found, 2); }); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/platform/commands/test/common/commands.test.ts b/src/vs/platform/commands/test/common/commands.test.ts index bc7e0c7b276..7dcb65de2d4 100644 --- a/src/vs/platform/commands/test/common/commands.test.ts +++ b/src/vs/platform/commands/test/common/commands.test.ts @@ -3,10 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { combinedDisposable } from 'vs/base/common/lifecycle'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; suite('Command Tests', function () { + ensureNoDisposablesAreLeakedInTestSuite(); + test('register command - no handler', function () { assert.throws(() => CommandsRegistry.registerCommand('foo', null!)); }); @@ -49,15 +53,15 @@ suite('Command Tests', function () { test('command with description', function () { - CommandsRegistry.registerCommand('test', function (accessor, args) { + const r1 = CommandsRegistry.registerCommand('test', function (accessor, args) { assert.ok(typeof args === 'string'); }); - CommandsRegistry.registerCommand('test2', function (accessor, args) { + const r2 = CommandsRegistry.registerCommand('test2', function (accessor, args) { assert.ok(typeof args === 'string'); }); - CommandsRegistry.registerCommand({ + const r3 = CommandsRegistry.registerCommand({ id: 'test3', handler: function (accessor, args) { return true; @@ -73,5 +77,6 @@ suite('Command Tests', function () { assert.throws(() => CommandsRegistry.getCommands().get('test3')!.handler.apply(undefined, [undefined!, 'string'])); assert.strictEqual(CommandsRegistry.getCommands().get('test3')!.handler.apply(undefined, [undefined!, 1]), true); + combinedDisposable(r1, r2, r3).dispose(); }); }); diff --git a/src/vs/platform/configuration/test/common/configurationService.test.ts b/src/vs/platform/configuration/test/common/configurationService.test.ts index 5337e51d667..afa34cc695d 100644 --- a/src/vs/platform/configuration/test/common/configurationService.test.ts +++ b/src/vs/platform/configuration/test/common/configurationService.test.ts @@ -6,10 +6,10 @@ import * as assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { Event } from 'vs/base/common/event'; -import { DisposableStore } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ConfigurationTarget, isConfigured } from 'vs/platform/configuration/common/configuration'; import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { ConfigurationService } from 'vs/platform/configuration/common/configurationService'; @@ -20,21 +20,20 @@ import { NullLogService } from 'vs/platform/log/common/log'; import { NullPolicyService } from 'vs/platform/policy/common/policy'; import { Registry } from 'vs/platform/registry/common/platform'; -suite('ConfigurationService', () => { +suite('ConfigurationService.test.ts', () => { + + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); let fileService: IFileService; let settingsResource: URI; - const disposables: DisposableStore = new DisposableStore(); setup(async () => { fileService = disposables.add(new FileService(new NullLogService())); const diskFileSystemProvider = disposables.add(new InMemoryFileSystemProvider()); - fileService.registerProvider(Schemas.file, diskFileSystemProvider); + disposables.add(fileService.registerProvider(Schemas.file, diskFileSystemProvider)); settingsResource = URI.file('settings.json'); }); - teardown(() => disposables.clear()); - test('simple', () => runWithFakedTimers({ useFakeTimers: true }, async () => { await fileService.writeFile(settingsResource, VSBuffer.fromString('{ "foo": "bar" }')); const testObject = disposables.add(new ConfigurationService(settingsResource, fileService, new NullPolicyService(), new NullLogService())); diff --git a/src/vs/platform/configuration/test/common/configurations.test.ts b/src/vs/platform/configuration/test/common/configurations.test.ts index 30cddfd2144..63269f63ffa 100644 --- a/src/vs/platform/configuration/test/common/configurations.test.ts +++ b/src/vs/platform/configuration/test/common/configurations.test.ts @@ -6,12 +6,14 @@ import * as assert from 'assert'; import { Event } from 'vs/base/common/event'; import { equals } from 'vs/base/common/objects'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Extensions, IConfigurationNode, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { DefaultConfiguration } from 'vs/platform/configuration/common/configurations'; import { Registry } from 'vs/platform/registry/common/platform'; suite('DefaultConfiguration', () => { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); const configurationRegistry = Registry.as(Extensions.Configuration); setup(() => reset()); @@ -24,7 +26,7 @@ suite('DefaultConfiguration', () => { } test('Test registering a property before initialize', async () => { - const testObject = new DefaultConfiguration(); + const testObject = disposables.add(new DefaultConfiguration()); configurationRegistry.registerConfiguration({ 'id': 'a', 'order': 1, @@ -43,7 +45,7 @@ suite('DefaultConfiguration', () => { }); test('Test registering a property and do not initialize', async () => { - const testObject = new DefaultConfiguration(); + const testObject = disposables.add(new DefaultConfiguration()); configurationRegistry.registerConfiguration({ 'id': 'a', 'order': 1, @@ -61,7 +63,7 @@ suite('DefaultConfiguration', () => { }); test('Test registering a property after initialize', async () => { - const testObject = new DefaultConfiguration(); + const testObject = disposables.add(new DefaultConfiguration()); await testObject.initialize(); const promise = Event.toPromise(testObject.onDidChangeConfiguration); configurationRegistry.registerConfiguration({ @@ -83,7 +85,7 @@ suite('DefaultConfiguration', () => { }); test('Test registering nested properties', async () => { - const testObject = new DefaultConfiguration(); + const testObject = disposables.add(new DefaultConfiguration()); configurationRegistry.registerConfiguration({ 'id': 'a', 'order': 1, @@ -111,7 +113,7 @@ suite('DefaultConfiguration', () => { }); test('Test registering the same property again', async () => { - const testObject = new DefaultConfiguration(); + const testObject = disposables.add(new DefaultConfiguration()); configurationRegistry.registerConfiguration({ 'id': 'a', 'order': 1, @@ -143,7 +145,7 @@ suite('DefaultConfiguration', () => { }); test('Test registering an override identifier', async () => { - const testObject = new DefaultConfiguration(); + const testObject = disposables.add(new DefaultConfiguration()); configurationRegistry.registerDefaultConfigurations([{ overrides: { '[a]': { @@ -160,7 +162,7 @@ suite('DefaultConfiguration', () => { }); test('Test registering a normal property and override identifier', async () => { - const testObject = new DefaultConfiguration(); + const testObject = disposables.add(new DefaultConfiguration()); configurationRegistry.registerConfiguration({ 'id': 'a', 'order': 1, @@ -193,7 +195,7 @@ suite('DefaultConfiguration', () => { }); test('Test normal property is registered after override identifier', async () => { - const testObject = new DefaultConfiguration(); + const testObject = disposables.add(new DefaultConfiguration()); const promise = Event.toPromise(testObject.onDidChangeConfiguration); configurationRegistry.registerDefaultConfigurations([{ overrides: { @@ -230,7 +232,7 @@ suite('DefaultConfiguration', () => { }); test('Test override identifier is registered after property', async () => { - const testObject = new DefaultConfiguration(); + const testObject = disposables.add(new DefaultConfiguration()); const promise = Event.toPromise(testObject.onDidChangeConfiguration); configurationRegistry.registerConfiguration({ 'id': 'a', @@ -266,7 +268,7 @@ suite('DefaultConfiguration', () => { }); test('Test register override identifier and property after initialize', async () => { - const testObject = new DefaultConfiguration(); + const testObject = disposables.add(new DefaultConfiguration()); await testObject.initialize(); @@ -301,7 +303,7 @@ suite('DefaultConfiguration', () => { }); test('Test deregistering a property', async () => { - const testObject = new DefaultConfiguration(); + const testObject = disposables.add(new DefaultConfiguration()); const promise = Event.toPromise(testObject.onDidChangeConfiguration); const node: IConfigurationNode = { 'id': 'a', @@ -328,7 +330,7 @@ suite('DefaultConfiguration', () => { }); test('Test deregistering an override identifier', async () => { - const testObject = new DefaultConfiguration(); + const testObject = disposables.add(new DefaultConfiguration()); configurationRegistry.registerConfiguration({ 'id': 'a', 'order': 1, diff --git a/src/vs/platform/configuration/test/common/policyConfiguration.test.ts b/src/vs/platform/configuration/test/common/policyConfiguration.test.ts index d5b31e5516d..e7c228e98fc 100644 --- a/src/vs/platform/configuration/test/common/policyConfiguration.test.ts +++ b/src/vs/platform/configuration/test/common/policyConfiguration.test.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { DisposableStore } from 'vs/base/common/lifecycle'; import { Event } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { DefaultConfiguration, PolicyConfiguration } from 'vs/platform/configuration/common/configurations'; @@ -19,14 +18,16 @@ import { deepClone } from 'vs/base/common/objects'; import { IPolicyService } from 'vs/platform/policy/common/policy'; import { FilePolicyService } from 'vs/platform/policy/common/filePolicyService'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('PolicyConfiguration', () => { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + let testObject: PolicyConfiguration; let fileService: IFileService; let policyService: IPolicyService; const policyFile = URI.file('policyFile').with({ scheme: 'vscode-tests' }); - const disposables = new DisposableStore(); const policyConfigurationNode: IConfigurationNode = { 'id': 'policyConfiguration', 'order': 1, @@ -64,13 +65,11 @@ suite('PolicyConfiguration', () => { await defaultConfiguration.initialize(); fileService = disposables.add(new FileService(new NullLogService())); const diskFileSystemProvider = disposables.add(new InMemoryFileSystemProvider()); - fileService.registerProvider(policyFile.scheme, diskFileSystemProvider); - policyService = new FilePolicyService(policyFile, fileService, new NullLogService()); + disposables.add(fileService.registerProvider(policyFile.scheme, diskFileSystemProvider)); + policyService = disposables.add(new FilePolicyService(policyFile, fileService, new NullLogService())); testObject = disposables.add(new PolicyConfiguration(defaultConfiguration, policyService, new NullLogService())); }); - teardown(() => disposables.clear()); - test('initialize: with policies', async () => { await fileService.writeFile(policyFile, VSBuffer.fromString(JSON.stringify({ 'PolicySettingA': 'policyValueA' }))); diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts index b63a262137f..f205cc30439 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -91,7 +91,7 @@ export interface NativeParsedArgs { 'export-default-configuration'?: string; 'install-source'?: string; 'disable-updates'?: boolean; - 'disable-keytar'?: boolean; + 'use-inmemory-secretstorage'?: boolean; 'password-store'?: string; 'disable-workspace-trust'?: boolean; 'disable-crash-reporter'?: boolean; diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index 96ccb252006..6c2e2b551ee 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -141,8 +141,8 @@ export interface INativeEnvironmentService extends IEnvironmentService { extensionsDownloadLocation: URI; builtinExtensionsPath: string; - // --- use keytar for credentials - disableKeytar?: boolean; + // --- use in-memory Secret Storage + useInMemorySecretStorage?: boolean; crossOriginIsolated?: boolean; diff --git a/src/vs/platform/environment/common/environmentService.ts b/src/vs/platform/environment/common/environmentService.ts index 3d1adfa7619..6d77e70d6b1 100644 --- a/src/vs/platform/environment/common/environmentService.ts +++ b/src/vs/platform/environment/common/environmentService.ts @@ -237,7 +237,7 @@ export abstract class AbstractNativeEnvironmentService implements INativeEnviron get disableWorkspaceTrust(): boolean { return !!this.args['disable-workspace-trust']; } @memoize - get disableKeytar(): boolean { return !!this.args['disable-keytar']; } + get useInMemorySecretStorage(): boolean { return !!this.args['use-inmemory-secretstorage']; } @memoize get policyFile(): URI | undefined { diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 63243fcf3a0..c69605d16e9 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -149,7 +149,7 @@ export const OPTIONS: OptionDescriptions> = { 'skip-welcome': { type: 'boolean' }, 'disable-telemetry': { type: 'boolean' }, 'disable-updates': { type: 'boolean' }, - 'disable-keytar': { type: 'boolean' }, + 'use-inmemory-secretstorage': { type: 'boolean', deprecates: ['disable-keytar'] }, 'password-store': { type: 'string' }, 'disable-workspace-trust': { type: 'boolean' }, 'disable-crash-reporter': { type: 'boolean' }, diff --git a/src/vs/platform/environment/test/electron-main/environmentMainService.test.ts b/src/vs/platform/environment/test/electron-main/environmentMainService.test.ts index deb22c605fc..78fd7354520 100644 --- a/src/vs/platform/environment/test/electron-main/environmentMainService.test.ts +++ b/src/vs/platform/environment/test/electron-main/environmentMainService.test.ts @@ -7,6 +7,7 @@ import * as assert from 'assert'; import { EnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import product from 'vs/platform/product/common/product'; import { isLinux } from 'vs/base/common/platform'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('EnvironmentMainService', () => { @@ -125,4 +126,6 @@ suite('EnvironmentMainService', () => { assert.strictEqual(process.env['TEST_ARG3'], 'test_arg3_non_empty'); } }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/platform/environment/test/node/environmentService.test.ts b/src/vs/platform/environment/test/node/environmentService.test.ts index 85cb855f1c9..ffe418fc702 100644 --- a/src/vs/platform/environment/test/node/environmentService.test.ts +++ b/src/vs/platform/environment/test/node/environmentService.test.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { parseExtensionHostDebugPort } from 'vs/platform/environment/common/environmentService'; import { OPTIONS, parseArgs } from 'vs/platform/environment/node/argv'; import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; @@ -67,4 +68,6 @@ suite('EnvironmentService', () => { const service2 = new NativeEnvironmentService(args, { _serviceBrand: undefined, ...product }); assert.notStrictEqual(service1.userDataPath, service2.userDataPath); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/platform/extensionManagement/common/extensionNls.ts b/src/vs/platform/extensionManagement/common/extensionNls.ts index 5a3e2dd582e..2f196970f3f 100644 --- a/src/vs/platform/extensionManagement/common/extensionNls.ts +++ b/src/vs/platform/extensionManagement/common/extensionNls.ts @@ -7,16 +7,17 @@ import { isObject, isString } from 'vs/base/common/types'; import { ILocalizedString } from 'vs/platform/action/common/action'; import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; import { localize } from 'vs/nls'; +import { ILogger } from 'vs/platform/log/common/log'; export interface ITranslations { [key: string]: string | { message: string; comment: string[] } | undefined; } -export function localizeManifest(extensionManifest: IExtensionManifest, translations: ITranslations, fallbackTranslations?: ITranslations): IExtensionManifest { +export function localizeManifest(logger: ILogger, extensionManifest: IExtensionManifest, translations: ITranslations, fallbackTranslations?: ITranslations): IExtensionManifest { try { - replaceNLStrings(extensionManifest, translations, fallbackTranslations); + replaceNLStrings(logger, extensionManifest, translations, fallbackTranslations); } catch (error) { - console.error(error?.message ?? error); + logger.error(error?.message ?? error); /*Ignore Error*/ } return extensionManifest; @@ -26,7 +27,7 @@ export function localizeManifest(extensionManifest: IExtensionManifest, translat * This routine makes the following assumptions: * The root element is an object literal */ -function replaceNLStrings(extensionManifest: IExtensionManifest, messages: ITranslations, originalMessages?: ITranslations): void { +function replaceNLStrings(logger: ILogger, extensionManifest: IExtensionManifest, messages: ITranslations, originalMessages?: ITranslations): void { const processEntry = (obj: any, key: string | number, command?: boolean) => { const value = obj[key]; if (isString(value)) { @@ -48,7 +49,7 @@ function replaceNLStrings(extensionManifest: IExtensionManifest, messages: ITran if (!message) { if (!originalMessage) { - console.warn(`[${extensionManifest.name}]: ${localize('missingNLSKey', "Couldn't find message for key {0}.", messageKey)}`); + logger.warn(`[${extensionManifest.name}]: ${localize('missingNLSKey', "Couldn't find message for key {0}.", messageKey)}`); } return; } diff --git a/src/vs/platform/extensionManagement/common/extensionTipsService.ts b/src/vs/platform/extensionManagement/common/extensionTipsService.ts index 34cea78ca9b..9c99ae68bb0 100644 --- a/src/vs/platform/extensionManagement/common/extensionTipsService.ts +++ b/src/vs/platform/extensionManagement/common/extensionTipsService.ts @@ -11,7 +11,7 @@ import { URI } from 'vs/base/common/uri'; import { IConfigBasedExtensionTip, IExecutableBasedExtensionTip, IExtensionManagementService, IExtensionTipsService, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IFileService } from 'vs/platform/files/common/files'; import { IProductService } from 'vs/platform/product/common/productService'; -import { disposableTimeout, timeout } from 'vs/base/common/async'; +import { disposableTimeout } from 'vs/base/common/async'; import { IStringDictionary } from 'vs/base/common/collections'; import { Event } from 'vs/base/common/event'; import { join } from 'vs/base/common/path'; @@ -154,11 +154,11 @@ export abstract class AbstractNativeExtensionTipsService extends ExtensionTipsSe 3s has come out to be the good number to fetch and prompt important exe based recommendations Also fetch important exe based recommendations for reporting telemetry */ - timeout(3000).then(async () => { + this._register(disposableTimeout(async () => { await this.collectTips(); this.promptHighImportanceExeBasedTip(); this.promptMediumImportanceExeBasedTip(); - }); + }, 3000)); } override async getImportantExecutableBasedTips(): Promise { diff --git a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts index b25fe1cfc56..36067bcad7f 100644 --- a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts @@ -712,7 +712,7 @@ class ExtensionsScanner extends Disposable { return extensionManifest; } const localized = localizedMessages.values || Object.create(null); - return localizeManifest(extensionManifest, localized, defaults); + return localizeManifest(this.logService, extensionManifest, localized, defaults); } catch (error) { /*Ignore Error*/ } diff --git a/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts b/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts index 7dcb41d56bf..a471a929653 100644 --- a/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts +++ b/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { DisposableStore } from 'vs/base/common/lifecycle'; import { joinPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { isUUID } from 'vs/base/common/uuid'; @@ -24,6 +23,7 @@ import { InMemoryStorageService, IStorageService } from 'vs/platform/storage/com import { TelemetryConfiguration, TELEMETRY_SETTING_ID } from 'vs/platform/telemetry/common/telemetry'; import { TargetPlatform } from 'vs/platform/extensions/common/extensions'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; class EnvironmentServiceMock extends mock() { override readonly serviceMachineIdResource: URI; @@ -35,7 +35,7 @@ class EnvironmentServiceMock extends mock() { } suite('Extension Gallery Service', () => { - const disposables: DisposableStore = new DisposableStore(); + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); let fileService: IFileService, environmentService: IEnvironmentService, storageService: IStorageService, productService: IProductService, configurationService: IConfigurationService; setup(() => { @@ -43,15 +43,13 @@ suite('Extension Gallery Service', () => { environmentService = new EnvironmentServiceMock(serviceMachineIdResource); fileService = disposables.add(new FileService(new NullLogService())); const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider()); - fileService.registerProvider(serviceMachineIdResource.scheme, fileSystemProvider); - storageService = new InMemoryStorageService(); + disposables.add(fileService.registerProvider(serviceMachineIdResource.scheme, fileSystemProvider)); + storageService = disposables.add(new InMemoryStorageService()); configurationService = new TestConfigurationService({ [TELEMETRY_SETTING_ID]: TelemetryConfiguration.ON }); configurationService.updateValue(TELEMETRY_SETTING_ID, TelemetryConfiguration.ON); productService = { _serviceBrand: undefined, ...product, enableTelemetry: true }; }); - teardown(() => disposables.clear()); - test('marketplace machine id', async () => { const headers = await resolveMarketplaceHeaders(product.version, productService, environmentService, configurationService, fileService, storageService, NullTelemetryService); assert.ok(isUUID(headers['X-Market-User-Id'])); diff --git a/src/vs/platform/extensionManagement/test/common/extensionNls.test.ts b/src/vs/platform/extensionManagement/test/common/extensionNls.test.ts index 533e23290b3..a3c2603a1eb 100644 --- a/src/vs/platform/extensionManagement/test/common/extensionNls.test.ts +++ b/src/vs/platform/extensionManagement/test/common/extensionNls.test.ts @@ -5,9 +5,11 @@ import * as assert from 'assert'; import { deepClone } from 'vs/base/common/objects'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ILocalizedString } from 'vs/platform/action/common/action'; import { localizeManifest } from 'vs/platform/extensionManagement/common/extensionNls'; import { IExtensionManifest, IConfiguration } from 'vs/platform/extensions/common/extensions'; +import { NullLogger } from 'vs/platform/log/common/log'; const manifest: IExtensionManifest = { name: 'test', @@ -44,8 +46,10 @@ const manifest: IExtensionManifest = { }; suite('Localize Manifest', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); test('replaces template strings', function () { const localizedManifest = localizeManifest( + store.add(new NullLogger()), deepClone(manifest), { 'test.command.title': 'Test Command', @@ -63,6 +67,7 @@ suite('Localize Manifest', () => { test('replaces template strings with fallback if not found in translations', function () { const localizedManifest = localizeManifest( + store.add(new NullLogger()), deepClone(manifest), {}, { @@ -81,6 +86,7 @@ suite('Localize Manifest', () => { test('replaces template strings - command title & categories become ILocalizedString', function () { const localizedManifest = localizeManifest( + store.add(new NullLogger()), deepClone(manifest), { 'test.command.title': 'Befehl test', @@ -135,6 +141,7 @@ suite('Localize Manifest', () => { }; const localizedManifest = localizeManifest( + store.add(new NullLogger()), deepClone(manifestWithTypo), { 'test.command.title': 'Test Command', diff --git a/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts b/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts index b2fa0668e72..9636eabcf69 100644 --- a/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts +++ b/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts @@ -6,9 +6,9 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; import { VSBuffer } from 'vs/base/common/buffer'; -import { DisposableStore } from 'vs/base/common/lifecycle'; import { joinPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { AbstractExtensionsProfileScannerService, ProfileExtensionsEvent } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService'; import { ExtensionType, IExtension, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; @@ -28,7 +28,7 @@ class TestObject extends AbstractExtensionsProfileScannerService { } suite('ExtensionsProfileScannerService', () => { const ROOT = URI.file('/ROOT'); - const disposables = new DisposableStore(); + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); const extensionsLocation = joinPath(ROOT, 'extensions'); let instantiationService: TestInstantiationService; @@ -38,22 +38,20 @@ suite('ExtensionsProfileScannerService', () => { const logService = new NullLogService(); const fileService = disposables.add(new FileService(logService)); const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider()); - fileService.registerProvider(ROOT.scheme, fileSystemProvider); + disposables.add(fileService.registerProvider(ROOT.scheme, fileSystemProvider)); instantiationService.stub(ILogService, logService); instantiationService.stub(IFileService, fileService); instantiationService.stub(ITelemetryService, NullTelemetryService); - const uriIdentityService = instantiationService.stub(IUriIdentityService, new UriIdentityService(fileService)); + const uriIdentityService = instantiationService.stub(IUriIdentityService, disposables.add(new UriIdentityService(fileService))); const environmentService = instantiationService.stub(IEnvironmentService, { userRoamingDataHome: ROOT, cacheHome: joinPath(ROOT, 'cache'), }); - const userDataProfilesService = new UserDataProfilesService(environmentService, fileService, uriIdentityService, logService); + const userDataProfilesService = disposables.add(new UserDataProfilesService(environmentService, fileService, uriIdentityService, logService)); instantiationService.stub(IUserDataProfilesService, userDataProfilesService); }); - teardown(() => disposables.clear()); - suiteTeardown(() => sinon.restore()); test('write extensions located in the same extensions folder', async () => { - const testObject = instantiationService.createInstance(TestObject, extensionsLocation); + const testObject = disposables.add(instantiationService.createInstance(TestObject, extensionsLocation)); const extensionsManifest = joinPath(extensionsLocation, 'extensions.json'); const extension = aExtension('pub.a', joinPath(extensionsLocation, 'pub.a-1.0.0')); @@ -64,7 +62,7 @@ suite('ExtensionsProfileScannerService', () => { }); test('write extensions located in the different folder', async () => { - const testObject = instantiationService.createInstance(TestObject, extensionsLocation); + const testObject = disposables.add(instantiationService.createInstance(TestObject, extensionsLocation)); const extensionsManifest = joinPath(extensionsLocation, 'extensions.json'); const extension = aExtension('pub.a', joinPath(ROOT, 'pub.a-1.0.0')); @@ -75,7 +73,7 @@ suite('ExtensionsProfileScannerService', () => { }); test('write extensions located in the same extensions folder has relative location ', async () => { - const testObject = instantiationService.createInstance(TestObject, extensionsLocation); + const testObject = disposables.add(instantiationService.createInstance(TestObject, extensionsLocation)); const extensionsManifest = joinPath(extensionsLocation, 'extensions.json'); const extension = aExtension('pub.a', joinPath(extensionsLocation, 'pub.a-1.0.0')); @@ -86,7 +84,7 @@ suite('ExtensionsProfileScannerService', () => { }); test('write extensions located in different extensions folder does not has relative location ', async () => { - const testObject = instantiationService.createInstance(TestObject, extensionsLocation); + const testObject = disposables.add(instantiationService.createInstance(TestObject, extensionsLocation)); const extensionsManifest = joinPath(extensionsLocation, 'extensions.json'); const extension = aExtension('pub.a', joinPath(ROOT, 'pub.a-1.0.0')); @@ -105,7 +103,7 @@ suite('ExtensionsProfileScannerService', () => { version: extension.manifest.version, }]))); - const testObject = instantiationService.createInstance(TestObject, extensionsLocation); + const testObject = disposables.add(instantiationService.createInstance(TestObject, extensionsLocation)); const actual = await testObject.scanProfileExtensions(extensionsManifest); assert.deepStrictEqual(actual.map(a => ({ ...a, location: a.location.toJSON() })), [{ identifier: extension.identifier, location: extension.location.toJSON(), version: extension.manifest.version, metadata: undefined }]); @@ -123,7 +121,7 @@ suite('ExtensionsProfileScannerService', () => { version: extension.manifest.version, }]))); - const testObject = instantiationService.createInstance(TestObject, extensionsLocation); + const testObject = disposables.add(instantiationService.createInstance(TestObject, extensionsLocation)); const actual = await testObject.scanProfileExtensions(extensionsManifest); assert.deepStrictEqual(actual.map(a => ({ ...a, location: a.location.toJSON() })), [{ identifier: extension.identifier, location: extension.location.toJSON(), version: extension.manifest.version, metadata: undefined }]); @@ -141,7 +139,7 @@ suite('ExtensionsProfileScannerService', () => { version: extension.manifest.version, }]))); - const testObject = instantiationService.createInstance(TestObject, extensionsLocation); + const testObject = disposables.add(instantiationService.createInstance(TestObject, extensionsLocation)); const extension2 = aExtension('pub.b', joinPath(extensionsLocation, 'pub.b-1.0.0')); await testObject.addExtensionsToProfile([[extension2, undefined]], extensionsManifest); @@ -173,7 +171,7 @@ suite('ExtensionsProfileScannerService', () => { version: extension2.manifest.version, }]))); - const testObject = instantiationService.createInstance(TestObject, extensionsLocation); + const testObject = disposables.add(instantiationService.createInstance(TestObject, extensionsLocation)); const actual = await testObject.scanProfileExtensions(extensionsManifest); assert.deepStrictEqual(actual.map(a => ({ ...a, location: a.location.toJSON() })), [ @@ -197,7 +195,7 @@ suite('ExtensionsProfileScannerService', () => { version: extension.manifest.version, }]))); - const testObject = instantiationService.createInstance(TestObject, extensionsLocation); + const testObject = disposables.add(instantiationService.createInstance(TestObject, extensionsLocation)); const actual = await testObject.scanProfileExtensions(extensionsManifest); assert.deepStrictEqual(actual.map(a => ({ ...a, location: a.location.toJSON() })), [{ identifier: extension.identifier, location: extension.location.toJSON(), version: extension.manifest.version, metadata: undefined }]); @@ -215,7 +213,7 @@ suite('ExtensionsProfileScannerService', () => { version: extension.manifest.version, }]))); - const testObject = instantiationService.createInstance(TestObject, extensionsLocation); + const testObject = disposables.add(instantiationService.createInstance(TestObject, extensionsLocation)); const extension2 = aExtension('pub.b', joinPath(extensionsLocation, 'pub.b-1.0.0')); await testObject.addExtensionsToProfile([[extension2, undefined]], extensionsManifest); @@ -247,7 +245,7 @@ suite('ExtensionsProfileScannerService', () => { version: extension2.manifest.version, }]))); - const testObject = instantiationService.createInstance(TestObject, extensionsLocation); + const testObject = disposables.add(instantiationService.createInstance(TestObject, extensionsLocation)); const actual = await testObject.scanProfileExtensions(extensionsManifest); assert.deepStrictEqual(actual.map(a => ({ ...a, location: a.location.toJSON() })), [ @@ -287,7 +285,7 @@ suite('ExtensionsProfileScannerService', () => { version: extension4.manifest.version, }]))); - const testObject = instantiationService.createInstance(TestObject, extensionsLocation); + const testObject = disposables.add(instantiationService.createInstance(TestObject, extensionsLocation)); const actual = await testObject.scanProfileExtensions(extensionsManifest); assert.deepStrictEqual(actual.map(a => ({ ...a, location: a.location.toJSON() })), [ @@ -316,7 +314,7 @@ suite('ExtensionsProfileScannerService', () => { relativePath: 2 }]))); - const testObject = instantiationService.createInstance(TestObject, extensionsLocation); + const testObject = disposables.add(instantiationService.createInstance(TestObject, extensionsLocation)); try { await testObject.scanProfileExtensions(extensionsManifest); @@ -333,7 +331,7 @@ suite('ExtensionsProfileScannerService', () => { relativePath: 'pub.a-1.0.0' }]))); - const testObject = instantiationService.createInstance(TestObject, extensionsLocation); + const testObject = disposables.add(instantiationService.createInstance(TestObject, extensionsLocation)); try { await testObject.scanProfileExtensions(extensionsManifest); @@ -351,7 +349,7 @@ suite('ExtensionsProfileScannerService', () => { relativePath: 'pub.a-1.0.0' }]))); - const testObject = instantiationService.createInstance(TestObject, extensionsLocation); + const testObject = disposables.add(instantiationService.createInstance(TestObject, extensionsLocation)); try { await testObject.scanProfileExtensions(extensionsManifest); @@ -367,7 +365,7 @@ suite('ExtensionsProfileScannerService', () => { version: extension.manifest.version, }]))); - const testObject = instantiationService.createInstance(TestObject, extensionsLocation); + const testObject = disposables.add(instantiationService.createInstance(TestObject, extensionsLocation)); try { await testObject.scanProfileExtensions(extensionsManifest); @@ -384,7 +382,7 @@ suite('ExtensionsProfileScannerService', () => { version: extension.manifest.version, }]))); - const testObject = instantiationService.createInstance(TestObject, extensionsLocation); + const testObject = disposables.add(instantiationService.createInstance(TestObject, extensionsLocation)); try { await testObject.scanProfileExtensions(extensionsManifest); @@ -400,7 +398,7 @@ suite('ExtensionsProfileScannerService', () => { location: extension.location.toJSON(), }]))); - const testObject = instantiationService.createInstance(TestObject, extensionsLocation); + const testObject = disposables.add(instantiationService.createInstance(TestObject, extensionsLocation)); try { await testObject.scanProfileExtensions(extensionsManifest); @@ -412,7 +410,7 @@ suite('ExtensionsProfileScannerService', () => { const extensionsManifest = joinPath(extensionsLocation, 'extensions.json'); await instantiationService.get(IFileService).writeFile(extensionsManifest, VSBuffer.fromString('')); - const testObject = instantiationService.createInstance(TestObject, extensionsLocation); + const testObject = disposables.add(instantiationService.createInstance(TestObject, extensionsLocation)); const actual = await testObject.scanProfileExtensions(extensionsManifest); assert.deepStrictEqual(actual, []); }); @@ -423,7 +421,7 @@ suite('ExtensionsProfileScannerService', () => { `)); - const testObject = instantiationService.createInstance(TestObject, extensionsLocation); + const testObject = disposables.add(instantiationService.createInstance(TestObject, extensionsLocation)); const actual = await testObject.scanProfileExtensions(extensionsManifest); assert.deepStrictEqual(actual, []); }); @@ -438,7 +436,7 @@ suite('ExtensionsProfileScannerService', () => { version: extension.manifest.version, }]))); - const testObject = instantiationService.createInstance(TestObject, extensionsLocation); + const testObject = disposables.add(instantiationService.createInstance(TestObject, extensionsLocation)); const actual = await testObject.scanProfileExtensions(extensionsManifest); assert.deepStrictEqual(actual.map(a => ({ ...a, location: a.location.toJSON() })), [{ identifier: extension.identifier, location: extension.location.toJSON(), version: extension.manifest.version, metadata: undefined }]); @@ -448,11 +446,11 @@ suite('ExtensionsProfileScannerService', () => { }); test('add extension trigger events', async () => { - const testObject = instantiationService.createInstance(TestObject, extensionsLocation); + const testObject = disposables.add(instantiationService.createInstance(TestObject, extensionsLocation)); const target1 = sinon.stub(); const target2 = sinon.stub(); - testObject.onAddExtensions(target1); - testObject.onDidAddExtensions(target2); + disposables.add(testObject.onAddExtensions(target1)); + disposables.add(testObject.onDidAddExtensions(target2)); const extensionsManifest = joinPath(extensionsLocation, 'extensions.json'); const extension = aExtension('pub.a', joinPath(ROOT, 'foo', 'pub.a-1.0.0')); @@ -477,11 +475,11 @@ suite('ExtensionsProfileScannerService', () => { }); test('remove extension trigger events', async () => { - const testObject = instantiationService.createInstance(TestObject, extensionsLocation); + const testObject = disposables.add(instantiationService.createInstance(TestObject, extensionsLocation)); const target1 = sinon.stub(); const target2 = sinon.stub(); - testObject.onRemoveExtensions(target1); - testObject.onDidRemoveExtensions(target2); + disposables.add(testObject.onRemoveExtensions(target1)); + disposables.add(testObject.onDidRemoveExtensions(target2)); const extensionsManifest = joinPath(extensionsLocation, 'extensions.json'); const extension = aExtension('pub.a', joinPath(ROOT, 'foo', 'pub.a-1.0.0')); @@ -507,7 +505,7 @@ suite('ExtensionsProfileScannerService', () => { }); test('add extension with same id but different version', async () => { - const testObject = instantiationService.createInstance(TestObject, extensionsLocation); + const testObject = disposables.add(instantiationService.createInstance(TestObject, extensionsLocation)); const extensionsManifest = joinPath(extensionsLocation, 'extensions.json'); @@ -518,10 +516,10 @@ suite('ExtensionsProfileScannerService', () => { const target2 = sinon.stub(); const target3 = sinon.stub(); const target4 = sinon.stub(); - testObject.onAddExtensions(target1); - testObject.onRemoveExtensions(target2); - testObject.onDidAddExtensions(target3); - testObject.onDidRemoveExtensions(target4); + disposables.add(testObject.onAddExtensions(target1)); + disposables.add(testObject.onRemoveExtensions(target2)); + disposables.add(testObject.onDidAddExtensions(target3)); + disposables.add(testObject.onDidRemoveExtensions(target4)); const extension2 = aExtension('pub.a', joinPath(ROOT, 'pub.a-2.0.0'), undefined, { version: '2.0.0' }); await testObject.addExtensionsToProfile([[extension2, undefined]], extensionsManifest); @@ -558,7 +556,7 @@ suite('ExtensionsProfileScannerService', () => { }); test('add same extension', async () => { - const testObject = instantiationService.createInstance(TestObject, extensionsLocation); + const testObject = disposables.add(instantiationService.createInstance(TestObject, extensionsLocation)); const extensionsManifest = joinPath(extensionsLocation, 'extensions.json'); @@ -569,10 +567,10 @@ suite('ExtensionsProfileScannerService', () => { const target2 = sinon.stub(); const target3 = sinon.stub(); const target4 = sinon.stub(); - testObject.onAddExtensions(target1); - testObject.onRemoveExtensions(target2); - testObject.onDidAddExtensions(target3); - testObject.onDidRemoveExtensions(target4); + disposables.add(testObject.onAddExtensions(target1)); + disposables.add(testObject.onRemoveExtensions(target2)); + disposables.add(testObject.onDidAddExtensions(target3)); + disposables.add(testObject.onDidRemoveExtensions(target4)); await testObject.addExtensionsToProfile([[extension, undefined]], extensionsManifest); const actual = await testObject.scanProfileExtensions(extensionsManifest); @@ -584,7 +582,7 @@ suite('ExtensionsProfileScannerService', () => { }); test('add same extension with different metadata', async () => { - const testObject = instantiationService.createInstance(TestObject, extensionsLocation); + const testObject = disposables.add(instantiationService.createInstance(TestObject, extensionsLocation)); const extensionsManifest = joinPath(extensionsLocation, 'extensions.json'); @@ -595,10 +593,10 @@ suite('ExtensionsProfileScannerService', () => { const target2 = sinon.stub(); const target3 = sinon.stub(); const target4 = sinon.stub(); - testObject.onAddExtensions(target1); - testObject.onRemoveExtensions(target2); - testObject.onDidAddExtensions(target3); - testObject.onDidRemoveExtensions(target4); + disposables.add(testObject.onAddExtensions(target1)); + disposables.add(testObject.onRemoveExtensions(target2)); + disposables.add(testObject.onDidAddExtensions(target3)); + disposables.add(testObject.onDidRemoveExtensions(target4)); await testObject.addExtensionsToProfile([[extension, { isApplicationScoped: true }]], extensionsManifest); const actual = await testObject.scanProfileExtensions(extensionsManifest); @@ -610,7 +608,7 @@ suite('ExtensionsProfileScannerService', () => { }); test('add extension with different version and metadata', async () => { - const testObject = instantiationService.createInstance(TestObject, extensionsLocation); + const testObject = disposables.add(instantiationService.createInstance(TestObject, extensionsLocation)); const extensionsManifest = joinPath(extensionsLocation, 'extensions.json'); @@ -622,10 +620,10 @@ suite('ExtensionsProfileScannerService', () => { const target2 = sinon.stub(); const target3 = sinon.stub(); const target4 = sinon.stub(); - testObject.onAddExtensions(target1); - testObject.onRemoveExtensions(target2); - testObject.onDidAddExtensions(target3); - testObject.onDidRemoveExtensions(target4); + disposables.add(testObject.onAddExtensions(target1)); + disposables.add(testObject.onRemoveExtensions(target2)); + disposables.add(testObject.onDidAddExtensions(target3)); + disposables.add(testObject.onDidRemoveExtensions(target4)); await testObject.addExtensionsToProfile([[extension2, { isApplicationScoped: true }]], extensionsManifest); const actual = await testObject.scanProfileExtensions(extensionsManifest); @@ -661,7 +659,7 @@ suite('ExtensionsProfileScannerService', () => { }); test('add extension with same id and version located in the different folder', async () => { - const testObject = instantiationService.createInstance(TestObject, extensionsLocation); + const testObject = disposables.add(instantiationService.createInstance(TestObject, extensionsLocation)); const extensionsManifest = joinPath(extensionsLocation, 'extensions.json'); @@ -672,10 +670,10 @@ suite('ExtensionsProfileScannerService', () => { const target2 = sinon.stub(); const target3 = sinon.stub(); const target4 = sinon.stub(); - testObject.onAddExtensions(target1); - testObject.onRemoveExtensions(target2); - testObject.onDidAddExtensions(target3); - testObject.onDidRemoveExtensions(target4); + disposables.add(testObject.onAddExtensions(target1)); + disposables.add(testObject.onRemoveExtensions(target2)); + disposables.add(testObject.onDidAddExtensions(target3)); + disposables.add(testObject.onDidRemoveExtensions(target4)); extension = aExtension('pub.a', joinPath(ROOT, 'pub.a-1.0.0')); await testObject.addExtensionsToProfile([[extension, undefined]], extensionsManifest); diff --git a/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts b/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts index 6834bb4c10f..8994ead0bf4 100644 --- a/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts +++ b/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; -import { DisposableStore } from 'vs/base/common/lifecycle'; import { dirname, joinPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; import { IExtensionsProfileScannerService, IProfileExtensionsScanOptions } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService'; import { AbstractExtensionsScannerService, ExtensionScannerInput, IExtensionsScannerService, IScannedExtensionManifest, Translations } from 'vs/platform/extensionManagement/common/extensionsScannerService'; @@ -55,7 +55,7 @@ class ExtensionsScannerService extends AbstractExtensionsScannerService implemen suite('NativeExtensionsScanerService Test', () => { - const disposables = new DisposableStore(); + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); let instantiationService: TestInstantiationService; setup(async () => { @@ -64,7 +64,7 @@ suite('NativeExtensionsScanerService Test', () => { const logService = new NullLogService(); const fileService = disposables.add(new FileService(logService)); const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider()); - fileService.registerProvider(ROOT.scheme, fileSystemProvider); + disposables.add(fileService.registerProvider(ROOT.scheme, fileSystemProvider)); instantiationService.stub(ILogService, logService); instantiationService.stub(IFileService, fileService); const systemExtensionsLocation = joinPath(ROOT, 'system'); @@ -77,21 +77,19 @@ suite('NativeExtensionsScanerService Test', () => { cacheHome: joinPath(ROOT, 'cache'), }); instantiationService.stub(IProductService, { version: '1.66.0' }); - const uriIdentityService = new UriIdentityService(fileService); + const uriIdentityService = disposables.add(new UriIdentityService(fileService)); instantiationService.stub(IUriIdentityService, uriIdentityService); - const userDataProfilesService = new UserDataProfilesService(environmentService, fileService, uriIdentityService, logService); + const userDataProfilesService = disposables.add(new UserDataProfilesService(environmentService, fileService, uriIdentityService, logService)); instantiationService.stub(IUserDataProfilesService, userDataProfilesService); - instantiationService.stub(IExtensionsProfileScannerService, new ExtensionsProfileScannerService(environmentService, fileService, userDataProfilesService, uriIdentityService, NullTelemetryService, logService)); + instantiationService.stub(IExtensionsProfileScannerService, disposables.add(new ExtensionsProfileScannerService(environmentService, fileService, userDataProfilesService, uriIdentityService, NullTelemetryService, logService))); await fileService.createFolder(systemExtensionsLocation); await fileService.createFolder(userExtensionsLocation); }); - teardown(() => disposables.clear()); - test('scan system extension', async () => { const manifest: Partial = anExtensionManifest({ 'name': 'name', 'publisher': 'pub' }); const extensionLocation = await aSystemExtension(manifest); - const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); + const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); const actual = await testObject.scanSystemExtensions({}); @@ -110,7 +108,7 @@ suite('NativeExtensionsScanerService Test', () => { test('scan user extension', async () => { const manifest: Partial = anExtensionManifest({ 'name': 'name', 'publisher': 'pub', __metadata: { id: 'uuid' } }); const extensionLocation = await aUserExtension(manifest); - const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); + const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); const actual = await testObject.scanUserExtensions({}); @@ -130,7 +128,7 @@ suite('NativeExtensionsScanerService Test', () => { test('scan existing extension', async () => { const manifest: Partial = anExtensionManifest({ 'name': 'name', 'publisher': 'pub' }); const extensionLocation = await aUserExtension(manifest); - const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); + const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); const actual = await testObject.scanExistingExtension(extensionLocation, ExtensionType.User, {}); @@ -149,7 +147,7 @@ suite('NativeExtensionsScanerService Test', () => { test('scan single extension', async () => { const manifest: Partial = anExtensionManifest({ 'name': 'name', 'publisher': 'pub' }); const extensionLocation = await aUserExtension(manifest); - const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); + const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); const actual = await testObject.scanOneOrMultipleExtensions(extensionLocation, ExtensionType.User, {}); @@ -168,7 +166,7 @@ suite('NativeExtensionsScanerService Test', () => { test('scan multiple extensions', async () => { const extensionLocation = await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' })); await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub' })); - const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); + const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); const actual = await testObject.scanOneOrMultipleExtensions(dirname(extensionLocation), ExtensionType.User, {}); @@ -180,7 +178,7 @@ suite('NativeExtensionsScanerService Test', () => { test('scan user extension with different versions', async () => { await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.1' })); await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.2' })); - const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); + const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); const actual = await testObject.scanUserExtensions({}); @@ -192,7 +190,7 @@ suite('NativeExtensionsScanerService Test', () => { test('scan user extension include all versions', async () => { await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.1' })); await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.2' })); - const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); + const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); const actual = await testObject.scanUserExtensions({ includeAllVersions: true }); @@ -206,7 +204,7 @@ suite('NativeExtensionsScanerService Test', () => { test('scan user extension with different versions and higher version is not compatible', async () => { await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.1' })); await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.2', engines: { vscode: '^1.67.0' } })); - const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); + const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); const actual = await testObject.scanUserExtensions({}); @@ -218,7 +216,7 @@ suite('NativeExtensionsScanerService Test', () => { test('scan exclude invalid extensions', async () => { await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' })); await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub', engines: { vscode: '^1.67.0' } })); - const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); + const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); const actual = await testObject.scanUserExtensions({}); @@ -230,7 +228,7 @@ suite('NativeExtensionsScanerService Test', () => { await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' })); await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub' })); await instantiationService.get(IFileService).writeFile(joinPath(URI.file(instantiationService.get(INativeEnvironmentService).extensionsPath), '.obsolete'), VSBuffer.fromString(JSON.stringify({ 'pub.name2-1.0.0': true }))); - const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); + const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); const actual = await testObject.scanUserExtensions({}); @@ -242,7 +240,7 @@ suite('NativeExtensionsScanerService Test', () => { await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' })); await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub' })); await instantiationService.get(IFileService).writeFile(joinPath(URI.file(instantiationService.get(INativeEnvironmentService).extensionsPath), '.obsolete'), VSBuffer.fromString(JSON.stringify({ 'pub.name2-1.0.0': true }))); - const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); + const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); const actual = await testObject.scanUserExtensions({ includeUninstalled: true }); @@ -254,7 +252,7 @@ suite('NativeExtensionsScanerService Test', () => { test('scan include invalid extensions', async () => { await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' })); await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub', engines: { vscode: '^1.67.0' } })); - const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); + const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); const actual = await testObject.scanUserExtensions({ includeInvalid: true }); @@ -275,7 +273,7 @@ suite('NativeExtensionsScanerService Test', () => { const extensionLocation = await anExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' }), joinPath(ROOT, 'additional')); await aSystemExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.1' })); await instantiationService.get(IFileService).writeFile(joinPath(instantiationService.get(INativeEnvironmentService).userHome, '.vscode-oss-dev', 'extensions', 'control.json'), VSBuffer.fromString(JSON.stringify({ 'pub.name2': 'disabled', 'pub.name': extensionLocation.fsPath }))); - const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); + const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); const actual = await testObject.scanSystemExtensions({ checkControlFile: true }); @@ -287,7 +285,7 @@ suite('NativeExtensionsScanerService Test', () => { test('scan extension with default nls replacements', async () => { const extensionLocation = await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', displayName: '%displayName%' })); await instantiationService.get(IFileService).writeFile(joinPath(extensionLocation, 'package.nls.json'), VSBuffer.fromString(JSON.stringify({ displayName: 'Hello World' }))); - const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); + const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); const actual = await testObject.scanUserExtensions({}); @@ -301,7 +299,7 @@ suite('NativeExtensionsScanerService Test', () => { await instantiationService.get(IFileService).writeFile(joinPath(extensionLocation, 'package.nls.json'), VSBuffer.fromString(JSON.stringify({ displayName: 'Hello World' }))); const nlsLocation = joinPath(extensionLocation, 'package.en.json'); await instantiationService.get(IFileService).writeFile(nlsLocation, VSBuffer.fromString(JSON.stringify({ contents: { package: { displayName: 'Hello World EN' } } }))); - const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); + const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); translations = { 'pub.name': nlsLocation.fsPath }; const actual = await testObject.scanUserExtensions({ language: 'en' }); @@ -316,7 +314,7 @@ suite('NativeExtensionsScanerService Test', () => { await instantiationService.get(IFileService).writeFile(joinPath(extensionLocation, 'package.nls.json'), VSBuffer.fromString(JSON.stringify({ displayName: 'Hello World' }))); const nlsLocation = joinPath(extensionLocation, 'package.en.json'); await instantiationService.get(IFileService).writeFile(nlsLocation, VSBuffer.fromString(JSON.stringify({ contents: { package: { displayName: 'Hello World EN' } } }))); - const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); + const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); translations = { 'pub.name2': nlsLocation.fsPath }; const actual = await testObject.scanUserExtensions({ language: 'en' }); diff --git a/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts b/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts index a4ca27c91fc..51395a86737 100644 --- a/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts +++ b/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts @@ -14,6 +14,7 @@ import { isBoolean } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { mock } from 'vs/base/test/common/mock'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -74,9 +75,9 @@ class TestInstallGalleryExtensionTask extends InstallGalleryExtensionTask { ) { const instantiationService = disposables.add(new TestInstantiationService()); const logService = instantiationService.stub(ILogService, new NullLogService()); - const fileService = instantiationService.stub(IFileService, new FileService(logService)); - const fileSystemProvider = new InMemoryFileSystemProvider(); - fileService.registerProvider(ROOT.scheme, fileSystemProvider); + const fileService = instantiationService.stub(IFileService, disposables.add(new FileService(logService))); + const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider()); + disposables.add(fileService.registerProvider(ROOT.scheme, fileSystemProvider)); const systemExtensionsLocation = joinPath(ROOT, 'system'); const userExtensionsLocation = joinPath(ROOT, 'extensions'); instantiationService.stub(INativeEnvironmentService, { @@ -89,10 +90,10 @@ class TestInstallGalleryExtensionTask extends InstallGalleryExtensionTask { }); instantiationService.stub(IProductService, {}); instantiationService.stub(ITelemetryService, NullTelemetryService); - const uriIdentityService = instantiationService.stub(IUriIdentityService, instantiationService.createInstance(UriIdentityService)); - const userDataProfilesService = instantiationService.stub(IUserDataProfilesService, instantiationService.createInstance(UserDataProfilesService)); - const extensionsProfileScannerService = instantiationService.stub(IExtensionsProfileScannerService, instantiationService.createInstance(ExtensionsProfileScannerService)); - const extensionsScannerService = instantiationService.stub(IExtensionsScannerService, instantiationService.createInstance(ExtensionsScannerService)); + const uriIdentityService = instantiationService.stub(IUriIdentityService, disposables.add(instantiationService.createInstance(UriIdentityService))); + const userDataProfilesService = instantiationService.stub(IUserDataProfilesService, disposables.add(instantiationService.createInstance(UserDataProfilesService))); + const extensionsProfileScannerService = instantiationService.stub(IExtensionsProfileScannerService, disposables.add(instantiationService.createInstance(ExtensionsProfileScannerService))); + const extensionsScannerService = instantiationService.stub(IExtensionsScannerService, disposables.add(instantiationService.createInstance(ExtensionsScannerService))); super( { name: extension.name, @@ -127,12 +128,10 @@ class TestInstallGalleryExtensionTask extends InstallGalleryExtensionTask { suite('InstallGalleryExtensionTask Tests', () => { - const disposables = new DisposableStore(); - - teardown(() => disposables.clear()); + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); test('if verification is enabled by default, the task completes', async () => { - const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader({ isSignatureVerificationEnabled: true, verificationResult: true, didExecute: true }), disposables); + const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader({ isSignatureVerificationEnabled: true, verificationResult: true, didExecute: true }), disposables.add(new DisposableStore())); await testObject.run(); @@ -141,7 +140,7 @@ suite('InstallGalleryExtensionTask Tests', () => { }); test('if verification is enabled in stable, the task completes', async () => { - const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader({ isSignatureVerificationEnabled: true, verificationResult: true, didExecute: true, quality: 'stable' }), disposables); + const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader({ isSignatureVerificationEnabled: true, verificationResult: true, didExecute: true, quality: 'stable' }), disposables.add(new DisposableStore())); await testObject.run(); @@ -150,7 +149,7 @@ suite('InstallGalleryExtensionTask Tests', () => { }); test('if verification is disabled by setting set to false, the task skips verification', async () => { - const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader({ isSignatureVerificationEnabled: false, verificationResult: 'error', didExecute: false }), disposables); + const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader({ isSignatureVerificationEnabled: false, verificationResult: 'error', didExecute: false }), disposables.add(new DisposableStore())); await testObject.run(); @@ -159,7 +158,7 @@ suite('InstallGalleryExtensionTask Tests', () => { }); test('if verification is disabled because the module is not loaded, the task skips verification', async () => { - const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader({ isSignatureVerificationEnabled: true, verificationResult: false, didExecute: false }), disposables); + const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader({ isSignatureVerificationEnabled: true, verificationResult: false, didExecute: false }), disposables.add(new DisposableStore())); await testObject.run(); @@ -169,7 +168,7 @@ suite('InstallGalleryExtensionTask Tests', () => { test('if verification fails to execute, the task completes', async () => { const errorCode = 'ENOENT'; - const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader({ isSignatureVerificationEnabled: true, verificationResult: errorCode, didExecute: false }), disposables); + const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader({ isSignatureVerificationEnabled: true, verificationResult: errorCode, didExecute: false }), disposables.add(new DisposableStore())); await testObject.run(); @@ -180,7 +179,7 @@ suite('InstallGalleryExtensionTask Tests', () => { test('if verification fails', async () => { const errorCode = 'IntegrityCheckFailed'; - const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader({ isSignatureVerificationEnabled: true, verificationResult: errorCode, didExecute: true }), disposables); + const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader({ isSignatureVerificationEnabled: true, verificationResult: errorCode, didExecute: true }), disposables.add(new DisposableStore())); await testObject.run(); @@ -189,7 +188,7 @@ suite('InstallGalleryExtensionTask Tests', () => { }); test('if verification succeeds, the task completes', async () => { - const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader({ isSignatureVerificationEnabled: true, verificationResult: true, didExecute: true }), disposables); + const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader({ isSignatureVerificationEnabled: true, verificationResult: true, didExecute: true }), disposables.add(new DisposableStore())); await testObject.run(); @@ -198,7 +197,7 @@ suite('InstallGalleryExtensionTask Tests', () => { }); test('task completes for unsigned extension', async () => { - const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: false }), anExtensionsDownloader({ isSignatureVerificationEnabled: true, verificationResult: true, didExecute: false }), disposables); + const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: false }), anExtensionsDownloader({ isSignatureVerificationEnabled: true, verificationResult: true, didExecute: false }), disposables.add(new DisposableStore())); await testObject.run(); @@ -207,7 +206,7 @@ suite('InstallGalleryExtensionTask Tests', () => { }); test('task completes for an unsigned extension even when signature verification throws error', async () => { - const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: false }), anExtensionsDownloader({ isSignatureVerificationEnabled: true, verificationResult: 'error', didExecute: true }), disposables); + const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: false }), anExtensionsDownloader({ isSignatureVerificationEnabled: true, verificationResult: 'error', didExecute: true }), disposables.add(new DisposableStore())); await testObject.run(); @@ -219,9 +218,9 @@ suite('InstallGalleryExtensionTask Tests', () => { const logService = new NullLogService(); const fileService = disposables.add(new FileService(logService)); const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider()); - fileService.registerProvider(ROOT.scheme, fileSystemProvider); + disposables.add(fileService.registerProvider(ROOT.scheme, fileSystemProvider)); - const instantiationService = new TestInstantiationService(); + const instantiationService = disposables.add(new TestInstantiationService()); instantiationService.stub(IProductService, { quality: options.quality ?? 'insiders' }); instantiationService.stub(IFileService, fileService); instantiationService.stub(ILogService, logService); @@ -236,7 +235,7 @@ suite('InstallGalleryExtensionTask Tests', () => { }); instantiationService.stub(IConfigurationService, new TestConfigurationService(isBoolean(options.isSignatureVerificationEnabled) ? { extensions: { verifySignature: options.isSignatureVerificationEnabled } } : undefined)); instantiationService.stub(IExtensionSignatureVerificationService, new TestExtensionSignatureVerificationService(options.verificationResult, !!options.didExecute)); - return instantiationService.createInstance(ExtensionsDownloader); + return disposables.add(instantiationService.createInstance(ExtensionsDownloader)); } function aGalleryExtension(name: string, properties: Partial = {}, galleryExtensionProperties: any = {}, assets: Partial = {}): IGalleryExtension { diff --git a/src/vs/platform/files/common/fileService.ts b/src/vs/platform/files/common/fileService.ts index e88ff723b8b..b65c4e8705a 100644 --- a/src/vs/platform/files/common/fileService.ts +++ b/src/vs/platform/files/common/fileService.ts @@ -537,7 +537,7 @@ export class FileService extends Disposable implements IFileService { // validate read operation const statPromise = this.validateReadFile(resource, options).then(stat => stat, error => { - cancellableSource.cancel(); + cancellableSource.dispose(true); throw error; }); @@ -572,6 +572,9 @@ export class FileService extends Disposable implements IFileService { fileStream = this.readFileBuffered(provider, resource, cancellableSource.token, options); } + fileStream.on('end', () => cancellableSource.dispose()); + fileStream.on('error', () => cancellableSource.dispose()); + const fileStat = await statPromise; return { diff --git a/src/vs/platform/files/node/diskFileSystemProviderServer.ts b/src/vs/platform/files/node/diskFileSystemProviderServer.ts index b7e81ab4491..fefc836be54 100644 --- a/src/vs/platform/files/node/diskFileSystemProviderServer.ts +++ b/src/vs/platform/files/node/diskFileSystemProviderServer.ts @@ -324,11 +324,11 @@ export abstract class AbstractSessionFileWatcher extends Disposable implements I } override dispose(): void { - super.dispose(); - for (const [, disposable] of this.watcherRequests) { disposable.dispose(); } this.watcherRequests.clear(); + + super.dispose(); } } diff --git a/src/vs/platform/files/node/watcher/nodejs/nodejsClient.ts b/src/vs/platform/files/node/watcher/nodejs/nodejsClient.ts index 49a38f7c4dc..b57721ef43c 100644 --- a/src/vs/platform/files/node/watcher/nodejs/nodejsClient.ts +++ b/src/vs/platform/files/node/watcher/nodejs/nodejsClient.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { DisposableStore } from 'vs/base/common/lifecycle'; import { IDiskFileChange, ILogMessage, AbstractNonRecursiveWatcherClient, INonRecursiveWatcher } from 'vs/platform/files/common/watcher'; import { NodeJSWatcher } from 'vs/platform/files/node/watcher/nodejs/nodejsWatcher'; @@ -18,7 +19,7 @@ export class NodeJSWatcherClient extends AbstractNonRecursiveWatcherClient { this.init(); } - protected override createWatcher(): INonRecursiveWatcher { - return new NodeJSWatcher(); + protected override createWatcher(disposables: DisposableStore): INonRecursiveWatcher { + return disposables.add(new NodeJSWatcher()); } } diff --git a/src/vs/platform/files/node/watcher/watcherMain.ts b/src/vs/platform/files/node/watcher/watcherMain.ts index e3fd4ca9cde..09f952c4f4a 100644 --- a/src/vs/platform/files/node/watcher/watcherMain.ts +++ b/src/vs/platform/files/node/watcher/watcherMain.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { DisposableStore } from 'vs/base/common/lifecycle'; import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc'; import { Server as ChildProcessServer } from 'vs/base/parts/ipc/node/ipc.cp'; import { Server as UtilityProcessServer } from 'vs/base/parts/ipc/node/ipc.mp'; @@ -17,4 +18,4 @@ if (isUtilityProcess(process)) { } const service = new UniversalWatcher(); -server.registerChannel('watcher', ProxyChannel.fromService(service)); +server.registerChannel('watcher', ProxyChannel.fromService(service, new DisposableStore())); diff --git a/src/vs/platform/files/test/browser/fileService.test.ts b/src/vs/platform/files/test/browser/fileService.test.ts index 79b44bdb46a..5b32db14d64 100644 --- a/src/vs/platform/files/test/browser/fileService.test.ts +++ b/src/vs/platform/files/test/browser/fileService.test.ts @@ -6,9 +6,10 @@ import * as assert from 'assert'; import { DeferredPromise, timeout } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { consumeStream, newWriteableStream, ReadableStreamEvents } from 'vs/base/common/stream'; import { URI } from 'vs/base/common/uri'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IFileOpenOptions, IFileReadStreamOptions, FileSystemProviderCapabilities, FileType, IFileSystemProviderCapabilitiesChangeEvent, IFileSystemProviderRegistrationEvent, IStat } from 'vs/platform/files/common/files'; import { FileService } from 'vs/platform/files/common/fileService'; import { NullFileSystemProvider } from 'vs/platform/files/test/common/nullFileSystemProvider'; @@ -16,8 +17,14 @@ import { NullLogService } from 'vs/platform/log/common/log'; suite('File Service', () => { + const disposables = new DisposableStore(); + + teardown(() => { + disposables.clear(); + }); + test('provider registration', async () => { - const service = new FileService(new NullLogService()); + const service = disposables.add(new FileService(new NullLogService())); const resource = URI.parse('test://foo/bar'); const provider = new NullFileSystemProvider(); @@ -26,18 +33,18 @@ suite('File Service', () => { assert.strictEqual(service.getProvider(resource.scheme), undefined); const registrations: IFileSystemProviderRegistrationEvent[] = []; - service.onDidChangeFileSystemProviderRegistrations(e => { + disposables.add(service.onDidChangeFileSystemProviderRegistrations(e => { registrations.push(e); - }); + })); const capabilityChanges: IFileSystemProviderCapabilitiesChangeEvent[] = []; - service.onDidChangeFileSystemProviderCapabilities(e => { + disposables.add(service.onDidChangeFileSystemProviderCapabilities(e => { capabilityChanges.push(e); - }); + })); let registrationDisposable: IDisposable | undefined; let callCount = 0; - service.onWillActivateFileSystemProvider(e => { + disposables.add(service.onWillActivateFileSystemProvider(e => { callCount++; if (e.scheme === 'test' && callCount === 1) { @@ -47,7 +54,7 @@ suite('File Service', () => { resolve(); })); } - }); + })); assert.strictEqual(await service.canHandleResource(resource), true); assert.strictEqual(service.hasProvider(resource), true); @@ -79,19 +86,17 @@ suite('File Service', () => { assert.strictEqual(registrations.length, 2); assert.strictEqual(registrations[1].scheme, 'test'); assert.strictEqual(registrations[1].added, false); - - service.dispose(); }); test('watch', async () => { - const service = new FileService(new NullLogService()); + const service = disposables.add(new FileService(new NullLogService())); let disposeCounter = 0; - service.registerProvider('test', new NullFileSystemProvider(() => { + disposables.add(service.registerProvider('test', new NullFileSystemProvider(() => { return toDisposable(() => { disposeCounter++; }); - })); + }))); await service.activateProvider('test'); const resource1 = URI.parse('test://foo/bar1'); @@ -144,7 +149,7 @@ suite('File Service', () => { }); async function testReadErrorBubbles(async: boolean) { - const service = new FileService(new NullLogService()); + const service = disposables.add(new FileService(new NullLogService())); const provider = new class extends NullFileSystemProvider { override async stat(resource: URI): Promise { @@ -158,7 +163,7 @@ suite('File Service', () => { override readFile(resource: URI): Promise { if (async) { - return timeout(5).then(() => { throw new Error('failed'); }); + return timeout(5, CancellationToken.None).then(() => { throw new Error('failed'); }); } throw new Error('failed'); @@ -166,7 +171,7 @@ suite('File Service', () => { override open(resource: URI, opts: IFileOpenOptions): Promise { if (async) { - return timeout(5).then(() => { throw new Error('failed'); }); + return timeout(5, CancellationToken.None).then(() => { throw new Error('failed'); }); } throw new Error('failed'); @@ -175,7 +180,7 @@ suite('File Service', () => { readFileStream(resource: URI, opts: IFileReadStreamOptions, token: CancellationToken): ReadableStreamEvents { if (async) { const stream = newWriteableStream(chunk => chunk[0]); - timeout(5).then(() => stream.error(new Error('failed'))); + timeout(5, CancellationToken.None).then(() => stream.error(new Error('failed'))); return stream; @@ -185,7 +190,7 @@ suite('File Service', () => { } }; - const disposable = service.registerProvider('test', provider); + disposables.add(service.registerProvider('test', provider)); for (const capabilities of [FileSystemProviderCapabilities.FileReadWrite, FileSystemProviderCapabilities.FileReadStream, FileSystemProviderCapabilities.FileOpenReadWriteClose]) { provider.setCapabilities(capabilities); @@ -209,12 +214,10 @@ suite('File Service', () => { assert.ok(e2); } - - disposable.dispose(); } test('readFile/readFileStream supports cancellation (https://github.com/microsoft/vscode/issues/138805)', async () => { - const service = new FileService(new NullLogService()); + const service = disposables.add(new FileService(new NullLogService())); let readFileStreamReady: DeferredPromise | undefined = undefined; @@ -231,10 +234,10 @@ suite('File Service', () => { readFileStream(resource: URI, opts: IFileReadStreamOptions, token: CancellationToken): ReadableStreamEvents { const stream = newWriteableStream(chunk => chunk[0]); - token.onCancellationRequested(() => { + disposables.add(token.onCancellationRequested(() => { stream.error(new Error('Expected cancellation')); stream.end(); - }); + })); readFileStreamReady!.complete(); @@ -272,4 +275,6 @@ suite('File Service', () => { disposable.dispose(); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/platform/files/test/common/files.test.ts b/src/vs/platform/files/test/common/files.test.ts index 8927ad945b9..2a6ae1a5a3a 100644 --- a/src/vs/platform/files/test/common/files.test.ts +++ b/src/vs/platform/files/test/common/files.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { isEqual, isEqualOrParent } from 'vs/base/common/extpath'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; -import { toResource } from 'vs/base/test/common/utils'; +import { ensureNoDisposablesAreLeakedInTestSuite, toResource } from 'vs/base/test/common/utils'; import { FileChangesEvent, FileChangeType, isParent } from 'vs/platform/files/common/files'; suite('Files', () => { @@ -249,4 +249,6 @@ suite('Files', () => { assert(!isEqualOrParent('foo/bar/test.ts', 'foo/BAR/test.', true)); } }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/platform/files/test/common/watcher.test.ts b/src/vs/platform/files/test/common/watcher.test.ts index 9c20f7693d4..9e964d9dfe8 100644 --- a/src/vs/platform/files/test/common/watcher.test.ts +++ b/src/vs/platform/files/test/common/watcher.test.ts @@ -5,17 +5,21 @@ import * as assert from 'assert'; import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { isLinux, isWindows } from 'vs/base/common/platform'; import { isEqual } from 'vs/base/common/resources'; import { URI as uri } from 'vs/base/common/uri'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { FileChangesEvent, FileChangeType, IFileChange } from 'vs/platform/files/common/files'; import { IDiskFileChange, coalesceEvents, toFileChanges, parseWatcherPatterns } from 'vs/platform/files/common/watcher'; -class TestFileWatcher { +class TestFileWatcher extends Disposable { private readonly _onDidFilesChange: Emitter<{ raw: IFileChange[]; event: FileChangesEvent }>; constructor() { - this._onDidFilesChange = new Emitter<{ raw: IFileChange[]; event: FileChangesEvent }>(); + super(); + + this._onDidFilesChange = this._register(new Emitter<{ raw: IFileChange[]; event: FileChangesEvent }>()); } get onDidFilesChange(): Event<{ raw: IFileChange[]; event: FileChangesEvent }> { @@ -103,12 +107,20 @@ suite('Watcher', () => { assert.strictEqual(parsedPattern('c:\\users\\data\\src\\foo.ts'), false); assert.strictEqual(parsedPattern('c:\\users\\data\\src\\bar\\foo.js'), true); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); suite('Watcher Events Normalizer', () => { + const disposables = new DisposableStore(); + + teardown(() => { + disposables.clear(); + }); + test('simple add/update/delete', done => { - const watch = new TestFileWatcher(); + const watch = disposables.add(new TestFileWatcher()); const added = uri.file('/users/data/src/added.txt'); const updated = uri.file('/users/data/src/updated.txt'); @@ -120,7 +132,7 @@ suite('Watcher Events Normalizer', () => { { path: deleted.fsPath, type: FileChangeType.DELETED }, ]; - watch.onDidFilesChange(({ event, raw }) => { + disposables.add(watch.onDidFilesChange(({ event, raw }) => { assert.ok(event); assert.strictEqual(raw.length, 3); assert.ok(event.contains(added, FileChangeType.ADDED)); @@ -128,14 +140,14 @@ suite('Watcher Events Normalizer', () => { assert.ok(event.contains(deleted, FileChangeType.DELETED)); done(); - }); + })); watch.report(raw); }); (isWindows ? [Path.WINDOWS, Path.UNC] : [Path.UNIX]).forEach(path => { test(`delete only reported for top level folder (${path})`, done => { - const watch = new TestFileWatcher(); + const watch = disposables.add(new TestFileWatcher()); const deletedFolderA = uri.file(path === Path.UNIX ? '/users/data/src/todelete1' : path === Path.WINDOWS ? 'C:\\users\\data\\src\\todelete1' : '\\\\localhost\\users\\data\\src\\todelete1'); const deletedFolderB = uri.file(path === Path.UNIX ? '/users/data/src/todelete2' : path === Path.WINDOWS ? 'C:\\users\\data\\src\\todelete2' : '\\\\localhost\\users\\data\\src\\todelete2'); @@ -158,7 +170,7 @@ suite('Watcher Events Normalizer', () => { { path: updatedFile.fsPath, type: FileChangeType.UPDATED } ]; - watch.onDidFilesChange(({ event, raw }) => { + disposables.add(watch.onDidFilesChange(({ event, raw }) => { assert.ok(event); assert.strictEqual(raw.length, 5); @@ -169,14 +181,14 @@ suite('Watcher Events Normalizer', () => { assert.ok(event.contains(updatedFile, FileChangeType.UPDATED)); done(); - }); + })); watch.report(raw); }); }); test('event coalescer: ignore CREATE followed by DELETE', done => { - const watch = new TestFileWatcher(); + const watch = disposables.add(new TestFileWatcher()); const created = uri.file('/users/data/src/related'); const deleted = uri.file('/users/data/src/related'); @@ -188,20 +200,20 @@ suite('Watcher Events Normalizer', () => { { path: unrelated.fsPath, type: FileChangeType.UPDATED }, ]; - watch.onDidFilesChange(({ event, raw }) => { + disposables.add(watch.onDidFilesChange(({ event, raw }) => { assert.ok(event); assert.strictEqual(raw.length, 1); assert.ok(event.contains(unrelated, FileChangeType.UPDATED)); done(); - }); + })); watch.report(raw); }); test('event coalescer: flatten DELETE followed by CREATE into CHANGE', done => { - const watch = new TestFileWatcher(); + const watch = disposables.add(new TestFileWatcher()); const deleted = uri.file('/users/data/src/related'); const created = uri.file('/users/data/src/related'); @@ -213,7 +225,7 @@ suite('Watcher Events Normalizer', () => { { path: unrelated.fsPath, type: FileChangeType.UPDATED }, ]; - watch.onDidFilesChange(({ event, raw }) => { + disposables.add(watch.onDidFilesChange(({ event, raw }) => { assert.ok(event); assert.strictEqual(raw.length, 2); @@ -221,13 +233,13 @@ suite('Watcher Events Normalizer', () => { assert.ok(event.contains(unrelated, FileChangeType.UPDATED)); done(); - }); + })); watch.report(raw); }); test('event coalescer: ignore UPDATE when CREATE received', done => { - const watch = new TestFileWatcher(); + const watch = disposables.add(new TestFileWatcher()); const created = uri.file('/users/data/src/related'); const updated = uri.file('/users/data/src/related'); @@ -239,7 +251,7 @@ suite('Watcher Events Normalizer', () => { { path: unrelated.fsPath, type: FileChangeType.UPDATED }, ]; - watch.onDidFilesChange(({ event, raw }) => { + disposables.add(watch.onDidFilesChange(({ event, raw }) => { assert.ok(event); assert.strictEqual(raw.length, 2); @@ -248,13 +260,13 @@ suite('Watcher Events Normalizer', () => { assert.ok(event.contains(unrelated, FileChangeType.UPDATED)); done(); - }); + })); watch.report(raw); }); test('event coalescer: apply DELETE', done => { - const watch = new TestFileWatcher(); + const watch = disposables.add(new TestFileWatcher()); const updated = uri.file('/users/data/src/related'); const updated2 = uri.file('/users/data/src/related'); @@ -268,7 +280,7 @@ suite('Watcher Events Normalizer', () => { { path: updated.fsPath, type: FileChangeType.DELETED } ]; - watch.onDidFilesChange(({ event, raw }) => { + disposables.add(watch.onDidFilesChange(({ event, raw }) => { assert.ok(event); assert.strictEqual(raw.length, 2); @@ -277,13 +289,13 @@ suite('Watcher Events Normalizer', () => { assert.ok(event.contains(unrelated, FileChangeType.UPDATED)); done(); - }); + })); watch.report(raw); }); test('event coalescer: track case renames', done => { - const watch = new TestFileWatcher(); + const watch = disposables.add(new TestFileWatcher()); const oldPath = uri.file('/users/data/src/added'); const newPath = uri.file('/users/data/src/ADDED'); @@ -293,7 +305,7 @@ suite('Watcher Events Normalizer', () => { { path: oldPath.fsPath, type: FileChangeType.DELETED } ]; - watch.onDidFilesChange(({ event, raw }) => { + disposables.add(watch.onDidFilesChange(({ event, raw }) => { assert.ok(event); assert.strictEqual(raw.length, 2); @@ -308,8 +320,10 @@ suite('Watcher Events Normalizer', () => { } done(); - }); + })); watch.report(raw); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index 6b698bc17f6..75b13f0a8fc 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -1469,7 +1469,7 @@ configurationRegistry.registerConfiguration({ type: 'string', enum: ['automatic', 'trigger'], default: 'automatic', - description: localize('typeNavigationMode', "Controls the how type navigation works in lists and trees in the workbench. When set to 'trigger', type navigation begins once the 'list.triggerTypeNavigation' command is run."), + markdownDescription: localize('typeNavigationMode2', "Controls how type navigation works in lists and trees in the workbench. When set to `trigger`, type navigation begins once the `list.triggerTypeNavigation` command is run."), } } }); diff --git a/src/vs/platform/state/node/stateService.ts b/src/vs/platform/state/node/stateService.ts index ebe9318124a..debf184e7d4 100644 --- a/src/vs/platform/state/node/stateService.ts +++ b/src/vs/platform/state/node/stateService.ts @@ -5,6 +5,7 @@ import { ThrottledDelayer } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; +import { Disposable } from 'vs/base/common/lifecycle'; import { isUndefined, isUndefinedOrNull } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -19,7 +20,7 @@ export const enum SaveStrategy { DELAYED } -export class FileStorage { +export class FileStorage extends Disposable { private storage: StorageDatabase = Object.create(null); private lastSavedStorageContents = ''; @@ -35,7 +36,9 @@ export class FileStorage { private readonly logService: ILogService, private readonly fileService: IFileService, ) { - this.flushDelayer = saveStrategy === SaveStrategy.IMMEDIATE ? undefined : new ThrottledDelayer(100 /* buffer saves over a short time */); + super(); + + this.flushDelayer = saveStrategy === SaveStrategy.IMMEDIATE ? undefined : this._register(new ThrottledDelayer(100 /* buffer saves over a short time */)); } init(): Promise { @@ -157,7 +160,7 @@ export class FileStorage { } } -export class StateReadonlyService implements IStateReadService { +export class StateReadonlyService extends Disposable implements IStateReadService { declare readonly _serviceBrand: undefined; @@ -169,7 +172,9 @@ export class StateReadonlyService implements IStateReadService { @ILogService logService: ILogService, @IFileService fileService: IFileService ) { - this.fileStorage = new FileStorage(environmentService.stateResource, saveStrategy, logService, fileService); + super(); + + this.fileStorage = this._register(new FileStorage(environmentService.stateResource, saveStrategy, logService, fileService)); } async init(): Promise { diff --git a/src/vs/platform/state/test/node/state.test.ts b/src/vs/platform/state/test/node/state.test.ts index af5d0781dd6..493d78d0e51 100644 --- a/src/vs/platform/state/test/node/state.test.ts +++ b/src/vs/platform/state/test/node/state.test.ts @@ -6,10 +6,12 @@ import * as assert from 'assert'; import { readFileSync } from 'fs'; import { tmpdir } from 'os'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { join } from 'vs/base/common/path'; import { URI } from 'vs/base/common/uri'; import { Promises, writeFileSync } from 'vs/base/node/pfs'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; import { IFileService } from 'vs/platform/files/common/files'; import { FileService } from 'vs/platform/files/common/fileService'; @@ -24,21 +26,22 @@ flakySuite('StateService', () => { let logService: ILogService; let diskFileSystemProvider: DiskFileSystemProvider; + const disposables = new DisposableStore(); + setup(() => { testDir = getRandomTestPath(tmpdir(), 'vsctests', 'statemainservice'); logService = new NullLogService(); - fileService = new FileService(logService); - diskFileSystemProvider = new DiskFileSystemProvider(logService); - fileService.registerProvider(Schemas.file, diskFileSystemProvider); + fileService = disposables.add(new FileService(logService)); + diskFileSystemProvider = disposables.add(new DiskFileSystemProvider(logService)); + disposables.add(fileService.registerProvider(Schemas.file, diskFileSystemProvider)); return Promises.mkdir(testDir, { recursive: true }); }); teardown(() => { - fileService.dispose(); - diskFileSystemProvider.dispose(); + disposables.clear(); return Promises.rm(testDir); }); @@ -47,7 +50,7 @@ flakySuite('StateService', () => { const storageFile = join(testDir, 'storage.json'); writeFileSync(storageFile, ''); - let service = new FileStorage(URI.file(storageFile), SaveStrategy.DELAYED, logService, fileService); + let service = disposables.add(new FileStorage(URI.file(storageFile), SaveStrategy.DELAYED, logService, fileService)); await service.init(); service.setItem('some.key', 'some.value'); @@ -62,7 +65,7 @@ flakySuite('StateService', () => { await service.close(); - service = new FileStorage(URI.file(storageFile), SaveStrategy.DELAYED, logService, fileService); + service = disposables.add(new FileStorage(URI.file(storageFile), SaveStrategy.DELAYED, logService, fileService)); await service.init(); assert.strictEqual(service.getItem('some.other.key'), 'some.other.value'); @@ -103,13 +106,15 @@ flakySuite('StateService', () => { assert.strictEqual(service.getItem('some.setItems.key3'), undefined); assert.strictEqual(service.getItem('some.setItems.key4'), undefined); assert.strictEqual(service.getItem('some.setItems.key5'), undefined); + + return service.close(); }); test('Basics (immediate strategy)', async function () { const storageFile = join(testDir, 'storage.json'); writeFileSync(storageFile, ''); - let service = new FileStorage(URI.file(storageFile), SaveStrategy.IMMEDIATE, logService, fileService); + let service = disposables.add(new FileStorage(URI.file(storageFile), SaveStrategy.IMMEDIATE, logService, fileService)); await service.init(); service.setItem('some.key', 'some.value'); @@ -124,7 +129,7 @@ flakySuite('StateService', () => { await service.close(); - service = new FileStorage(URI.file(storageFile), SaveStrategy.IMMEDIATE, logService, fileService); + service = disposables.add(new FileStorage(URI.file(storageFile), SaveStrategy.IMMEDIATE, logService, fileService)); await service.init(); assert.strictEqual(service.getItem('some.other.key'), 'some.other.value'); @@ -165,13 +170,15 @@ flakySuite('StateService', () => { assert.strictEqual(service.getItem('some.setItems.key3'), undefined); assert.strictEqual(service.getItem('some.setItems.key4'), undefined); assert.strictEqual(service.getItem('some.setItems.key5'), undefined); + + return service.close(); }); test('Multiple ops are buffered and applied', async function () { const storageFile = join(testDir, 'storage.json'); writeFileSync(storageFile, ''); - let service = new FileStorage(URI.file(storageFile), SaveStrategy.DELAYED, logService, fileService); + let service = disposables.add(new FileStorage(URI.file(storageFile), SaveStrategy.DELAYED, logService, fileService)); await service.init(); service.setItem('some.key1', 'some.value1'); @@ -187,20 +194,22 @@ flakySuite('StateService', () => { await service.close(); - service = new FileStorage(URI.file(storageFile), SaveStrategy.DELAYED, logService, fileService); + service = disposables.add(new FileStorage(URI.file(storageFile), SaveStrategy.DELAYED, logService, fileService)); await service.init(); assert.strictEqual(service.getItem('some.key1'), 'some.value1'); assert.strictEqual(service.getItem('some.key2'), 'some.value2'); assert.strictEqual(service.getItem('some.key3'), 'some.value3'); assert.strictEqual(service.getItem('some.key4'), undefined); + + return service.close(); }); test('Multiple ops (Immediate Strategy)', async function () { const storageFile = join(testDir, 'storage.json'); writeFileSync(storageFile, ''); - let service = new FileStorage(URI.file(storageFile), SaveStrategy.IMMEDIATE, logService, fileService); + let service = disposables.add(new FileStorage(URI.file(storageFile), SaveStrategy.IMMEDIATE, logService, fileService)); await service.init(); service.setItem('some.key1', 'some.value1'); @@ -216,20 +225,22 @@ flakySuite('StateService', () => { await service.close(); - service = new FileStorage(URI.file(storageFile), SaveStrategy.IMMEDIATE, logService, fileService); + service = disposables.add(new FileStorage(URI.file(storageFile), SaveStrategy.IMMEDIATE, logService, fileService)); await service.init(); assert.strictEqual(service.getItem('some.key1'), 'some.value1'); assert.strictEqual(service.getItem('some.key2'), 'some.value2'); assert.strictEqual(service.getItem('some.key3'), 'some.value3'); assert.strictEqual(service.getItem('some.key4'), undefined); + + return service.close(); }); test('Used before init', async function () { const storageFile = join(testDir, 'storage.json'); writeFileSync(storageFile, ''); - const service = new FileStorage(URI.file(storageFile), SaveStrategy.DELAYED, logService, fileService); + const service = disposables.add(new FileStorage(URI.file(storageFile), SaveStrategy.DELAYED, logService, fileService)); service.setItem('some.key1', 'some.value1'); service.setItem('some.key2', 'some.value2'); @@ -248,13 +259,15 @@ flakySuite('StateService', () => { assert.strictEqual(service.getItem('some.key2'), 'some.value2'); assert.strictEqual(service.getItem('some.key3'), 'some.value3'); assert.strictEqual(service.getItem('some.key4'), undefined); + + return service.close(); }); test('Used after close', async function () { const storageFile = join(testDir, 'storage.json'); writeFileSync(storageFile, ''); - const service = new FileStorage(URI.file(storageFile), SaveStrategy.DELAYED, logService, fileService); + const service = disposables.add(new FileStorage(URI.file(storageFile), SaveStrategy.DELAYED, logService, fileService)); await service.init(); @@ -271,14 +284,14 @@ flakySuite('StateService', () => { assert.ok(contents.includes('some.value1')); assert.ok(!contents.includes('some.marker')); - await service.close(); + return service.close(); }); test('Closed before init', async function () { const storageFile = join(testDir, 'storage.json'); writeFileSync(storageFile, ''); - const service = new FileStorage(URI.file(storageFile), SaveStrategy.DELAYED, logService, fileService); + const service = disposables.add(new FileStorage(URI.file(storageFile), SaveStrategy.DELAYED, logService, fileService)); service.setItem('some.key1', 'some.value1'); service.setItem('some.key2', 'some.value2'); @@ -290,4 +303,6 @@ flakySuite('StateService', () => { const contents = readFileSync(storageFile).toString(); assert.strictEqual(contents.length, 0); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/platform/storage/common/storage.ts b/src/vs/platform/storage/common/storage.ts index 2af9ffd0281..174a3038aaa 100644 --- a/src/vs/platform/storage/common/storage.ts +++ b/src/vs/platform/storage/common/storage.ts @@ -748,6 +748,10 @@ export class InMemoryStorageService extends AbstractStorageService { // no-op when in-memory } + protected override shouldFlushWhenIdle(): boolean { + return false; + } + hasScope(scope: IAnyWorkspaceIdentifier | IUserDataProfile): boolean { return false; } diff --git a/src/vs/platform/storage/electron-main/storageMain.ts b/src/vs/platform/storage/electron-main/storageMain.ts index 6843793692f..c110f28015b 100644 --- a/src/vs/platform/storage/electron-main/storageMain.ts +++ b/src/vs/platform/storage/electron-main/storageMain.ts @@ -122,7 +122,7 @@ abstract class BaseStorageMain extends Disposable implements IStorageMain { private readonly _onDidCloseStorage = this._register(new Emitter()); readonly onDidCloseStorage = this._onDidCloseStorage.event; - private _storage = new Storage(new InMemoryStorageDatabase(), { hint: StorageHint.STORAGE_IN_MEMORY }); // storage is in-memory until initialized + private _storage = this._register(new Storage(new InMemoryStorageDatabase(), { hint: StorageHint.STORAGE_IN_MEMORY })); // storage is in-memory until initialized get storage(): IStorage { return this._storage; } abstract get path(): string | undefined; @@ -155,7 +155,7 @@ abstract class BaseStorageMain extends Disposable implements IStorageMain { try { // Create storage via subclasses - const storage = await this.doCreate(); + const storage = this._register(await this.doCreate()); // Replace our in-memory storage with the real // once as soon as possible without awaiting diff --git a/src/vs/platform/storage/electron-main/storageMainService.ts b/src/vs/platform/storage/electron-main/storageMainService.ts index bdfad4eacb6..4d5c2eb209a 100644 --- a/src/vs/platform/storage/electron-main/storageMainService.ts +++ b/src/vs/platform/storage/electron-main/storageMainService.ts @@ -164,16 +164,16 @@ export class StorageMainService extends Disposable implements IStorageMainServic //#region Application Storage - readonly applicationStorage = this.createApplicationStorage(); + readonly applicationStorage = this._register(this.createApplicationStorage()); private createApplicationStorage(): IStorageMain { this.logService.trace(`StorageMainService: creating application storage`); const applicationStorage = new ApplicationStorageMain(this.getStorageOptions(), this.userDataProfilesService, this.logService, this.fileService); - once(applicationStorage.onDidCloseStorage)(() => { + this._register(once(applicationStorage.onDidCloseStorage)(() => { this.logService.trace(`StorageMainService: closed application storage`); - }); + })); return applicationStorage; } @@ -193,7 +193,7 @@ export class StorageMainService extends Disposable implements IStorageMainServic if (!profileStorage) { this.logService.trace(`StorageMainService: creating profile storage (${profile.name})`); - profileStorage = this.createProfileStorage(profile); + profileStorage = this._register(this.createProfileStorage(profile)); this.mapProfileToStorage.set(profile.id, profileStorage); const listener = this._register(profileStorage.onDidChangeStorage(e => this._onDidChangeProfileStorage.fire({ @@ -202,12 +202,12 @@ export class StorageMainService extends Disposable implements IStorageMainServic profile }))); - once(profileStorage.onDidCloseStorage)(() => { + this._register(once(profileStorage.onDidCloseStorage)(() => { this.logService.trace(`StorageMainService: closed profile storage (${profile.name})`); this.mapProfileToStorage.delete(profile.id); listener.dispose(); - }); + })); } return profileStorage; @@ -238,14 +238,14 @@ export class StorageMainService extends Disposable implements IStorageMainServic if (!workspaceStorage) { this.logService.trace(`StorageMainService: creating workspace storage (${workspace.id})`); - workspaceStorage = this.createWorkspaceStorage(workspace); + workspaceStorage = this._register(this.createWorkspaceStorage(workspace)); this.mapWorkspaceToStorage.set(workspace.id, workspaceStorage); - once(workspaceStorage.onDidCloseStorage)(() => { + this._register(once(workspaceStorage.onDidCloseStorage)(() => { this.logService.trace(`StorageMainService: closed workspace storage (${workspace.id})`); this.mapWorkspaceToStorage.delete(workspace.id); - }); + })); } return workspaceStorage; diff --git a/src/vs/platform/storage/test/common/storageService.test.ts b/src/vs/platform/storage/test/common/storageService.test.ts index 4ace3be8706..d781cf027e6 100644 --- a/src/vs/platform/storage/test/common/storageService.test.ts +++ b/src/vs/platform/storage/test/common/storageService.test.ts @@ -5,17 +5,21 @@ import { deepStrictEqual, ok, strictEqual } from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { InMemoryStorageService, IStorageService, IStorageTargetChangeEvent, IStorageValueChangeEvent, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; export function createSuite(params: { setup: () => Promise; teardown: (service: T) => Promise }): void { let storageService: T; + const disposables = new DisposableStore(); + setup(async () => { storageService = await params.setup(); }); teardown(() => { + disposables.clear(); return params.teardown(storageService); }); @@ -33,7 +37,7 @@ export function createSuite(params: { setup: () => Pr test('Storage change source', () => { const storageValueChangeEvents: IStorageValueChangeEvent[] = []; - storageService.onDidChangeValue(StorageScope.WORKSPACE, undefined, new DisposableStore())(e => storageValueChangeEvents.push(e)); + storageService.onDidChangeValue(StorageScope.WORKSPACE, undefined, disposables)(e => storageValueChangeEvents.push(e), undefined, disposables); // Explicit external source storageService.storeAll([{ key: 'testExternalChange', value: 'foobar', scope: StorageScope.WORKSPACE, target: StorageTarget.MACHINE }], true); @@ -52,7 +56,7 @@ export function createSuite(params: { setup: () => Pr test('Storage change event scope (all keys)', () => { const storageValueChangeEvents: IStorageValueChangeEvent[] = []; - storageService.onDidChangeValue(StorageScope.WORKSPACE, undefined, new DisposableStore())(e => storageValueChangeEvents.push(e)); + storageService.onDidChangeValue(StorageScope.WORKSPACE, undefined, disposables)(e => storageValueChangeEvents.push(e), undefined, disposables); storageService.store('testChange', 'foobar', StorageScope.WORKSPACE, StorageTarget.MACHINE); storageService.store('testChange2', 'foobar', StorageScope.WORKSPACE, StorageTarget.MACHINE); @@ -64,7 +68,7 @@ export function createSuite(params: { setup: () => Pr test('Storage change event scope (specific key)', () => { const storageValueChangeEvents: IStorageValueChangeEvent[] = []; - storageService.onDidChangeValue(StorageScope.WORKSPACE, 'testChange', new DisposableStore())(e => storageValueChangeEvents.push(e)); + storageService.onDidChangeValue(StorageScope.WORKSPACE, 'testChange', disposables)(e => storageValueChangeEvents.push(e), undefined, disposables); storageService.store('testChange', 'foobar', StorageScope.WORKSPACE, StorageTarget.MACHINE); storageService.store('testChange', 'foobar', StorageScope.PROFILE, StorageTarget.USER); @@ -77,7 +81,7 @@ export function createSuite(params: { setup: () => Pr function storeData(scope: StorageScope): void { let storageValueChangeEvents: IStorageValueChangeEvent[] = []; - storageService.onDidChangeValue(scope, undefined, new DisposableStore())(e => storageValueChangeEvents.push(e)); + storageService.onDidChangeValue(scope, undefined, disposables)(e => storageValueChangeEvents.push(e), undefined, disposables); strictEqual(storageService.get('test.get', scope, 'foobar'), 'foobar'); strictEqual(storageService.get('test.get', scope, ''), ''); @@ -153,7 +157,7 @@ export function createSuite(params: { setup: () => Pr function removeData(scope: StorageScope): void { const storageValueChangeEvents: IStorageValueChangeEvent[] = []; - storageService.onDidChangeValue(scope, undefined, new DisposableStore())(e => storageValueChangeEvents.push(e)); + storageService.onDidChangeValue(scope, undefined, disposables)(e => storageValueChangeEvents.push(e), undefined, disposables); storageService.store('test.remove', 'foobar', scope, StorageTarget.MACHINE); strictEqual('foobar', storageService.get('test.remove', scope, (undefined)!)); @@ -167,7 +171,7 @@ export function createSuite(params: { setup: () => Pr test('Keys (in-memory)', () => { let storageTargetEvent: IStorageTargetChangeEvent | undefined = undefined; - storageService.onDidChangeTarget(e => storageTargetEvent = e); + storageService.onDidChangeTarget(e => storageTargetEvent = e, undefined, disposables); // Empty for (const scope of [StorageScope.WORKSPACE, StorageScope.PROFILE, StorageScope.APPLICATION]) { @@ -180,7 +184,7 @@ export function createSuite(params: { setup: () => Pr // Add values for (const scope of [StorageScope.WORKSPACE, StorageScope.PROFILE, StorageScope.APPLICATION]) { - storageService.onDidChangeValue(scope, undefined, new DisposableStore())(e => storageValueChangeEvent = e); + storageService.onDidChangeValue(scope, undefined, disposables)(e => storageValueChangeEvent = e, undefined, disposables); for (const target of [StorageTarget.MACHINE, StorageTarget.USER]) { storageTargetEvent = Object.create(null); @@ -281,8 +285,17 @@ export function createSuite(params: { setup: () => Pr } suite('StorageService (in-memory)', function () { + + const disposables = new DisposableStore(); + + teardown(() => { + disposables.clear(); + }); + createSuite({ - setup: async () => new InMemoryStorageService(), + setup: async () => disposables.add(new InMemoryStorageService()), teardown: async () => { } }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/platform/storage/test/electron-main/storageMainService.test.ts b/src/vs/platform/storage/test/electron-main/storageMainService.test.ts index 3ca4ea12ffb..1455f234f7b 100644 --- a/src/vs/platform/storage/test/electron-main/storageMainService.test.ts +++ b/src/vs/platform/storage/test/electron-main/storageMainService.test.ts @@ -24,9 +24,13 @@ import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentitySe import { IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; import { UserDataProfilesMainService } from 'vs/platform/userDataProfile/electron-main/userDataProfile'; import { TestLifecycleMainService } from 'vs/platform/test/electron-main/workbenchTestServices'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { DisposableStore } from 'vs/base/common/lifecycle'; suite('StorageMainService', function () { + const disposables = new DisposableStore(); + const productService: IProductService = { _serviceBrand: undefined, ...product }; const inMemoryProfileRoot = URI.file('/location').with({ scheme: Schemas.inMemory }); @@ -68,12 +72,12 @@ suite('StorageMainService', function () { } let storageChangeEvent: IStorageChangeEvent | undefined = undefined; - const storageChangeListener = storage.onDidChangeStorage(e => { + disposables.add(storage.onDidChangeStorage(e => { storageChangeEvent = e; - }); + })); let storageDidClose = false; - const storageCloseListener = storage.onDidCloseStorage(() => storageDidClose = true); + disposables.add(storage.onDidCloseStorage(() => storageDidClose = true)); // Basic store/get/remove const size = storage.items.size; @@ -101,15 +105,21 @@ suite('StorageMainService', function () { await storage.close(); strictEqual(storageDidClose, true); - - storageChangeListener.dispose(); - storageCloseListener.dispose(); } + teardown(() => { + disposables.clear(); + }); + function createStorageService(lifecycleMainService: ILifecycleMainService = new TestLifecycleMainService()): TestStorageMainService { const environmentService = new NativeEnvironmentService(parseArgs(process.argv, OPTIONS), productService); - const fileService = new FileService(new NullLogService()); - return new TestStorageMainService(new NullLogService(), environmentService, new UserDataProfilesMainService(new StateService(SaveStrategy.DELAYED, environmentService, new NullLogService(), fileService), new UriIdentityService(fileService), environmentService, fileService, new NullLogService()), lifecycleMainService, fileService, new UriIdentityService(fileService)); + const fileService = disposables.add(new FileService(new NullLogService())); + const uriIdentityService = disposables.add(new UriIdentityService(fileService)); + const testStorageService = disposables.add(new TestStorageMainService(new NullLogService(), environmentService, disposables.add(new UserDataProfilesMainService(disposables.add(new StateService(SaveStrategy.DELAYED, environmentService, new NullLogService(), fileService)), disposables.add(uriIdentityService), environmentService, fileService, new NullLogService())), lifecycleMainService, fileService, uriIdentityService)); + + disposables.add(testStorageService.applicationStorage); + + return testStorageService; } test('basics (application)', function () { @@ -141,21 +151,21 @@ suite('StorageMainService', function () { const workspaceStorage = storageMainService.workspaceStorage(workspace); let didCloseWorkspaceStorage = false; - workspaceStorage.onDidCloseStorage(() => { + disposables.add(workspaceStorage.onDidCloseStorage(() => { didCloseWorkspaceStorage = true; - }); + })); const profileStorage = storageMainService.profileStorage(profile); let didCloseProfileStorage = false; - profileStorage.onDidCloseStorage(() => { + disposables.add(profileStorage.onDidCloseStorage(() => { didCloseProfileStorage = true; - }); + })); const applicationStorage = storageMainService.applicationStorage; let didCloseApplicationStorage = false; - applicationStorage.onDidCloseStorage(() => { + disposables.add(applicationStorage.onDidCloseStorage(() => { didCloseApplicationStorage = true; - }); + })); strictEqual(applicationStorage, storageMainService.applicationStorage); // same instance as long as not closed strictEqual(profileStorage, storageMainService.profileStorage(profile)); // same instance as long as not closed @@ -177,7 +187,7 @@ suite('StorageMainService', function () { const workspaceStorage2 = storageMainService.workspaceStorage(workspace); notStrictEqual(workspaceStorage, workspaceStorage2); - return workspaceStorage2.close(); + await workspaceStorage2.close(); }); test('storage closed before init works', async function () { @@ -187,21 +197,21 @@ suite('StorageMainService', function () { const workspaceStorage = storageMainService.workspaceStorage(workspace); let didCloseWorkspaceStorage = false; - workspaceStorage.onDidCloseStorage(() => { + disposables.add(workspaceStorage.onDidCloseStorage(() => { didCloseWorkspaceStorage = true; - }); + })); const profileStorage = storageMainService.profileStorage(profile); let didCloseProfileStorage = false; - profileStorage.onDidCloseStorage(() => { + disposables.add(profileStorage.onDidCloseStorage(() => { didCloseProfileStorage = true; - }); + })); const applicationStorage = storageMainService.applicationStorage; let didCloseApplicationStorage = false; - applicationStorage.onDidCloseStorage(() => { + disposables.add(applicationStorage.onDidCloseStorage(() => { didCloseApplicationStorage = true; - }); + })); await applicationStorage.close(); await profileStorage.close(); @@ -219,21 +229,21 @@ suite('StorageMainService', function () { const workspaceStorage = storageMainService.workspaceStorage(workspace); let didCloseWorkspaceStorage = false; - workspaceStorage.onDidCloseStorage(() => { + disposables.add(workspaceStorage.onDidCloseStorage(() => { didCloseWorkspaceStorage = true; - }); + })); const profileStorage = storageMainService.profileStorage(profile); let didCloseProfileStorage = false; - profileStorage.onDidCloseStorage(() => { + disposables.add(profileStorage.onDidCloseStorage(() => { didCloseProfileStorage = true; - }); + })); const applicationtorage = storageMainService.applicationStorage; let didCloseApplicationStorage = false; - applicationtorage.onDidCloseStorage(() => { + disposables.add(applicationtorage.onDidCloseStorage(() => { didCloseApplicationStorage = true; - }); + })); applicationtorage.init(); profileStorage.init(); @@ -247,4 +257,6 @@ suite('StorageMainService', function () { strictEqual(didCloseProfileStorage, true); strictEqual(didCloseWorkspaceStorage, true); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts index f3c2519be9e..1105d761a0e 100644 --- a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts +++ b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts @@ -164,7 +164,7 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe // For a Windows backend we cannot listen to CSI J, instead we assume running clear or // cls will clear all commands in the viewport. This is not perfect but it's right most // of the time. - this.onBeforeCommandFinished(command => { + this._register(this.onBeforeCommandFinished(command => { if (this._isWindowsPty) { if (command.command.trim().toLowerCase() === 'clear' || command.command.trim().toLowerCase() === 'cls') { this._clearCommandsInViewport(); @@ -172,7 +172,7 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe this._onCurrentCommandInvalidated.fire({ reason: CommandInvalidationReason.Windows }); } } - }); + })); // For non-Windows backends we can just listen to CSI J which is what the clear command // typically emits. diff --git a/src/vs/platform/terminal/common/capabilities/partialCommandDetectionCapability.ts b/src/vs/platform/terminal/common/capabilities/partialCommandDetectionCapability.ts index 5ec1e03c9b4..2b8f7cd0350 100644 --- a/src/vs/platform/terminal/common/capabilities/partialCommandDetectionCapability.ts +++ b/src/vs/platform/terminal/common/capabilities/partialCommandDetectionCapability.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter } from 'vs/base/common/event'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { IPartialCommandDetectionCapability, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; // Importing types is safe in any layer // eslint-disable-next-line local/code-import-patterns @@ -20,27 +21,28 @@ const enum Constants { * This capability guesses where commands are based on where the cursor was when enter was pressed. * It's very hit or miss but it's often correct and better than nothing. */ -export class PartialCommandDetectionCapability implements IPartialCommandDetectionCapability { +export class PartialCommandDetectionCapability extends DisposableStore implements IPartialCommandDetectionCapability { readonly type = TerminalCapability.PartialCommandDetection; private readonly _commands: IMarker[] = []; get commands(): readonly IMarker[] { return this._commands; } - private readonly _onCommandFinished = new Emitter(); + private readonly _onCommandFinished = this.add(new Emitter()); readonly onCommandFinished = this._onCommandFinished.event; constructor( private readonly _terminal: Terminal, ) { - this._terminal.onData(e => this._onData(e)); - this._terminal.parser.registerCsiHandler({ final: 'J' }, params => { + super(); + this.add(this._terminal.onData(e => this._onData(e))); + this.add(this._terminal.parser.registerCsiHandler({ final: 'J' }, params => { if (params.length >= 1 && (params[0] === 2 || params[0] === 3)) { this._clearCommandsInViewport(); } // We don't want to override xterm.js' default behavior, just augment it return false; - }); + })); } private _onData(data: string): void { diff --git a/src/vs/platform/terminal/common/capabilities/terminalCapabilityStore.ts b/src/vs/platform/terminal/common/capabilities/terminalCapabilityStore.ts index e630b8d898c..501ce1a78f3 100644 --- a/src/vs/platform/terminal/common/capabilities/terminalCapabilityStore.ts +++ b/src/vs/platform/terminal/common/capabilities/terminalCapabilityStore.ts @@ -102,9 +102,9 @@ export class TerminalCapabilityStoreMultiplexer extends Disposable implements IT this._onDidAddCapabilityType.fire(capability); this._onDidAddCapability.fire({ id: capability, capability: store.get(capability)! }); } - store.onDidAddCapabilityType(e => this._onDidAddCapabilityType.fire(e)); - store.onDidAddCapability(e => this._onDidAddCapability.fire(e)); - store.onDidRemoveCapabilityType(e => this._onDidRemoveCapabilityType.fire(e)); - store.onDidRemoveCapability(e => this._onDidRemoveCapability.fire(e)); + this._register(store.onDidAddCapabilityType(e => this._onDidAddCapabilityType.fire(e))); + this._register(store.onDidAddCapability(e => this._onDidAddCapability.fire(e))); + this._register(store.onDidRemoveCapabilityType(e => this._onDidRemoveCapabilityType.fire(e))); + this._register(store.onDidRemoveCapability(e => this._onDidRemoveCapability.fire(e))); } } diff --git a/src/vs/platform/terminal/common/requestStore.ts b/src/vs/platform/terminal/common/requestStore.ts index 8e843553d68..62ad1c641e8 100644 --- a/src/vs/platform/terminal/common/requestStore.ts +++ b/src/vs/platform/terminal/common/requestStore.ts @@ -32,6 +32,11 @@ export class RequestStore extends Disposable { ) { super(); this._timeout = timeout === undefined ? 15000 : timeout; + this._register(toDisposable(() => { + for (const d of this._pendingRequestDisposables.values()) { + dispose(d); + } + })); } /** diff --git a/src/vs/platform/terminal/common/terminalPlatformConfiguration.ts b/src/vs/platform/terminal/common/terminalPlatformConfiguration.ts index 01ab403711e..e901ffd34f8 100644 --- a/src/vs/platform/terminal/common/terminalPlatformConfiguration.ts +++ b/src/vs/platform/terminal/common/terminalPlatformConfiguration.ts @@ -353,7 +353,7 @@ const terminalPlatformConfiguration: IConfigurationNode = { default: true }, [TerminalSettingId.IgnoreProcessNames]: { - description: localize('terminal.integrated.confirmIgnoreProcesses', "A set of process names to ignore when using the {0} setting.", '`terminal.integrated.confirmOnKill`'), + markdownDescription: localize('terminal.integrated.confirmIgnoreProcesses', "A set of process names to ignore when using the {0} setting.", '`#terminal.integrated.confirmOnKill#`'), type: 'array', items: { type: 'string', diff --git a/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts b/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts index dd97f30b522..fb070c971d3 100644 --- a/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts +++ b/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts @@ -194,7 +194,7 @@ const enum ITermOscPt { */ export class ShellIntegrationAddon extends Disposable implements IShellIntegration, ITerminalAddon { private _terminal?: Terminal; - readonly capabilities = new TerminalCapabilityStore(); + readonly capabilities = this._register(new TerminalCapabilityStore()); private _hasUpdatedTelemetry: boolean = false; private _activationTimeout: any; private _commonProtocolDisposables: IDisposable[] = []; @@ -225,7 +225,7 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati activate(xterm: Terminal) { this._terminal = xterm; - this.capabilities.add(TerminalCapability.PartialCommandDetection, new PartialCommandDetectionCapability(this._terminal)); + this.capabilities.add(TerminalCapability.PartialCommandDetection, this._register(new PartialCommandDetectionCapability(this._terminal))); this._register(xterm.parser.registerOscHandler(ShellIntegrationOscPs.VSCode, data => this._handleVSCodeSequence(data))); this._register(xterm.parser.registerOscHandler(ShellIntegrationOscPs.ITerm, data => this._doHandleITermSequence(data))); this._commonProtocolDisposables.push( diff --git a/src/vs/platform/terminal/node/childProcessMonitor.ts b/src/vs/platform/terminal/node/childProcessMonitor.ts index 8e819de519d..9586353a96a 100644 --- a/src/vs/platform/terminal/node/childProcessMonitor.ts +++ b/src/vs/platform/terminal/node/childProcessMonitor.ts @@ -29,8 +29,6 @@ export const ignoreProcessNames: string[] = []; * calls into the monitor. */ export class ChildProcessMonitor extends Disposable { - private _isDisposed: boolean = false; - private _hasChildProcesses: boolean = false; private set hasChildProcesses(value: boolean) { if (this._hasChildProcesses !== value) { @@ -57,11 +55,6 @@ export class ChildProcessMonitor extends Disposable { super(); } - override dispose() { - this._isDisposed = true; - super.dispose(); - } - /** * Input was triggered on the process. */ @@ -78,7 +71,7 @@ export class ChildProcessMonitor extends Disposable { @debounce(Constants.ActiveDebounceDuration) private async _refreshActive(): Promise { - if (this._isDisposed) { + if (this._store.isDisposed) { return; } try { diff --git a/src/vs/platform/terminal/node/ptyHostMain.ts b/src/vs/platform/terminal/node/ptyHostMain.ts index cd0faa5ff20..f3693acbda3 100644 --- a/src/vs/platform/terminal/node/ptyHostMain.ts +++ b/src/vs/platform/terminal/node/ptyHostMain.ts @@ -21,6 +21,7 @@ import { HeartbeatService } from 'vs/platform/terminal/node/heartbeatService'; import { PtyService } from 'vs/platform/terminal/node/ptyService'; import { isUtilityProcess } from 'vs/base/parts/sandbox/node/electronTypes'; import { timeout } from 'vs/base/common/async'; +import { DisposableStore } from 'vs/base/common/lifecycle'; startPtyHost(); @@ -72,13 +73,15 @@ async function startPtyHost() { logService.warn(`Pty host is simulating ${simulatedLatency}ms latency`); } + const disposables = new DisposableStore(); + // Heartbeat responsiveness tracking const heartbeatService = new HeartbeatService(); - server.registerChannel(TerminalIpcChannels.Heartbeat, ProxyChannel.fromService(heartbeatService)); + server.registerChannel(TerminalIpcChannels.Heartbeat, ProxyChannel.fromService(heartbeatService, disposables)); // Init pty service const ptyService = new PtyService(logService, productService, reconnectConstants, simulatedLatency); - const ptyServiceChannel = ProxyChannel.fromService(ptyService); + const ptyServiceChannel = ProxyChannel.fromService(ptyService, disposables); server.registerChannel(TerminalIpcChannels.PtyHost, ptyServiceChannel); // Register a channel for direct communication via Message Port diff --git a/src/vs/platform/terminal/node/ptyHostService.ts b/src/vs/platform/terminal/node/ptyHostService.ts index 324e4063d95..f91b87baed4 100644 --- a/src/vs/platform/terminal/node/ptyHostService.ts +++ b/src/vs/platform/terminal/node/ptyHostService.ts @@ -63,7 +63,6 @@ export class PtyHostService extends Disposable implements IPtyHostService { private _wasQuitRequested = false; private _restartCount = 0; private _isResponsive = true; - private _isDisposed = false; private _heartbeatFirstTimeout?: NodeJS.Timeout; private _heartbeatSecondTimeout?: NodeJS.Timeout; @@ -158,7 +157,7 @@ export class PtyHostService extends Disposable implements IPtyHostService { // Handle exit this._register(connection.onDidProcessExit(e => { this._onPtyHostExit.fire(e.code); - if (!this._wasQuitRequested && !this._isDisposed) { + if (!this._wasQuitRequested && !this._store.isDisposed) { if (this._restartCount <= Constants.MaxRestarts) { this._logService.error(`ptyHost terminated unexpectedly with code ${e.code}`); this._restartCount++; @@ -196,11 +195,6 @@ export class PtyHostService extends Disposable implements IPtyHostService { return [connection, proxy]; } - override dispose() { - this._isDisposed = true; - super.dispose(); - } - async createProcess( shellLaunchConfig: IShellLaunchConfig, cwd: string, diff --git a/src/vs/platform/terminal/node/terminalProcess.ts b/src/vs/platform/terminal/node/terminalProcess.ts index b237e821fcc..a9ef29ca05e 100644 --- a/src/vs/platform/terminal/node/terminalProcess.ts +++ b/src/vs/platform/terminal/node/terminalProcess.ts @@ -6,7 +6,7 @@ import { exec } from 'child_process'; import { timeout } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import * as path from 'vs/base/common/path'; import { IProcessEnvironment, isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; @@ -106,7 +106,6 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess private _ptyProcess: IPty | undefined; private _currentTitle: string = ''; private _processStartupComplete: Promise | undefined; - private _isDisposed: boolean = false; private _windowsShellHelper: WindowsShellHelper | undefined; private _childProcessMonitor: ChildProcessMonitor | undefined; private _titleInterval: NodeJS.Timer | null = null; @@ -190,6 +189,12 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess this._register(this._windowsShellHelper.onShellNameChanged(e => this._onDidChangeProperty.fire({ type: ProcessPropertyType.Title, value: e }))); }); } + this._register(toDisposable(() => { + if (this._titleInterval) { + clearInterval(this._titleInterval); + this._titleInterval = null; + } + })); } async start(): Promise { @@ -327,15 +332,6 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess this._setupTitlePolling(ptyProcess); } - override dispose(): void { - this._isDisposed = true; - if (this._titleInterval) { - clearInterval(this._titleInterval); - } - this._titleInterval = null; - super.dispose(); - } - private _setupTitlePolling(ptyProcess: IPty) { // Send initial timeout async to give event listeners a chance to init setTimeout(() => this._sendProcessTitle(ptyProcess)); @@ -368,7 +364,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess // Wait to kill to process until the start up code has run. This prevents us from firing a process exit before a // process start. await this._processStartupComplete; - if (this._isDisposed) { + if (this._store.isDisposed) { return; } // Attempt to kill the pty, it may have already been killed at this @@ -408,7 +404,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess } private _sendProcessTitle(ptyProcess: IPty): void { - if (this._isDisposed) { + if (this._store.isDisposed) { return; } this._currentTitle = ptyProcess.process; @@ -428,11 +424,11 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess if (immediate && !isWindows) { this._kill(); } else { - if (!this._closeTimeout && !this._isDisposed) { + if (!this._closeTimeout && !this._store.isDisposed) { this._queueProcessExit(); // Allow a maximum amount of time for the process to exit, otherwise force kill it setTimeout(() => { - if (this._closeTimeout && !this._isDisposed) { + if (this._closeTimeout && !this._store.isDisposed) { this._closeTimeout = undefined; this._kill(); } @@ -442,7 +438,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess } input(data: string, isBinary?: boolean): void { - if (this._isDisposed || !this._ptyProcess) { + if (this._store.isDisposed || !this._ptyProcess) { return; } for (let i = 0; i <= Math.floor(data.length / Constants.WriteMaxChunkSize); i++) { @@ -523,7 +519,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess } resize(cols: number, rows: number): void { - if (this._isDisposed) { + if (this._store.isDisposed) { return; } if (typeof cols !== 'number' || typeof rows !== 'number' || isNaN(cols) || isNaN(rows)) { @@ -650,15 +646,6 @@ class DelayedResizer extends Disposable { this._timeout = setTimeout(() => { this._onTrigger.fire({ rows: this.rows, cols: this.cols }); }, 1000); - this._register({ - dispose: () => { - clearTimeout(this._timeout); - } - }); - } - - override dispose(): void { - super.dispose(); - clearTimeout(this._timeout); + this._register(toDisposable(() => clearTimeout(this._timeout))); } } diff --git a/src/vs/platform/terminal/node/windowsShellHelper.ts b/src/vs/platform/terminal/node/windowsShellHelper.ts index b93e17247a2..1def6545d69 100644 --- a/src/vs/platform/terminal/node/windowsShellHelper.ts +++ b/src/vs/platform/terminal/node/windowsShellHelper.ts @@ -35,7 +35,6 @@ const SHELL_EXECUTABLES = [ let windowsProcessTree: typeof WindowsProcessTreeType; export class WindowsShellHelper extends Disposable implements IWindowsShellHelper { - private _isDisposed: boolean; private _currentRequest: Promise | undefined; private _shellType: TerminalShellType | undefined; get shellType(): TerminalShellType | undefined { return this._shellType; } @@ -55,13 +54,11 @@ export class WindowsShellHelper extends Disposable implements IWindowsShellHelpe throw new Error(`WindowsShellHelper cannot be instantiated on ${platform}`); } - this._isDisposed = false; - this._startMonitoringShell(); } private async _startMonitoringShell(): Promise { - if (this._isDisposed) { + if (this._store.isDisposed) { return; } this.checkShell(); @@ -112,16 +109,11 @@ export class WindowsShellHelper extends Disposable implements IWindowsShellHelpe return this.traverseTree(tree.children[favouriteChild]); } - override dispose(): void { - this._isDisposed = true; - super.dispose(); - } - /** * Returns the innermost shell executable running in the terminal */ async getShellName(): Promise { - if (this._isDisposed) { + if (this._store.isDisposed) { return Promise.resolve(''); } // Prevent multiple requests at once, instead return current request diff --git a/src/vs/platform/terminal/test/common/requestStore.test.ts b/src/vs/platform/terminal/test/common/requestStore.test.ts index 377e878f955..3d0301d55b0 100644 --- a/src/vs/platform/terminal/test/common/requestStore.test.ts +++ b/src/vs/platform/terminal/test/common/requestStore.test.ts @@ -4,27 +4,34 @@ *--------------------------------------------------------------------------------------------*/ import { fail, strictEqual } from 'assert'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { ConsoleLogger, ILogService } from 'vs/platform/log/common/log'; import { LogService } from 'vs/platform/log/common/logService'; import { RequestStore } from 'vs/platform/terminal/common/requestStore'; suite('RequestStore', () => { + let disposables: DisposableStore; let instantiationService: TestInstantiationService; setup(() => { + disposables = new DisposableStore(); instantiationService = new TestInstantiationService(); instantiationService.stub(ILogService, new LogService(new ConsoleLogger())); }); teardown(() => { instantiationService.dispose(); + disposables.dispose(); }); + ensureNoDisposablesAreLeakedInTestSuite(); + test('should resolve requests', async () => { - const store: RequestStore<{ data: string }, { arg: string }> = instantiationService.createInstance(RequestStore<{ data: string }, { arg: string }>, undefined); + const store: RequestStore<{ data: string }, { arg: string }> = disposables.add(instantiationService.createInstance(RequestStore<{ data: string }, { arg: string }>, undefined)); let eventArgs: { requestId: number; arg: string } | undefined; - store.onCreateRequest(e => eventArgs = e); + disposables.add(store.onCreateRequest(e => eventArgs = e)); const request = store.createRequest({ arg: 'foo' }); strictEqual(typeof eventArgs?.requestId, 'number'); strictEqual(eventArgs?.arg, 'foo'); @@ -34,7 +41,7 @@ suite('RequestStore', () => { }); test('should reject the promise when the request times out', async () => { - const store: RequestStore<{ data: string }, { arg: string }> = instantiationService.createInstance(RequestStore<{ data: string }, { arg: string }>, 1); + const store: RequestStore<{ data: string }, { arg: string }> = disposables.add(instantiationService.createInstance(RequestStore<{ data: string }, { arg: string }>, 1)); const request = store.createRequest({ arg: 'foo' }); let threw = false; try { diff --git a/src/vs/platform/terminal/test/common/terminalEnvironment.test.ts b/src/vs/platform/terminal/test/common/terminalEnvironment.test.ts index 5f8aaca7e15..a6e8eb4f95a 100644 --- a/src/vs/platform/terminal/test/common/terminalEnvironment.test.ts +++ b/src/vs/platform/terminal/test/common/terminalEnvironment.test.ts @@ -5,9 +5,12 @@ import { strictEqual } from 'assert'; import { OperatingSystem, OS } from 'vs/base/common/platform'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { collapseTildePath, sanitizeCwd } from 'vs/platform/terminal/common/terminalEnvironment'; suite('terminalEnvironment', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + suite('collapseTildePath', () => { test('should return empty string for a falsy path', () => { strictEqual(collapseTildePath('', '/foo', '/'), ''); diff --git a/src/vs/platform/terminal/test/common/terminalProfiles.test.ts b/src/vs/platform/terminal/test/common/terminalProfiles.test.ts index 4720794b4f8..789fe9821bb 100644 --- a/src/vs/platform/terminal/test/common/terminalProfiles.test.ts +++ b/src/vs/platform/terminal/test/common/terminalProfiles.test.ts @@ -5,10 +5,13 @@ import { deepStrictEqual } from 'assert'; import { Codicon } from 'vs/base/common/codicons'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ITerminalProfile } from 'vs/platform/terminal/common/terminal'; import { createProfileSchemaEnums } from 'vs/platform/terminal/common/terminalProfiles'; suite('terminalProfiles', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + suite('createProfileSchemaEnums', () => { test('should return an empty array when there are no profiles', () => { deepStrictEqual(createProfileSchemaEnums([]), { diff --git a/src/vs/platform/terminal/test/common/terminalRecorder.test.ts b/src/vs/platform/terminal/test/common/terminalRecorder.test.ts index 9670c17e8c3..b1c523a2228 100644 --- a/src/vs/platform/terminal/test/common/terminalRecorder.test.ts +++ b/src/vs/platform/terminal/test/common/terminalRecorder.test.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ReplayEntry } from 'vs/platform/terminal/common/terminalProcess'; import { TerminalRecorder } from 'vs/platform/terminal/common/terminalRecorder'; @@ -15,6 +16,8 @@ async function eventsEqual(recorder: TerminalRecorder, expected: ReplayEntry[]) } suite('TerminalRecorder', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + test('should record dimensions', async () => { const recorder = new TerminalRecorder(1, 2); await eventsEqual(recorder, [ diff --git a/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts b/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts index e313005bf63..2f68a7be2df 100644 --- a/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts +++ b/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts @@ -6,6 +6,7 @@ import { deepStrictEqual, ok, strictEqual } from 'assert'; import { homedir, userInfo } from 'os'; import { isWindows } from 'vs/base/common/platform'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { NullLogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; import { ITerminalProcessOptions } from 'vs/platform/terminal/common/terminal'; @@ -21,6 +22,7 @@ const productService = { applicationName: 'vscode' } as IProductService; const defaultEnvironment = {}; suite('platform - terminalEnvironment', () => { + ensureNoDisposablesAreLeakedInTestSuite(); suite('getShellIntegrationInjection', () => { suite('should not enable', () => { // This test is only expected to work on Windows 10 build 18309 and above diff --git a/src/vs/platform/uriIdentity/test/common/uriIdentityService.test.ts b/src/vs/platform/uriIdentity/test/common/uriIdentityService.test.ts index caea0117d08..4b20f54a4db 100644 --- a/src/vs/platform/uriIdentity/test/common/uriIdentityService.test.ts +++ b/src/vs/platform/uriIdentity/test/common/uriIdentityService.test.ts @@ -9,6 +9,7 @@ import { mock } from 'vs/base/test/common/mock'; import { IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; import { Event } from 'vs/base/common/event'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('URI Identity', function () { @@ -38,6 +39,12 @@ suite('URI Identity', function () { ]))); }); + teardown(function () { + _service.dispose(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + function assertCanonical(input: URI, expected: URI, service: UriIdentityService = _service) { const actual = service.asCanonicalUri(input); assert.strictEqual(actual.toString(), expected.toString()); diff --git a/src/vs/platform/userData/test/browser/fileUserDataProvider.test.ts b/src/vs/platform/userData/test/browser/fileUserDataProvider.test.ts index be1d5377a39..98bafbffd0a 100644 --- a/src/vs/platform/userData/test/browser/fileUserDataProvider.test.ts +++ b/src/vs/platform/userData/test/browser/fileUserDataProvider.test.ts @@ -20,6 +20,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import product from 'vs/platform/product/common/product'; import { IUserDataProfilesService, UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; const ROOT = URI.file('tests').with({ scheme: 'vscode-tests' }); @@ -38,7 +39,7 @@ suite('FileUserDataProvider', () => { let backupWorkspaceHomeOnDisk: URI; let environmentService: IEnvironmentService; let userDataProfilesService: IUserDataProfilesService; - const disposables = new DisposableStore(); + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); let fileUserDataProvider: FileUserDataProvider; setup(async () => { @@ -54,15 +55,13 @@ suite('FileUserDataProvider', () => { await testObject.createFolder(backupWorkspaceHomeOnDisk); environmentService = new TestEnvironmentService(userDataHomeOnDisk); - userDataProfilesService = new UserDataProfilesService(environmentService, testObject, new UriIdentityService(testObject), logService); + userDataProfilesService = disposables.add(new UserDataProfilesService(environmentService, testObject, disposables.add(new UriIdentityService(testObject)), logService)); - fileUserDataProvider = new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, logService); + fileUserDataProvider = disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, logService)); disposables.add(fileUserDataProvider); disposables.add(testObject.registerProvider(Schemas.vscodeUserData, fileUserDataProvider)); }); - teardown(() => disposables.clear()); - test('exists return false when file does not exist', async () => { const exists = await testObject.exists(userDataProfilesService.defaultProfile.settingsResource); assert.strictEqual(exists, false); diff --git a/src/vs/platform/userDataProfile/test/common/userDataProfileService.test.ts b/src/vs/platform/userDataProfile/test/common/userDataProfileService.test.ts index 38f24155224..7e898dd15fa 100644 --- a/src/vs/platform/userDataProfile/test/common/userDataProfileService.test.ts +++ b/src/vs/platform/userDataProfile/test/common/userDataProfileService.test.ts @@ -9,12 +9,12 @@ import { NullLogService } from 'vs/platform/log/common/log'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { joinPath } from 'vs/base/common/resources'; -import { DisposableStore } from 'vs/base/common/lifecycle'; import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; import { AbstractNativeEnvironmentService } from 'vs/platform/environment/common/environmentService'; import product from 'vs/platform/product/common/product'; import { InMemoryUserDataProfilesService, UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; const ROOT = URI.file('tests').with({ scheme: 'vscode-tests' }); @@ -28,7 +28,7 @@ class TestEnvironmentService extends AbstractNativeEnvironmentService { suite('UserDataProfileService (Common)', () => { - const disposables = new DisposableStore(); + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); let testObject: UserDataProfilesService; let environmentService: TestEnvironmentService; @@ -40,10 +40,9 @@ suite('UserDataProfileService (Common)', () => { disposables.add(fileService.registerProvider(Schemas.vscodeUserData, fileSystemProvider)); environmentService = new TestEnvironmentService(joinPath(ROOT, 'User')); - testObject = new InMemoryUserDataProfilesService(environmentService, fileService, new UriIdentityService(fileService), logService); + testObject = disposables.add(new InMemoryUserDataProfilesService(environmentService, fileService, disposables.add(new UriIdentityService(fileService)), logService)); }); - teardown(() => disposables.clear()); test('default profile', () => { assert.strictEqual(testObject.defaultProfile.isDefault, true); diff --git a/src/vs/platform/userDataProfile/test/common/userDataProfileStorageService.test.ts b/src/vs/platform/userDataProfile/test/common/userDataProfileStorageService.test.ts index 1277db12632..48a68ee98a1 100644 --- a/src/vs/platform/userDataProfile/test/common/userDataProfileStorageService.test.ts +++ b/src/vs/platform/userDataProfile/test/common/userDataProfileStorageService.test.ts @@ -5,13 +5,13 @@ import * as assert from 'assert'; import { Emitter, Event } from 'vs/base/common/event'; -import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { InMemoryStorageDatabase, IStorageItemsChangeEvent, IUpdateRequest, Storage } from 'vs/base/parts/storage/common/storage'; import { AbstractUserDataProfileStorageService, IUserDataProfileStorageService } from 'vs/platform/userDataProfile/common/userDataProfileStorageService'; import { InMemoryStorageService, loadKeyTargets, StorageTarget, TARGET_KEY } from 'vs/platform/storage/common/storage'; import { IUserDataProfile, toUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; class TestStorageDatabase extends InMemoryStorageDatabase { @@ -48,18 +48,17 @@ export class TestUserDataProfileStorageService extends AbstractUserDataProfileSt suite('ProfileStorageService', () => { - const disposables = new DisposableStore(); + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); const profile = toUserDataProfile('test', 'test', URI.file('foo'), URI.file('cache')); let testObject: TestUserDataProfileStorageService; let storage: Storage; setup(async () => { - testObject = disposables.add(new TestUserDataProfileStorageService(new InMemoryStorageService())); - storage = new Storage(await testObject.setupStorageDatabase(profile)); + testObject = disposables.add(new TestUserDataProfileStorageService(disposables.add(new InMemoryStorageService()))); + storage = disposables.add(new Storage(await testObject.setupStorageDatabase(profile))); await storage.init(); }); - teardown(() => disposables.clear()); test('read empty storage', () => runWithFakedTimers({ useFakeTimers: true }, async () => { const actual = await testObject.readStorageData(profile); diff --git a/src/vs/platform/userDataProfile/test/electron-main/userDataProfileMainService.test.ts b/src/vs/platform/userDataProfile/test/electron-main/userDataProfileMainService.test.ts index 3ed115ec932..515b2ce80da 100644 --- a/src/vs/platform/userDataProfile/test/electron-main/userDataProfileMainService.test.ts +++ b/src/vs/platform/userDataProfile/test/electron-main/userDataProfileMainService.test.ts @@ -9,13 +9,13 @@ import { NullLogService } from 'vs/platform/log/common/log'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { joinPath } from 'vs/base/common/resources'; -import { DisposableStore } from 'vs/base/common/lifecycle'; import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; import { AbstractNativeEnvironmentService } from 'vs/platform/environment/common/environmentService'; import product from 'vs/platform/product/common/product'; import { UserDataProfilesMainService } from 'vs/platform/userDataProfile/electron-main/userDataProfile'; import { SaveStrategy, StateService } from 'vs/platform/state/node/stateService'; import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; const ROOT = URI.file('tests').with({ scheme: 'vscode-tests' }); @@ -31,7 +31,7 @@ class TestEnvironmentService extends AbstractNativeEnvironmentService { suite('UserDataProfileMainService', () => { - const disposables = new DisposableStore(); + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); let testObject: UserDataProfilesMainService; let environmentService: TestEnvironmentService, stateService: StateService; @@ -42,14 +42,12 @@ suite('UserDataProfileMainService', () => { disposables.add(fileService.registerProvider(Schemas.vscodeUserData, fileSystemProvider)); environmentService = new TestEnvironmentService(joinPath(ROOT, 'User')); - stateService = new StateService(SaveStrategy.DELAYED, environmentService, logService, fileService); + stateService = disposables.add(new StateService(SaveStrategy.DELAYED, environmentService, logService, fileService)); - testObject = new UserDataProfilesMainService(stateService, new UriIdentityService(fileService), environmentService, fileService, logService); + testObject = disposables.add(new UserDataProfilesMainService(stateService, disposables.add(new UriIdentityService(fileService)), environmentService, fileService, logService)); await stateService.init(); }); - teardown(() => disposables.clear()); - test('default profile', () => { assert.strictEqual(testObject.defaultProfile.isDefault, true); }); diff --git a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts index bda030426b5..d70ba14220d 100644 --- a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts +++ b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts @@ -133,7 +133,7 @@ export abstract class AbstractSynchroniser extends Disposable implements IUserDa private _onDidChangeConflicts = this._register(new Emitter()); readonly onDidChangeConflicts = this._onDidChangeConflicts.event; - private readonly localChangeTriggerThrottler = new ThrottledDelayer(50); + private readonly localChangeTriggerThrottler = this._register(new ThrottledDelayer(50)); private readonly _onDidChangeLocal: Emitter = this._register(new Emitter()); readonly onDidChangeLocal: Event = this._onDidChangeLocal.event; diff --git a/src/vs/platform/userDataSync/common/settingsSync.ts b/src/vs/platform/userDataSync/common/settingsSync.ts index 42eeb9df44d..8e48d445f5b 100644 --- a/src/vs/platform/userDataSync/common/settingsSync.ts +++ b/src/vs/platform/userDataSync/common/settingsSync.ts @@ -319,12 +319,12 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement private async getIgnoredSettings(content?: string): Promise { if (!this._defaultIgnoredSettings) { this._defaultIgnoredSettings = this.userDataSyncUtilService.resolveDefaultIgnoredSettings(); - const disposable = Event.any( + const disposable = this._register(Event.any( Event.filter(this.extensionManagementService.onDidInstallExtensions, (e => e.some(({ local }) => !!local))), Event.filter(this.extensionManagementService.onDidUninstallExtension, (e => !e.error)))(() => { disposable.dispose(); this._defaultIgnoredSettings = undefined; - }); + })); } const defaultIgnoredSettings = await this._defaultIgnoredSettings; return getIgnoredSettings(defaultIgnoredSettings, this.configurationService, content); diff --git a/src/vs/platform/userDataSync/test/common/globalStateSync.test.ts b/src/vs/platform/userDataSync/test/common/globalStateSync.test.ts index 6cf08639193..9e0c8296474 100644 --- a/src/vs/platform/userDataSync/test/common/globalStateSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/globalStateSync.test.ts @@ -5,7 +5,6 @@ import * as assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; -import { DisposableStore } from 'vs/base/common/lifecycle'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IFileService } from 'vs/platform/files/common/files'; @@ -15,17 +14,23 @@ import { GlobalStateSynchroniser } from 'vs/platform/userDataSync/common/globalS import { IGlobalState, ISyncData, IUserDataSyncStoreService, SyncResource, SyncStatus } from 'vs/platform/userDataSync/common/userDataSync'; import { IUserDataProfileStorageService } from 'vs/platform/userDataProfile/common/userDataProfileStorageService'; import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('GlobalStateSync', () => { - const disposableStore = new DisposableStore(); const server = new UserDataSyncTestServer(); let testClient: UserDataSyncClient; let client2: UserDataSyncClient; let testObject: GlobalStateSynchroniser; + teardown(async () => { + await testClient.instantiationService.get(IUserDataSyncStoreService).clear(); + }); + + const disposableStore = ensureNoDisposablesAreLeakedInTestSuite(); + setup(async () => { testClient = disposableStore.add(new UserDataSyncClient(server)); await testClient.setUp(true); @@ -35,11 +40,6 @@ suite('GlobalStateSync', () => { await client2.setUp(true); }); - teardown(async () => { - await testClient.instantiationService.get(IUserDataSyncStoreService).clear(); - disposableStore.clear(); - }); - test('when global state does not exist', () => runWithFakedTimers({ useFakeTimers: true }, async () => { assert.deepStrictEqual(await testObject.getLastSyncUserData(), null); let manifest = await testClient.getResourceManifest(); diff --git a/src/vs/platform/userDataSync/test/common/keybindingsMerge.test.ts b/src/vs/platform/userDataSync/test/common/keybindingsMerge.test.ts index f70f7a82d69..935c553a872 100644 --- a/src/vs/platform/userDataSync/test/common/keybindingsMerge.test.ts +++ b/src/vs/platform/userDataSync/test/common/keybindingsMerge.test.ts @@ -4,11 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { merge } from 'vs/platform/userDataSync/common/keybindingsMerge'; import { TestUserDataSyncUtilService } from 'vs/platform/userDataSync/test/common/userDataSyncClient'; suite('KeybindingsMerge - No Conflicts', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + test('merge when local and remote are same with one entry', async () => { const localContent = stringify([{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }]); const remoteContent = stringify([{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }]); @@ -542,7 +545,7 @@ suite('KeybindingsMerge - No Conflicts', () => { ]`); }); - test('merge when local and remote has moved forwareded with conflicts', async () => { + test('merge when local and remote has moved forwareded with conflicts (2)', async () => { const baseContent = stringify([ { key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' }, { key: 'alt+c', command: '-a' }, diff --git a/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts b/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts index 44fb14361a7..3e241a7bd9d 100644 --- a/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IFileService } from 'vs/platform/files/common/files'; import { ILogService } from 'vs/platform/log/common/log'; import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; @@ -15,22 +15,23 @@ import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userData suite('KeybindingsSync', () => { - const disposableStore = new DisposableStore(); const server = new UserDataSyncTestServer(); let client: UserDataSyncClient; let testObject: KeybindingsSynchroniser; + teardown(async () => { + await client.instantiationService.get(IUserDataSyncStoreService).clear(); + }); + + const disposableStore = ensureNoDisposablesAreLeakedInTestSuite(); + setup(async () => { client = disposableStore.add(new UserDataSyncClient(server)); await client.setUp(true); testObject = client.getSynchronizer(SyncResource.Keybindings) as KeybindingsSynchroniser; }); - teardown(async () => { - await client.instantiationService.get(IUserDataSyncStoreService).clear(); - disposableStore.clear(); - }); test('when keybindings file does not exist', async () => { const fileService = client.instantiationService.get(IFileService); diff --git a/src/vs/platform/userDataSync/test/common/settingsSync.test.ts b/src/vs/platform/userDataSync/test/common/settingsSync.test.ts index 8ce9aadef2d..e6908111ebc 100644 --- a/src/vs/platform/userDataSync/test/common/settingsSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/settingsSync.test.ts @@ -6,8 +6,8 @@ import * as assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { Event } from 'vs/base/common/event'; -import { DisposableStore } from 'vs/base/common/lifecycle'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ConfigurationScope, Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { IFileService } from 'vs/platform/files/common/files'; @@ -19,11 +19,16 @@ import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userData suite('SettingsSync - Auto', () => { - const disposableStore = new DisposableStore(); const server = new UserDataSyncTestServer(); let client: UserDataSyncClient; let testObject: SettingsSynchroniser; + teardown(async () => { + await client.instantiationService.get(IUserDataSyncStoreService).clear(); + }); + + const disposableStore = ensureNoDisposablesAreLeakedInTestSuite(); + setup(async () => { Registry.as(Extensions.Configuration).registerConfiguration({ 'id': 'settingsSync', @@ -44,11 +49,6 @@ suite('SettingsSync - Auto', () => { testObject = client.getSynchronizer(SyncResource.Settings) as SettingsSynchroniser; }); - teardown(async () => { - await client.instantiationService.get(IUserDataSyncStoreService).clear(); - disposableStore.clear(); - }); - test('when settings file does not exist', () => runWithFakedTimers({ useFakeTimers: true }, async () => { const fileService = client.instantiationService.get(IFileService); const settingResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.settingsResource; @@ -529,23 +529,22 @@ suite('SettingsSync - Auto', () => { suite('SettingsSync - Manual', () => { - const disposableStore = new DisposableStore(); const server = new UserDataSyncTestServer(); let client: UserDataSyncClient; let testObject: SettingsSynchroniser; + teardown(async () => { + await client.instantiationService.get(IUserDataSyncStoreService).clear(); + }); + + const disposableStore = ensureNoDisposablesAreLeakedInTestSuite(); + setup(async () => { client = disposableStore.add(new UserDataSyncClient(server)); await client.setUp(true); testObject = client.getSynchronizer(SyncResource.Settings) as SettingsSynchroniser; }); - teardown(async () => { - await client.instantiationService.get(IUserDataSyncStoreService).clear(); - disposableStore.clear(); - }); - - test('do not sync ignored settings', () => runWithFakedTimers({ useFakeTimers: true }, async () => { const settingsContent = `{ diff --git a/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts b/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts index 2b39ada8d1c..531665ceddd 100644 --- a/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts @@ -6,9 +6,9 @@ import * as assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { IStringDictionary } from 'vs/base/common/collections'; -import { DisposableStore } from 'vs/base/common/lifecycle'; import { dirname, joinPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IFileService } from 'vs/platform/files/common/files'; import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; @@ -148,13 +148,18 @@ const globalSnippet = `{ suite('SnippetsSync', () => { - const disposableStore = new DisposableStore(); const server = new UserDataSyncTestServer(); let testClient: UserDataSyncClient; let client2: UserDataSyncClient; let testObject: SnippetsSynchroniser; + teardown(async () => { + await testClient.instantiationService.get(IUserDataSyncStoreService).clear(); + }); + + const disposableStore = ensureNoDisposablesAreLeakedInTestSuite(); + setup(async () => { testClient = disposableStore.add(new UserDataSyncClient(server)); await testClient.setUp(true); @@ -164,11 +169,6 @@ suite('SnippetsSync', () => { await client2.setUp(true); }); - teardown(async () => { - await testClient.instantiationService.get(IUserDataSyncStoreService).clear(); - disposableStore.clear(); - }); - test('when snippets does not exist', async () => { const fileService = testClient.instantiationService.get(IFileService); const snippetsResource = testClient.instantiationService.get(IUserDataProfilesService).defaultProfile.snippetsHome; diff --git a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts index 1db10faccdc..d226ceb9dba 100644 --- a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts +++ b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts @@ -8,10 +8,10 @@ import { Barrier } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; -import { DisposableStore } from 'vs/base/common/lifecycle'; import { isEqual, joinPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IFileService } from 'vs/platform/files/common/files'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; @@ -177,20 +177,20 @@ class TestSynchroniser extends AbstractSynchroniser { suite('TestSynchronizer - Auto Sync', () => { - const disposableStore = new DisposableStore(); const server = new UserDataSyncTestServer(); let client: UserDataSyncClient; + teardown(async () => { + await client.instantiationService.get(IUserDataSyncStoreService).clear(); + }); + + const disposableStore = ensureNoDisposablesAreLeakedInTestSuite(); + setup(async () => { client = disposableStore.add(new UserDataSyncClient(server)); await client.setUp(); }); - teardown(async () => { - await client.instantiationService.get(IUserDataSyncStoreService).clear(); - disposableStore.clear(); - }); - test('status is syncing', () => runWithFakedTimers({ useFakeTimers: true }, async () => { const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); @@ -484,20 +484,20 @@ suite('TestSynchronizer - Auto Sync', () => { suite('TestSynchronizer - Manual Sync', () => { - const disposableStore = new DisposableStore(); const server = new UserDataSyncTestServer(); let client: UserDataSyncClient; + teardown(async () => { + await client.instantiationService.get(IUserDataSyncStoreService).clear(); + }); + + const disposableStore = ensureNoDisposablesAreLeakedInTestSuite(); + setup(async () => { client = disposableStore.add(new UserDataSyncClient(server)); await client.setUp(); }); - teardown(async () => { - await client.instantiationService.get(IUserDataSyncStoreService).clear(); - disposableStore.clear(); - }); - test('preview', () => runWithFakedTimers({ useFakeTimers: true }, async () => { const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); testObject.syncResult = { hasConflicts: false, hasError: false }; @@ -1062,20 +1062,20 @@ suite('TestSynchronizer - Manual Sync', () => { }); suite('TestSynchronizer - Last Sync Data', () => { - const disposableStore = new DisposableStore(); const server = new UserDataSyncTestServer(); let client: UserDataSyncClient; + teardown(async () => { + await client.instantiationService.get(IUserDataSyncStoreService).clear(); + }); + + const disposableStore = ensureNoDisposablesAreLeakedInTestSuite(); + setup(async () => { client = disposableStore.add(new UserDataSyncClient(server)); await client.setUp(); }); - teardown(async () => { - await client.instantiationService.get(IUserDataSyncStoreService).clear(); - disposableStore.clear(); - }); - test('last sync data is null when not synced before', () => runWithFakedTimers({ useFakeTimers: true }, async () => { const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); diff --git a/src/vs/platform/userDataSync/test/common/tasksSync.test.ts b/src/vs/platform/userDataSync/test/common/tasksSync.test.ts index 01be339a733..52669939bef 100644 --- a/src/vs/platform/userDataSync/test/common/tasksSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/tasksSync.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IFileService } from 'vs/platform/files/common/files'; import { ILogService } from 'vs/platform/log/common/log'; import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; @@ -15,23 +15,23 @@ import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userData suite('TasksSync', () => { - const disposableStore = new DisposableStore(); const server = new UserDataSyncTestServer(); let client: UserDataSyncClient; let testObject: TasksSynchroniser; + teardown(async () => { + await client.instantiationService.get(IUserDataSyncStoreService).clear(); + }); + + const disposableStore = ensureNoDisposablesAreLeakedInTestSuite(); + setup(async () => { client = disposableStore.add(new UserDataSyncClient(server)); await client.setUp(true); testObject = client.getSynchronizer(SyncResource.Tasks) as TasksSynchroniser; }); - teardown(async () => { - await client.instantiationService.get(IUserDataSyncStoreService).clear(); - disposableStore.clear(); - }); - test('when tasks file does not exist', async () => { const fileService = client.instantiationService.get(IFileService); const tasksResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; diff --git a/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts b/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts index 7f6e72d2ff2..47cde8003c7 100644 --- a/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts @@ -4,12 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { timeout } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; import { Event } from 'vs/base/common/event'; -import { DisposableStore } from 'vs/base/common/lifecycle'; import { joinPath } from 'vs/base/common/resources'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IFileService } from 'vs/platform/files/common/files'; import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; @@ -29,9 +28,7 @@ class TestUserDataAutoSyncService extends UserDataAutoSyncService { suite('UserDataAutoSyncService', () => { - const disposableStore = new DisposableStore(); - - teardown(() => disposableStore.clear()); + const disposableStore = ensureNoDisposablesAreLeakedInTestSuite(); test('test auto sync with sync resource change triggers sync', async () => { await runWithFakedTimers({}, async () => { @@ -382,7 +379,7 @@ suite('UserDataAutoSyncService', () => { const errorPromise = Event.toPromise(testObject.onError); await testObject.sync(); - const e = await Promise.race([errorPromise, timeout(0)]); + const e = await errorPromise; assert.ok(e instanceof UserDataAutoSyncError); assert.deepStrictEqual((e).code, UserDataSyncErrorCode.SessionExpired); assert.deepStrictEqual(target.requests, [ diff --git a/src/vs/platform/userDataSync/test/common/userDataProfilesManifestSync.test.ts b/src/vs/platform/userDataSync/test/common/userDataProfilesManifestSync.test.ts index 502dc913380..745eac0ae58 100644 --- a/src/vs/platform/userDataSync/test/common/userDataProfilesManifestSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataProfilesManifestSync.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { UserDataProfilesManifestSynchroniser } from 'vs/platform/userDataSync/common/userDataProfilesManifestSync'; import { ISyncData, ISyncUserDataProfile, IUserDataSyncStoreService, SyncResource, SyncStatus } from 'vs/platform/userDataSync/common/userDataSync'; @@ -12,13 +12,18 @@ import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userData suite('UserDataProfilesManifestSync', () => { - const disposableStore = new DisposableStore(); const server = new UserDataSyncTestServer(); let testClient: UserDataSyncClient; let client2: UserDataSyncClient; let testObject: UserDataProfilesManifestSynchroniser; + teardown(async () => { + await testClient.instantiationService.get(IUserDataSyncStoreService).clear(); + }); + + const disposableStore = ensureNoDisposablesAreLeakedInTestSuite(); + setup(async () => { testClient = disposableStore.add(new UserDataSyncClient(server)); await testClient.setUp(true); @@ -28,11 +33,6 @@ suite('UserDataProfilesManifestSync', () => { await client2.setUp(true); }); - teardown(async () => { - await testClient.instantiationService.get(IUserDataSyncStoreService).clear(); - disposableStore.clear(); - }); - test('when profiles does not exist', async () => { assert.deepStrictEqual(await testObject.getLastSyncUserData(), null); let manifest = await testClient.getResourceManifest(); diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts index c9398a04e20..730850b4b69 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts @@ -56,7 +56,7 @@ export class UserDataSyncClient extends Disposable { } async setUp(empty: boolean = false): Promise { - registerConfiguration(); + this._register(registerConfiguration()); const logService = this.instantiationService.stub(ILogService, new NullLogService()); @@ -83,17 +83,17 @@ export class UserDataSyncClient extends Disposable { }); const fileService = this._register(new FileService(logService)); - fileService.registerProvider(Schemas.inMemory, new InMemoryFileSystemProvider()); - fileService.registerProvider(USER_DATA_SYNC_SCHEME, new InMemoryFileSystemProvider()); + this._register(fileService.registerProvider(Schemas.inMemory, this._register(new InMemoryFileSystemProvider()))); + this._register(fileService.registerProvider(USER_DATA_SYNC_SCHEME, this._register(new InMemoryFileSystemProvider()))); this.instantiationService.stub(IFileService, fileService); - const uriIdentityService = this.instantiationService.createInstance(UriIdentityService); + const uriIdentityService = this._register(this.instantiationService.createInstance(UriIdentityService)); this.instantiationService.stub(IUriIdentityService, uriIdentityService); - const userDataProfilesService = new InMemoryUserDataProfilesService(environmentService, fileService, uriIdentityService, logService); + const userDataProfilesService = this._register(new InMemoryUserDataProfilesService(environmentService, fileService, uriIdentityService, logService)); this.instantiationService.stub(IUserDataProfilesService, userDataProfilesService); - const storageService = new TestStorageService(userDataProfilesService.defaultProfile); + const storageService = this._register(new TestStorageService(userDataProfilesService.defaultProfile)); this.instantiationService.stub(IStorageService, this._register(storageService)); this.instantiationService.stub(IUserDataProfileStorageService, this._register(new TestUserDataProfileStorageService(storageService))); diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts b/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts index 42cfe4a1e47..665d09ed41e 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts @@ -5,8 +5,8 @@ import * as assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; -import { DisposableStore } from 'vs/base/common/lifecycle'; import { dirname, joinPath } from 'vs/base/common/resources'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IFileService } from 'vs/platform/files/common/files'; import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; @@ -15,9 +15,7 @@ import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userData suite('UserDataSyncService', () => { - const disposableStore = new DisposableStore(); - - teardown(() => disposableStore.clear()); + const disposableStore = ensureNoDisposablesAreLeakedInTestSuite(); test('test first time sync ever', async () => { // Setup the client diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts b/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts index 83a6a6f0f36..42f3abd7e56 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts @@ -8,9 +8,9 @@ import { timeout } from 'vs/base/common/async'; import { newWriteableBufferStream } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; -import { DisposableStore } from 'vs/base/common/lifecycle'; import { isWeb } from 'vs/base/common/platform'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { NullLogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; import { IRequestService } from 'vs/platform/request/common/request'; @@ -20,9 +20,7 @@ import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userData suite('UserDataSyncStoreService', () => { - const disposableStore = new DisposableStore(); - - teardown(() => disposableStore.clear()); + const disposableStore = ensureNoDisposablesAreLeakedInTestSuite(); test('test read manifest for the first time', async () => { // Setup the client diff --git a/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts b/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts index faebfc38595..3215e9961ec 100644 --- a/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts +++ b/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts @@ -17,6 +17,7 @@ import { findWindowOnFile } from 'vs/platform/windows/electron-main/windowsFinde import { toWorkspaceFolders } from 'vs/platform/workspaces/common/workspaces'; import { IWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; import { FileAccess } from 'vs/base/common/network'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('WindowsFinder', () => { @@ -107,4 +108,6 @@ suite('WindowsFinder', () => { const window: ICodeWindow = createTestCodeWindow({ lastFocusTime: 1, openedWorkspace: testWorkspace }); assert.strictEqual(await findWindowOnFile([window], URI.file(join(fixturesFolder, 'vscode_workspace_2_folder', 'nested_vscode_folder', 'subfolder', 'file.txt')), localWorkspaceResolver), window); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/platform/windows/test/electron-main/windowsStateHandler.test.ts b/src/vs/platform/windows/test/electron-main/windowsStateHandler.test.ts index 60cc320d34f..0b96b1bf740 100644 --- a/src/vs/platform/windows/test/electron-main/windowsStateHandler.test.ts +++ b/src/vs/platform/windows/test/electron-main/windowsStateHandler.test.ts @@ -7,6 +7,7 @@ import * as assert from 'assert'; import { tmpdir } from 'os'; import { join } from 'vs/base/common/path'; import { URI } from 'vs/base/common/uri'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IWindowState as IWindowUIState, WindowMode } from 'vs/platform/window/electron-main/window'; import { getWindowsStateStoreData, IWindowsState, IWindowState, restoreWindowsState } from 'vs/platform/windows/electron-main/windowsStateHandler'; import { IWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; @@ -198,4 +199,6 @@ suite('Windows State Storing', () => { }; assertEqualWindowsState(expected, windowsState, 'v1_32_empty_window'); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/platform/workspaces/test/electron-main/workspacesHistoryStorage.test.ts b/src/vs/platform/workspaces/test/electron-main/workspacesHistoryStorage.test.ts index f75cdcc3f38..2677ffba87d 100644 --- a/src/vs/platform/workspaces/test/electron-main/workspacesHistoryStorage.test.ts +++ b/src/vs/platform/workspaces/test/electron-main/workspacesHistoryStorage.test.ts @@ -7,6 +7,7 @@ import * as assert from 'assert'; import { tmpdir } from 'os'; import { join } from 'vs/base/common/path'; import { URI } from 'vs/base/common/uri'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { NullLogService } from 'vs/platform/log/common/log'; import { IWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; import { IRecentFolder, IRecentlyOpened, IRecentWorkspace, isRecentFolder, restoreRecentlyOpened, toStoreData } from 'vs/platform/workspaces/common/workspaces'; @@ -143,4 +144,6 @@ suite('History Storage', () => { assertEqualRecentlyOpened(windowsState, expected, 'v1_33'); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts b/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts index d6b76495b43..0234ce57bb9 100644 --- a/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts +++ b/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts @@ -13,6 +13,7 @@ import { isWindows } from 'vs/base/common/platform'; import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import * as pfs from 'vs/base/node/pfs'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; import { IWorkspaceBackupInfo, IFolderBackupInfo } from 'vs/platform/backup/common/backup'; import { IBackupMainService } from 'vs/platform/backup/electron-main/backup'; @@ -357,4 +358,6 @@ flakySuite('WorkspacesManagementMainService', () => { untitled = service.getUntitledWorkspaces(); assert.strictEqual(0, untitled.length); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/api/browser/mainThreadTreeViews.ts b/src/vs/workbench/api/browser/mainThreadTreeViews.ts index 279a4f05c28..5451d23f352 100644 --- a/src/vs/workbench/api/browser/mainThreadTreeViews.ts +++ b/src/vs/workbench/api/browser/mainThreadTreeViews.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ExtHostContext, MainThreadTreeViewsShape, ExtHostTreeViewsShape, MainContext, CheckboxUpdate } from 'vs/workbench/api/common/extHost.protocol'; import { ITreeViewDataProvider, ITreeItem, IViewsService, ITreeView, IViewsRegistry, ITreeViewDescriptor, IRevealOptions, Extensions, ResolvableTreeItem, ITreeViewDragAndDropController, IViewBadge, NoTreeViewError } from 'vs/workbench/common/views'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; @@ -24,7 +24,7 @@ import { IMarkdownString } from 'vs/base/common/htmlContent'; export class MainThreadTreeViews extends Disposable implements MainThreadTreeViewsShape { private readonly _proxy: ExtHostTreeViewsShape; - private readonly _dataProviders: Map = new Map(); + private readonly _dataProviders: Map = new Map(); private readonly _dndControllers = new Map(); constructor( @@ -43,7 +43,9 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie this.extensionService.whenInstalledExtensionsRegistered().then(() => { const dataProvider = new TreeViewDataProvider(treeViewId, this._proxy, this.notificationService); - this._dataProviders.set(treeViewId, dataProvider); + const disposables = new DisposableStore(); + this._register(disposables); + this._dataProviders.set(treeViewId, { dataProvider, disposables }); const dndController = (options.hasHandleDrag || options.hasHandleDrop) ? new TreeViewDragAndDropController(treeViewId, options.dropMimeTypes, options.dragMimeTypes, options.hasHandleDrag, this._proxy) : undefined; const viewer = this.getTreeView(treeViewId); @@ -58,7 +60,7 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie this._dndControllers.set(treeViewId, dndController); } viewer.dataProvider = dataProvider; - this.registerListeners(treeViewId, viewer); + this.registerListeners(treeViewId, viewer, disposables); this._proxy.$setVisible(treeViewId, viewer.visible); } else { this.notificationService.error('No view is registered with id: ' + treeViewId); @@ -73,7 +75,7 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie .then(() => { const viewer = this.getTreeView(treeViewId); if (viewer && itemInfo) { - return this.reveal(viewer, this._dataProviders.get(treeViewId)!, itemInfo.item, itemInfo.parentChain, options); + return this.reveal(viewer, this._dataProviders.get(treeViewId)!.dataProvider, itemInfo.item, itemInfo.parentChain, options); } return undefined; }); @@ -85,7 +87,7 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie const viewer = this.getTreeView(treeViewId); const dataProvider = this._dataProviders.get(treeViewId); if (viewer && dataProvider) { - const itemsToRefresh = dataProvider.getItemsToRefresh(itemsToRefreshByHandle); + const itemsToRefresh = dataProvider.dataProvider.getItemsToRefresh(itemsToRefreshByHandle); return viewer.refresh(itemsToRefresh.length ? itemsToRefresh : undefined); } return Promise.resolve(); @@ -132,6 +134,11 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie if (viewer) { viewer.dataProvider = undefined; } + const dataProvider = this._dataProviders.get(treeViewId); + if (dataProvider) { + dataProvider.disposables.dispose(); + this._dataProviders.delete(treeViewId); + } } private async reveal(treeView: ITreeView, dataProvider: TreeViewDataProvider, itemIn: ITreeItem, parentChain: ITreeItem[], options: IRevealOptions): Promise { @@ -175,12 +182,12 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie } } - private registerListeners(treeViewId: string, treeView: ITreeView): void { - this._register(treeView.onDidExpandItem(item => this._proxy.$setExpanded(treeViewId, item.handle, true))); - this._register(treeView.onDidCollapseItem(item => this._proxy.$setExpanded(treeViewId, item.handle, false))); - this._register(treeView.onDidChangeSelectionAndFocus(items => this._proxy.$setSelectionAndFocus(treeViewId, items.selection.map(({ handle }) => handle), items.focus.handle))); - this._register(treeView.onDidChangeVisibility(isVisible => this._proxy.$setVisible(treeViewId, isVisible))); - this._register(treeView.onDidChangeCheckboxState(items => { + private registerListeners(treeViewId: string, treeView: ITreeView, disposables: DisposableStore): void { + disposables.add(treeView.onDidExpandItem(item => this._proxy.$setExpanded(treeViewId, item.handle, true))); + disposables.add(treeView.onDidCollapseItem(item => this._proxy.$setExpanded(treeViewId, item.handle, false))); + disposables.add(treeView.onDidChangeSelectionAndFocus(items => this._proxy.$setSelectionAndFocus(treeViewId, items.selection.map(({ handle }) => handle), items.focus.handle))); + disposables.add(treeView.onDidChangeVisibility(isVisible => this._proxy.$setVisible(treeViewId, isVisible))); + disposables.add(treeView.onDidChangeCheckboxState(items => { this._proxy.$changeCheckboxState(treeViewId, items.map(item => { return { treeItemHandle: item.handle, newState: item.checkbox?.isChecked ?? false }; })); diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 95d016287a2..fcc80b4cacc 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1298,9 +1298,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I // this needs to be updated whenever the API proposal changes _version: 1, - registerInteractiveEditorSessionProvider(provider: vscode.InteractiveEditorSessionProvider) { + registerInteractiveEditorSessionProvider(provider: vscode.InteractiveEditorSessionProvider, metadata?: vscode.InteractiveEditorSessionProviderMetadata) { checkProposedApiEnabled(extension, 'interactive'); - return extHostInteractiveEditor.registerProvider(extension, provider); + return extHostInteractiveEditor.registerProvider(extension, provider, metadata = { label: provider.label ?? extension.displayName ?? extension.name }); }, registerInteractiveSessionProvider(id: string, provider: vscode.InteractiveSessionProvider) { checkProposedApiEnabled(extension, 'interactive'); diff --git a/src/vs/workbench/api/common/extHostApiCommands.ts b/src/vs/workbench/api/common/extHostApiCommands.ts index 84545808c0f..f4f7c8ee4a7 100644 --- a/src/vs/workbench/api/common/extHostApiCommands.ts +++ b/src/vs/workbench/api/common/extHostApiCommands.ts @@ -14,7 +14,7 @@ import { decodeSemanticTokensDto } from 'vs/editor/common/services/semanticToken import { validateWhenClauses } from 'vs/platform/contextkey/common/contextkey'; import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { matchesSomeScheme } from 'vs/platform/opener/common/opener'; -import { ICallHierarchyItemDto, IIncomingCallDto, IOutgoingCallDto, IRawColorInfo, ITypeHierarchyItemDto, IWorkspaceEditDto } from 'vs/workbench/api/common/extHost.protocol'; +import { ICallHierarchyItemDto, IIncomingCallDto, IInlineValueContextDto, IOutgoingCallDto, IRawColorInfo, ITypeHierarchyItemDto, IWorkspaceEditDto } from 'vs/workbench/api/common/extHost.protocol'; import { ApiCommand, ApiCommandArgument, ApiCommandResult, ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { CustomCodeAction } from 'vs/workbench/api/common/extHostLanguageFeatures'; import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; @@ -385,7 +385,11 @@ const newCommands: ApiCommand[] = [ // --- debug support new ApiCommand( 'vscode.executeInlineValueProvider', '_executeInlineValueProvider', 'Execute inline value provider', - [ApiCommandArgument.Uri, ApiCommandArgument.Range], + [ + ApiCommandArgument.Uri, + ApiCommandArgument.Range, + new ApiCommandArgument('context', 'An InlineValueContext', v => v && typeof v.frameId === 'number' && v.stoppedLocation instanceof types.Range, v => typeConverters.InlineValueContext.from(v)) + ], new ApiCommandResult('A promise that resolves to an array of InlineValue objects', result => { return result.map(typeConverters.InlineValue.to); }) diff --git a/src/vs/workbench/api/common/extHostInlineChat.ts b/src/vs/workbench/api/common/extHostInlineChat.ts index d46e67243fc..65260f369cb 100644 --- a/src/vs/workbench/api/common/extHostInlineChat.ts +++ b/src/vs/workbench/api/common/extHostInlineChat.ts @@ -92,10 +92,10 @@ export class ExtHostInteractiveEditor implements ExtHostInlineChatShape { )); } - registerProvider(extension: Readonly, provider: vscode.InteractiveEditorSessionProvider): vscode.Disposable { + registerProvider(extension: Readonly, provider: vscode.InteractiveEditorSessionProvider, metadata: vscode.InteractiveEditorSessionProviderMetadata): vscode.Disposable { const wrapper = new ProviderWrapper(extension, provider); this._inputProvider.set(wrapper.handle, wrapper); - this._proxy.$registerInteractiveEditorProvider(wrapper.handle, provider.label, extension.identifier.value, typeof provider.handleInteractiveEditorResponseFeedback === 'function'); + this._proxy.$registerInteractiveEditorProvider(wrapper.handle, metadata.label, extension.identifier.value, typeof provider.handleInteractiveEditorResponseFeedback === 'function'); return toDisposable(() => { this._proxy.$unregisterInteractiveEditorProvider(wrapper.handle); this._inputProvider.delete(wrapper.handle); @@ -173,7 +173,7 @@ export class ExtHostInteractiveEditor implements ExtHostInlineChatShape { const task = typeof entry.provider.provideInteractiveEditorResponse2 === 'function' ? entry.provider.provideInteractiveEditorResponse2(apiRequest, progress, token) - : entry.provider.provideInteractiveEditorResponse(apiRequest, token); + : entry.provider.provideInteractiveEditorResponse(apiRequest.session, apiRequest, progress, token); Promise.resolve(task).finally(() => done = true); diff --git a/src/vs/workbench/api/common/extHostNotebookKernels.ts b/src/vs/workbench/api/common/extHostNotebookKernels.ts index f3e1263c357..2401b9f34f3 100644 --- a/src/vs/workbench/api/common/extHostNotebookKernels.ts +++ b/src/vs/workbench/api/common/extHostNotebookKernels.ts @@ -111,7 +111,6 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { const _defaultExecutHandler = () => console.warn(`NO execute handler from notebook controller '${data.id}' of extension: '${extension.identifier}'`); let isDisposed = false; - const commandDisposables = new DisposableStore(); const onDidChangeSelection = new Emitter<{ selected: boolean; notebook: vscode.NotebookDocument }>(); const onDidReceiveMessage = new Emitter<{ editor: vscode.NotebookEditor; message: any }>(); @@ -236,7 +235,6 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { this._logService.trace(`NotebookController[${handle}], DISPOSED`); isDisposed = true; this._kernelData.delete(handle); - commandDisposables.dispose(); onDidChangeSelection.dispose(); onDidReceiveMessage.dispose(); this._proxy.$removeKernel(handle); diff --git a/src/vs/workbench/api/common/extHostTreeViews.ts b/src/vs/workbench/api/common/extHostTreeViews.ts index 3de4e43cbf9..51d394e431d 100644 --- a/src/vs/workbench/api/common/extHostTreeViews.ts +++ b/src/vs/workbench/api/common/extHostTreeViews.ts @@ -47,7 +47,7 @@ function toTreeItemLabel(label: any, extension: IExtensionDescription): ITreeIte } -export class ExtHostTreeViews implements ExtHostTreeViewsShape { +export class ExtHostTreeViews extends Disposable implements ExtHostTreeViewsShape { private treeViews: Map> = new Map>(); private treeDragAndDropService: ITreeViewsDnDService = new TreeViewsDnDService(); @@ -57,7 +57,7 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape { private commands: ExtHostCommands, private logService: ILogService ) { - + super(); function isTreeViewConvertableItem(arg: any): boolean { return arg && arg.$treeViewId && (arg.$treeItemHandle || arg.$selectedTreeItems || arg.$focusedTreeItem); } @@ -152,6 +152,7 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape { treeView.dispose(); } }; + this._register(view); return view as vscode.TreeView; } @@ -262,7 +263,7 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape { } private createExtHostTreeView(id: string, options: vscode.TreeViewOptions, extension: IExtensionDescription): ExtHostTreeView { - const treeView = new ExtHostTreeView(id, options, this._proxy, this.commands.converter, this.logService, extension); + const treeView = this._register(new ExtHostTreeView(id, options, this._proxy, this.commands.converter, this.logService, extension)); this.treeViews.set(id, treeView); return treeView; } @@ -359,7 +360,7 @@ class ExtHostTreeView extends Disposable { let refreshingPromise: Promise | null; let promiseCallback: () => void; - this._register(Event.debounce, { message: boolean; elements: (T | Root)[] }>(this._onDidChangeData.event, (result, current) => { + const onDidChangeData = Event.debounce, { message: boolean; elements: (T | Root)[] }>(this._onDidChangeData.event, (result, current) => { if (!result) { result = { message: false, elements: [] }; } @@ -379,7 +380,8 @@ class ExtHostTreeView extends Disposable { result.message = true; } return result; - }, 200, true)(({ message, elements }) => { + }, 200, true); + this._register(onDidChangeData(({ message, elements }) => { if (elements.length) { this.refreshQueue = this.refreshQueue.then(() => { const _promiseCallback = promiseCallback; @@ -801,7 +803,7 @@ class ExtHostTreeView extends Disposable { private createTreeNode(element: T, extensionTreeItem: vscode.TreeItem, parent: TreeNode | Root): TreeNode { this.validateTreeItem(extensionTreeItem); - const disposableStore = new DisposableStore(); + const disposableStore = this._register(new DisposableStore()); const handle = this.createHandle(element, extensionTreeItem, parent); const icon = this.getLightIconPath(extensionTreeItem); const item: ITreeItem = { @@ -970,6 +972,7 @@ class ExtHostTreeView extends Disposable { } override dispose() { + super.dispose(); this._refreshCancellationSource.dispose(); this.clearAll(); diff --git a/src/vs/workbench/api/node/extHostTunnelService.ts b/src/vs/workbench/api/node/extHostTunnelService.ts index 561f731654d..2ab734a61a6 100644 --- a/src/vs/workbench/api/node/extHostTunnelService.ts +++ b/src/vs/workbench/api/node/extHostTunnelService.ts @@ -22,7 +22,7 @@ import { NodeRemoteTunnel } from 'vs/platform/tunnel/node/tunnelService'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { ExtHostTunnelService } from 'vs/workbench/api/common/extHostTunnelService'; -import { CandidatePort } from 'vs/workbench/services/remote/common/tunnelModel'; +import { CandidatePort, parseAddress } from 'vs/workbench/services/remote/common/tunnelModel'; import * as vscode from 'vscode'; export function getSockets(stdout: string): Record { @@ -349,7 +349,7 @@ export class NodeExtHostTunnelService extends ExtHostTunnelService { const disposeEmitter = new Emitter(); return { - localAddress: t.localAddress, + localAddress: parseAddress(t.localAddress) ?? t.localAddress, remoteAddress: { port: t.tunnelRemotePort, host: t.tunnelRemoteHost }, onDidDispose: disposeEmitter.event, dispose: () => { diff --git a/src/vs/workbench/api/test/browser/extHostApiCommands.test.ts b/src/vs/workbench/api/test/browser/extHostApiCommands.test.ts index ab55eb49740..35e7543d864 100644 --- a/src/vs/workbench/api/test/browser/extHostApiCommands.test.ts +++ b/src/vs/workbench/api/test/browser/extHostApiCommands.test.ts @@ -62,6 +62,7 @@ import { IExtHostTelemetry } from 'vs/workbench/api/common/extHostTelemetry'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { ensureFileSystemProviderError } from 'vs/platform/files/common/files'; function assertRejects(fn: () => Promise, message: string = 'Expected rejection') { return fn().then(() => assert.ok(false, message), _err => assert.ok(true)); @@ -203,6 +204,8 @@ suite('ExtHostLanguageFeatureCommands', function () { return rpcProtocol.sync(); }); + ensureFileSystemProviderError(); + // --- workspace symbols test('WorkspaceSymbols, invalid arguments', function () { diff --git a/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts b/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts index 8ba10f3b7fe..6e8a62e0ce9 100644 --- a/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts +++ b/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts @@ -19,6 +19,7 @@ import { IExtHostFileSystemInfo } from 'vs/workbench/api/common/extHostFileSyste import { FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; import { isLinux } from 'vs/base/common/platform'; import { IURITransformerService } from 'vs/workbench/api/common/extHostUriTransformerService'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('ExtHostConfiguration', function () { @@ -53,6 +54,8 @@ suite('ExtHostConfiguration', function () { }; } + const store = ensureNoDisposablesAreLeakedInTestSuite(); + test('getConfiguration fails regression test 1.7.1 -> 1.8 #15552', function () { const extHostConfig = createExtHostConfiguration({ 'search': { @@ -742,7 +745,7 @@ suite('ExtHostConfiguration', function () { } }); const configEventData: IConfigurationChange = { keys: ['farboo.updatedConfig', 'farboo.newConfig'], overrides: [] }; - testObject.onDidChangeConfiguration(e => { + store.add(testObject.onDidChangeConfiguration(e => { assert.deepStrictEqual(testObject.getConfiguration().get('farboo'), { 'config': false, @@ -766,7 +769,7 @@ suite('ExtHostConfiguration', function () { assert.ok(!e.affectsConfiguration('farboo.config', workspaceFolder.uri)); assert.ok(!e.affectsConfiguration('farboo.config', URI.file('any'))); done(); - }); + })); testObject.$acceptConfigurationChanged(newConfigData, configEventData); }); diff --git a/src/vs/workbench/api/test/browser/extHostDecorations.test.ts b/src/vs/workbench/api/test/browser/extHostDecorations.test.ts index 9666ee3e640..26b419f6e06 100644 --- a/src/vs/workbench/api/test/browser/extHostDecorations.test.ts +++ b/src/vs/workbench/api/test/browser/extHostDecorations.test.ts @@ -8,6 +8,7 @@ import { timeout } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { NullLogService } from 'vs/platform/log/common/log'; import { MainThreadDecorationsShape } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostDecorations } from 'vs/workbench/api/common/extHostDecorations'; @@ -20,6 +21,8 @@ suite('ExtHostDecorations', function () { let extHostDecorations: ExtHostDecorations; const providers = new Set(); + ensureNoDisposablesAreLeakedInTestSuite(); + setup(function () { providers.clear(); @@ -79,6 +82,9 @@ suite('ExtHostDecorations', function () { const secondResult = await Promise.race([second, timeout(30).then(() => false)]); assert.strictEqual(typeof secondResult, 'object'); + + + await timeout(30); }); }); diff --git a/src/vs/workbench/api/test/browser/extHostDiagnostics.test.ts b/src/vs/workbench/api/test/browser/extHostDiagnostics.test.ts index b6bf3ee4406..6c6de8c7e17 100644 --- a/src/vs/workbench/api/test/browser/extHostDiagnostics.test.ts +++ b/src/vs/workbench/api/test/browser/extHostDiagnostics.test.ts @@ -18,6 +18,7 @@ import { ExtUri, extUri } from 'vs/base/common/resources'; import { IExtHostFileSystemInfo } from 'vs/workbench/api/common/extHostFileSystemInfo'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; import { IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('ExtHostDiagnostics', () => { @@ -38,6 +39,8 @@ suite('ExtHostDiagnostics', () => { return undefined; }; + const store = ensureNoDisposablesAreLeakedInTestSuite(); + test('disposeCheck', () => { const collection = new DiagnosticCollection('test', 'test', 100, 100, versionProvider, extUri, new DiagnosticsShape(), new Emitter()); @@ -208,7 +211,7 @@ suite('ExtHostDiagnostics', () => { let eventCount = 0; const emitter = new Emitter(); - emitter.event(_ => eventCount += 1); + store.add(emitter.event(_ => eventCount += 1)); const collection = new DiagnosticCollection('test', 'test', 100, 100, versionProvider, extUri, new class extends DiagnosticsShape { override $changeMany() { changeCount += 1; @@ -225,6 +228,7 @@ suite('ExtHostDiagnostics', () => { collection.set(uri, [diag]); assert.strictEqual(changeCount, 2); assert.strictEqual(eventCount, 2); + }); test('diagnostics collection, tuples and undefined (small array), #15585', function () { diff --git a/src/vs/workbench/api/test/browser/extHostDocumentData.test.ts b/src/vs/workbench/api/test/browser/extHostDocumentData.test.ts index 638cce74296..b8912834f80 100644 --- a/src/vs/workbench/api/test/browser/extHostDocumentData.test.ts +++ b/src/vs/workbench/api/test/browser/extHostDocumentData.test.ts @@ -13,6 +13,7 @@ import { IModelChangedEvent } from 'vs/editor/common/model/mirrorTextModel'; import { mock } from 'vs/base/test/common/mock'; import * as perfData from './extHostDocumentData.test.perf-data'; import { setDefaultGetWordAtTextConfig } from 'vs/editor/common/core/wordHelper'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('ExtHostDocumentData', () => { @@ -39,6 +40,8 @@ suite('ExtHostDocumentData', () => { ], '\n', 1, 'text', false); }); + ensureNoDisposablesAreLeakedInTestSuite(); + test('readonly-ness', () => { assert.throws(() => (data as any).document.uri = null); assert.throws(() => (data as any).document.fileName = 'foofile'); diff --git a/src/vs/workbench/api/test/browser/extHostDocumentsAndEditors.test.ts b/src/vs/workbench/api/test/browser/extHostDocumentsAndEditors.test.ts index 22e8c4a4a11..3f4255c49c8 100644 --- a/src/vs/workbench/api/test/browser/extHostDocumentsAndEditors.test.ts +++ b/src/vs/workbench/api/test/browser/extHostDocumentsAndEditors.test.ts @@ -8,6 +8,7 @@ import { URI } from 'vs/base/common/uri'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { TestRPCProtocol } from 'vs/workbench/api/test/common/testRPCProtocol'; import { NullLogService } from 'vs/platform/log/common/log'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('ExtHostDocumentsAndEditors', () => { @@ -17,6 +18,8 @@ suite('ExtHostDocumentsAndEditors', () => { editors = new ExtHostDocumentsAndEditors(new TestRPCProtocol(), new NullLogService()); }); + ensureNoDisposablesAreLeakedInTestSuite(); + test('The value of TextDocument.isClosed is incorrect when a text document is closed, #27949', () => { editors.$acceptDocumentsAndEditorsDelta({ @@ -35,7 +38,7 @@ suite('ExtHostDocumentsAndEditors', () => { return new Promise((resolve, reject) => { - editors.onDidRemoveDocuments(e => { + const d = editors.onDidRemoveDocuments(e => { try { for (const data of e) { @@ -44,6 +47,8 @@ suite('ExtHostDocumentsAndEditors', () => { resolve(undefined); } catch (e) { reject(e); + } finally { + d.dispose(); } }); diff --git a/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts b/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts index 39bf2306faa..cedfaa5e426 100644 --- a/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts +++ b/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts @@ -11,6 +11,7 @@ import { IEditorTabDto, IEditorTabGroupDto, MainThreadEditorTabsShape, TabInputK import { ExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs'; import { SingleProxyRPCProtocol } from 'vs/workbench/api/test/common/testRPCProtocol'; import { TextMergeTabInput, TextTabInput } from 'vs/workbench/api/common/extHostTypes'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('ExtHostEditorTabs', function () { @@ -28,6 +29,8 @@ suite('ExtHostEditorTabs', function () { return { ...defaultTabDto, ...dto }; } + const store = ensureNoDisposablesAreLeakedInTestSuite(); + test('Ensure empty model throws when accessing active group', function () { const extHostEditorTabs = new ExtHostEditorTabs( SingleProxyRPCProtocol(new class extends mock() { @@ -109,7 +112,7 @@ suite('ExtHostEditorTabs', function () { ); let count = 0; - extHostEditorTabs.tabGroups.onDidChangeTabGroups(() => count++); + store.add(extHostEditorTabs.tabGroups.onDidChangeTabGroups(() => count++)); assert.strictEqual(count, 0); @@ -142,7 +145,7 @@ suite('ExtHostEditorTabs', function () { const group2Data: IEditorTabGroupDto = { ...group1Data, groupId: 13 }; const events: vscode.TabGroupChangeEvent[] = []; - extHostEditorTabs.tabGroups.onDidChangeTabGroups(e => events.push(e)); + store.add(extHostEditorTabs.tabGroups.onDidChangeTabGroups(e => events.push(e))); // OPEN extHostEditorTabs.$acceptEditorTabModel([group1Data]); assert.deepStrictEqual(events, [{ @@ -446,7 +449,8 @@ suite('ExtHostEditorTabs', function () { const tab = extHostEditorTabs.tabGroups.all[0].tabs[0]; - const p = new Promise(resolve => extHostEditorTabs.tabGroups.onDidChangeTabs(resolve)); + + const p = new Promise(resolve => store.add(extHostEditorTabs.tabGroups.onDidChangeTabs(resolve))); extHostEditorTabs.$acceptTabOperation({ groupId: 12, diff --git a/src/vs/workbench/api/test/browser/extHostFileSystemEventService.test.ts b/src/vs/workbench/api/test/browser/extHostFileSystemEventService.test.ts index 64d35e85b79..5063354ee59 100644 --- a/src/vs/workbench/api/test/browser/extHostFileSystemEventService.test.ts +++ b/src/vs/workbench/api/test/browser/extHostFileSystemEventService.test.ts @@ -6,9 +6,12 @@ import * as assert from 'assert'; import { ExtHostFileSystemEventService } from 'vs/workbench/api/common/extHostFileSystemEventService'; import { IMainContext } from 'vs/workbench/api/common/extHost.protocol'; import { NullLogService } from 'vs/platform/log/common/log'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('ExtHostFileSystemEventService', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + test('FileSystemWatcher ignore events properties are reversed #26851', function () { const protocol: IMainContext = { @@ -23,11 +26,13 @@ suite('ExtHostFileSystemEventService', () => { assert.strictEqual(watcher1.ignoreChangeEvents, false); assert.strictEqual(watcher1.ignoreCreateEvents, false); assert.strictEqual(watcher1.ignoreDeleteEvents, false); + watcher1.dispose(); const watcher2 = new ExtHostFileSystemEventService(protocol, new NullLogService(), undefined!).createFileSystemWatcher(undefined!, undefined!, '**/somethingBoring', true, true, true); assert.strictEqual(watcher2.ignoreChangeEvents, true); assert.strictEqual(watcher2.ignoreCreateEvents, true); assert.strictEqual(watcher2.ignoreDeleteEvents, true); + watcher2.dispose(); }); }); diff --git a/src/vs/workbench/api/test/browser/extHostLanguageFeatures.test.ts b/src/vs/workbench/api/test/browser/extHostLanguageFeatures.test.ts index dc5f1061385..47646595113 100644 --- a/src/vs/workbench/api/test/browser/extHostLanguageFeatures.test.ts +++ b/src/vs/workbench/api/test/browser/extHostLanguageFeatures.test.ts @@ -44,7 +44,7 @@ import { nullExtensionDescription as defaultExtension } from 'vs/workbench/servi import { provideSelectionRanges } from 'vs/editor/contrib/smartSelect/browser/smartSelect'; import { mock } from 'vs/base/test/common/mock'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; -import { dispose } from 'vs/base/common/lifecycle'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { NullApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; import { Progress } from 'vs/platform/progress/common/progress'; import { IExtHostFileSystemInfo } from 'vs/workbench/api/common/extHostFileSystemInfo'; @@ -55,6 +55,7 @@ import { LanguageFeaturesService } from 'vs/editor/common/services/languageFeatu import { CodeActionTriggerSource } from 'vs/editor/contrib/codeAction/common/types'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IExtHostTelemetry } from 'vs/workbench/api/common/extHostTelemetry'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('ExtHostLanguageFeatures', function () { @@ -62,13 +63,13 @@ suite('ExtHostLanguageFeatures', function () { let model: ITextModel; let extHost: ExtHostLanguageFeatures; let mainThread: MainThreadLanguageFeatures; - let disposables: vscode.Disposable[] = []; + const disposables = new DisposableStore(); let rpcProtocol: TestRPCProtocol; let languageFeaturesService: ILanguageFeaturesService; let originalErrorHandler: (e: any) => any; let instantiationService: TestInstantiationService; - suiteSetup(() => { + setup(() => { model = createTextModel( [ @@ -121,7 +122,7 @@ suite('ExtHostLanguageFeatures', function () { } }); rpcProtocol.set(ExtHostContext.ExtHostCommands, commands); - rpcProtocol.set(MainContext.MainThreadCommands, inst.createInstance(MainThreadCommands, rpcProtocol)); + rpcProtocol.set(MainContext.MainThreadCommands, disposables.add(inst.createInstance(MainThreadCommands, rpcProtocol))); const diagnostics = new ExtHostDiagnostics(rpcProtocol, new NullLogService(), new class extends mock() { }, extHostDocumentsAndEditors); rpcProtocol.set(ExtHostContext.ExtHostDiagnostics, diagnostics); @@ -133,21 +134,22 @@ suite('ExtHostLanguageFeatures', function () { }); rpcProtocol.set(ExtHostContext.ExtHostLanguageFeatures, extHost); - mainThread = rpcProtocol.set(MainContext.MainThreadLanguageFeatures, inst.createInstance(MainThreadLanguageFeatures, rpcProtocol)); + mainThread = rpcProtocol.set(MainContext.MainThreadLanguageFeatures, disposables.add(inst.createInstance(MainThreadLanguageFeatures, rpcProtocol))); }); - suiteTeardown(() => { + teardown(() => { + disposables.clear(); + setUnexpectedErrorHandler(originalErrorHandler); model.dispose(); mainThread.dispose(); instantiationService.dispose(); - }); - teardown(() => { - disposables = dispose(disposables); return rpcProtocol.sync(); }); + ensureNoDisposablesAreLeakedInTestSuite(); + // --- outline test('DocumentSymbols, register/deregister', async () => { @@ -166,12 +168,12 @@ suite('ExtHostLanguageFeatures', function () { }); test('DocumentSymbols, evil provider', async () => { - disposables.push(extHost.registerDocumentSymbolProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentSymbolProvider { + disposables.add(extHost.registerDocumentSymbolProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentSymbolProvider { provideDocumentSymbols(): any { throw new Error('evil document symbol provider'); } })); - disposables.push(extHost.registerDocumentSymbolProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentSymbolProvider { + disposables.add(extHost.registerDocumentSymbolProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentSymbolProvider { provideDocumentSymbols(): any { return [new types.SymbolInformation('test', types.SymbolKind.Field, new types.Range(0, 0, 0, 0))]; } @@ -183,7 +185,7 @@ suite('ExtHostLanguageFeatures', function () { }); test('DocumentSymbols, data conversion', async () => { - disposables.push(extHost.registerDocumentSymbolProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentSymbolProvider { + disposables.add(extHost.registerDocumentSymbolProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentSymbolProvider { provideDocumentSymbols(): any { return [new types.SymbolInformation('test', types.SymbolKind.Field, new types.Range(0, 0, 0, 0))]; } @@ -207,7 +209,7 @@ suite('ExtHostLanguageFeatures', function () { { name: 'containerPort', range: { startLineNumber: 4, startColumn: 9, endLineNumber: 4, endColumn: 26 } } ]; - extHost.registerDocumentSymbolProvider(defaultExtension, defaultSelector, { + disposables.add(extHost.registerDocumentSymbolProvider(defaultExtension, defaultSelector, { provideDocumentSymbols: (doc, token): any => { return symbols.map(s => { return new types.SymbolInformation( @@ -217,7 +219,7 @@ suite('ExtHostLanguageFeatures', function () { ); }); } - }); + })); await rpcProtocol.sync(); @@ -231,12 +233,12 @@ suite('ExtHostLanguageFeatures', function () { test('CodeLens, evil provider', async () => { - disposables.push(extHost.registerCodeLensProvider(defaultExtension, defaultSelector, new class implements vscode.CodeLensProvider { + disposables.add(extHost.registerCodeLensProvider(defaultExtension, defaultSelector, new class implements vscode.CodeLensProvider { provideCodeLenses(): any { throw new Error('evil'); } })); - disposables.push(extHost.registerCodeLensProvider(defaultExtension, defaultSelector, new class implements vscode.CodeLensProvider { + disposables.add(extHost.registerCodeLensProvider(defaultExtension, defaultSelector, new class implements vscode.CodeLensProvider { provideCodeLenses() { return [new types.CodeLens(new types.Range(0, 0, 0, 0))]; } @@ -245,11 +247,12 @@ suite('ExtHostLanguageFeatures', function () { await rpcProtocol.sync(); const value = await getCodeLensModel(languageFeaturesService.codeLensProvider, model, CancellationToken.None); assert.strictEqual(value.lenses.length, 1); + value.dispose(); }); test('CodeLens, do not resolve a resolved lens', async () => { - disposables.push(extHost.registerCodeLensProvider(defaultExtension, defaultSelector, new class implements vscode.CodeLensProvider { + disposables.add(extHost.registerCodeLensProvider(defaultExtension, defaultSelector, new class implements vscode.CodeLensProvider { provideCodeLenses(): any { return [new types.CodeLens( new types.Range(0, 0, 0, 0), @@ -267,11 +270,12 @@ suite('ExtHostLanguageFeatures', function () { const symbol = await Promise.resolve(data.provider.resolveCodeLens!(model, data.symbol, CancellationToken.None)); assert.strictEqual(symbol!.command!.id, 'id'); assert.strictEqual(symbol!.command!.title, 'Title'); + value.dispose(); }); test('CodeLens, missing command', async () => { - disposables.push(extHost.registerCodeLensProvider(defaultExtension, defaultSelector, new class implements vscode.CodeLensProvider { + disposables.add(extHost.registerCodeLensProvider(defaultExtension, defaultSelector, new class implements vscode.CodeLensProvider { provideCodeLenses() { return [new types.CodeLens(new types.Range(0, 0, 0, 0))]; } @@ -284,13 +288,14 @@ suite('ExtHostLanguageFeatures', function () { const symbol = await Promise.resolve(data.provider.resolveCodeLens!(model, data.symbol, CancellationToken.None)); assert.strictEqual(symbol!.command!.id, 'missing'); assert.strictEqual(symbol!.command!.title, '!!MISSING: command!!'); + value.dispose(); }); // --- definition test('Definition, data conversion', async () => { - disposables.push(extHost.registerDefinitionProvider(defaultExtension, defaultSelector, new class implements vscode.DefinitionProvider { + disposables.add(extHost.registerDefinitionProvider(defaultExtension, defaultSelector, new class implements vscode.DefinitionProvider { provideDefinition(): any { return [new types.Location(model.uri, new types.Range(1, 2, 3, 4))]; } @@ -306,12 +311,12 @@ suite('ExtHostLanguageFeatures', function () { test('Definition, one or many', async () => { - disposables.push(extHost.registerDefinitionProvider(defaultExtension, defaultSelector, new class implements vscode.DefinitionProvider { + disposables.add(extHost.registerDefinitionProvider(defaultExtension, defaultSelector, new class implements vscode.DefinitionProvider { provideDefinition(): any { return [new types.Location(model.uri, new types.Range(1, 1, 1, 1))]; } })); - disposables.push(extHost.registerDefinitionProvider(defaultExtension, defaultSelector, new class implements vscode.DefinitionProvider { + disposables.add(extHost.registerDefinitionProvider(defaultExtension, defaultSelector, new class implements vscode.DefinitionProvider { provideDefinition(): any { return new types.Location(model.uri, new types.Range(2, 1, 1, 1)); } @@ -324,13 +329,13 @@ suite('ExtHostLanguageFeatures', function () { test('Definition, registration order', async () => { - disposables.push(extHost.registerDefinitionProvider(defaultExtension, defaultSelector, new class implements vscode.DefinitionProvider { + disposables.add(extHost.registerDefinitionProvider(defaultExtension, defaultSelector, new class implements vscode.DefinitionProvider { provideDefinition(): any { return [new types.Location(URI.parse('far://first'), new types.Range(2, 3, 4, 5))]; } })); - disposables.push(extHost.registerDefinitionProvider(defaultExtension, defaultSelector, new class implements vscode.DefinitionProvider { + disposables.add(extHost.registerDefinitionProvider(defaultExtension, defaultSelector, new class implements vscode.DefinitionProvider { provideDefinition(): any { return new types.Location(URI.parse('far://second'), new types.Range(1, 2, 3, 4)); } @@ -346,12 +351,12 @@ suite('ExtHostLanguageFeatures', function () { test('Definition, evil provider', async () => { - disposables.push(extHost.registerDefinitionProvider(defaultExtension, defaultSelector, new class implements vscode.DefinitionProvider { + disposables.add(extHost.registerDefinitionProvider(defaultExtension, defaultSelector, new class implements vscode.DefinitionProvider { provideDefinition(): any { throw new Error('evil provider'); } })); - disposables.push(extHost.registerDefinitionProvider(defaultExtension, defaultSelector, new class implements vscode.DefinitionProvider { + disposables.add(extHost.registerDefinitionProvider(defaultExtension, defaultSelector, new class implements vscode.DefinitionProvider { provideDefinition(): any { return new types.Location(model.uri, new types.Range(1, 1, 1, 1)); } @@ -366,7 +371,7 @@ suite('ExtHostLanguageFeatures', function () { test('Declaration, data conversion', async () => { - disposables.push(extHost.registerDeclarationProvider(defaultExtension, defaultSelector, new class implements vscode.DeclarationProvider { + disposables.add(extHost.registerDeclarationProvider(defaultExtension, defaultSelector, new class implements vscode.DeclarationProvider { provideDeclaration(): any { return [new types.Location(model.uri, new types.Range(1, 2, 3, 4))]; } @@ -384,7 +389,7 @@ suite('ExtHostLanguageFeatures', function () { test('Implementation, data conversion', async () => { - disposables.push(extHost.registerImplementationProvider(defaultExtension, defaultSelector, new class implements vscode.ImplementationProvider { + disposables.add(extHost.registerImplementationProvider(defaultExtension, defaultSelector, new class implements vscode.ImplementationProvider { provideImplementation(): any { return [new types.Location(model.uri, new types.Range(1, 2, 3, 4))]; } @@ -402,7 +407,7 @@ suite('ExtHostLanguageFeatures', function () { test('Type Definition, data conversion', async () => { - disposables.push(extHost.registerTypeDefinitionProvider(defaultExtension, defaultSelector, new class implements vscode.TypeDefinitionProvider { + disposables.add(extHost.registerTypeDefinitionProvider(defaultExtension, defaultSelector, new class implements vscode.TypeDefinitionProvider { provideTypeDefinition(): any { return [new types.Location(model.uri, new types.Range(1, 2, 3, 4))]; } @@ -420,7 +425,7 @@ suite('ExtHostLanguageFeatures', function () { test('HoverProvider, word range at pos', async () => { - disposables.push(extHost.registerHoverProvider(defaultExtension, defaultSelector, new class implements vscode.HoverProvider { + disposables.add(extHost.registerHoverProvider(defaultExtension, defaultSelector, new class implements vscode.HoverProvider { provideHover(): any { return new types.Hover('Hello'); } @@ -436,7 +441,7 @@ suite('ExtHostLanguageFeatures', function () { test('HoverProvider, given range', async () => { - disposables.push(extHost.registerHoverProvider(defaultExtension, defaultSelector, new class implements vscode.HoverProvider { + disposables.add(extHost.registerHoverProvider(defaultExtension, defaultSelector, new class implements vscode.HoverProvider { provideHover(): any { return new types.Hover('Hello', new types.Range(3, 0, 8, 7)); } @@ -451,14 +456,14 @@ suite('ExtHostLanguageFeatures', function () { test('HoverProvider, registration order', async () => { - disposables.push(extHost.registerHoverProvider(defaultExtension, defaultSelector, new class implements vscode.HoverProvider { + disposables.add(extHost.registerHoverProvider(defaultExtension, defaultSelector, new class implements vscode.HoverProvider { provideHover(): any { return new types.Hover('registered first'); } })); - disposables.push(extHost.registerHoverProvider(defaultExtension, defaultSelector, new class implements vscode.HoverProvider { + disposables.add(extHost.registerHoverProvider(defaultExtension, defaultSelector, new class implements vscode.HoverProvider { provideHover(): any { return new types.Hover('registered second'); } @@ -475,12 +480,12 @@ suite('ExtHostLanguageFeatures', function () { test('HoverProvider, evil provider', async () => { - disposables.push(extHost.registerHoverProvider(defaultExtension, defaultSelector, new class implements vscode.HoverProvider { + disposables.add(extHost.registerHoverProvider(defaultExtension, defaultSelector, new class implements vscode.HoverProvider { provideHover(): any { throw new Error('evil'); } })); - disposables.push(extHost.registerHoverProvider(defaultExtension, defaultSelector, new class implements vscode.HoverProvider { + disposables.add(extHost.registerHoverProvider(defaultExtension, defaultSelector, new class implements vscode.HoverProvider { provideHover(): any { return new types.Hover('Hello'); } @@ -495,7 +500,7 @@ suite('ExtHostLanguageFeatures', function () { test('Occurrences, data conversion', async () => { - disposables.push(extHost.registerDocumentHighlightProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentHighlightProvider { + disposables.add(extHost.registerDocumentHighlightProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentHighlightProvider { provideDocumentHighlights(): any { return [new types.DocumentHighlight(new types.Range(0, 0, 0, 4))]; } @@ -511,12 +516,12 @@ suite('ExtHostLanguageFeatures', function () { test('Occurrences, order 1/2', async () => { - disposables.push(extHost.registerDocumentHighlightProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentHighlightProvider { + disposables.add(extHost.registerDocumentHighlightProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentHighlightProvider { provideDocumentHighlights(): any { return []; } })); - disposables.push(extHost.registerDocumentHighlightProvider(defaultExtension, '*', new class implements vscode.DocumentHighlightProvider { + disposables.add(extHost.registerDocumentHighlightProvider(defaultExtension, '*', new class implements vscode.DocumentHighlightProvider { provideDocumentHighlights(): any { return [new types.DocumentHighlight(new types.Range(0, 0, 0, 4))]; } @@ -532,12 +537,12 @@ suite('ExtHostLanguageFeatures', function () { test('Occurrences, order 2/2', async () => { - disposables.push(extHost.registerDocumentHighlightProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentHighlightProvider { + disposables.add(extHost.registerDocumentHighlightProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentHighlightProvider { provideDocumentHighlights(): any { return [new types.DocumentHighlight(new types.Range(0, 0, 0, 2))]; } })); - disposables.push(extHost.registerDocumentHighlightProvider(defaultExtension, '*', new class implements vscode.DocumentHighlightProvider { + disposables.add(extHost.registerDocumentHighlightProvider(defaultExtension, '*', new class implements vscode.DocumentHighlightProvider { provideDocumentHighlights(): any { return [new types.DocumentHighlight(new types.Range(0, 0, 0, 4))]; } @@ -553,13 +558,13 @@ suite('ExtHostLanguageFeatures', function () { test('Occurrences, evil provider', async () => { - disposables.push(extHost.registerDocumentHighlightProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentHighlightProvider { + disposables.add(extHost.registerDocumentHighlightProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentHighlightProvider { provideDocumentHighlights(): any { throw new Error('evil'); } })); - disposables.push(extHost.registerDocumentHighlightProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentHighlightProvider { + disposables.add(extHost.registerDocumentHighlightProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentHighlightProvider { provideDocumentHighlights(): any { return [new types.DocumentHighlight(new types.Range(0, 0, 0, 4))]; } @@ -574,13 +579,13 @@ suite('ExtHostLanguageFeatures', function () { test('References, registration order', async () => { - disposables.push(extHost.registerReferenceProvider(defaultExtension, defaultSelector, new class implements vscode.ReferenceProvider { + disposables.add(extHost.registerReferenceProvider(defaultExtension, defaultSelector, new class implements vscode.ReferenceProvider { provideReferences(): any { return [new types.Location(URI.parse('far://register/first'), new types.Range(0, 0, 0, 0))]; } })); - disposables.push(extHost.registerReferenceProvider(defaultExtension, defaultSelector, new class implements vscode.ReferenceProvider { + disposables.add(extHost.registerReferenceProvider(defaultExtension, defaultSelector, new class implements vscode.ReferenceProvider { provideReferences(): any { return [new types.Location(URI.parse('far://register/second'), new types.Range(0, 0, 0, 0))]; } @@ -596,7 +601,7 @@ suite('ExtHostLanguageFeatures', function () { test('References, data conversion', async () => { - disposables.push(extHost.registerReferenceProvider(defaultExtension, defaultSelector, new class implements vscode.ReferenceProvider { + disposables.add(extHost.registerReferenceProvider(defaultExtension, defaultSelector, new class implements vscode.ReferenceProvider { provideReferences(): any { return [new types.Location(model.uri, new types.Position(0, 0))]; } @@ -612,12 +617,12 @@ suite('ExtHostLanguageFeatures', function () { test('References, evil provider', async () => { - disposables.push(extHost.registerReferenceProvider(defaultExtension, defaultSelector, new class implements vscode.ReferenceProvider { + disposables.add(extHost.registerReferenceProvider(defaultExtension, defaultSelector, new class implements vscode.ReferenceProvider { provideReferences(): any { throw new Error('evil'); } })); - disposables.push(extHost.registerReferenceProvider(defaultExtension, defaultSelector, new class implements vscode.ReferenceProvider { + disposables.add(extHost.registerReferenceProvider(defaultExtension, defaultSelector, new class implements vscode.ReferenceProvider { provideReferences(): any { return [new types.Location(model.uri, new types.Range(0, 0, 0, 0))]; } @@ -632,7 +637,7 @@ suite('ExtHostLanguageFeatures', function () { test('Quick Fix, command data conversion', async () => { - disposables.push(extHost.registerCodeActionProvider(defaultExtension, defaultSelector, { + disposables.add(extHost.registerCodeActionProvider(defaultExtension, defaultSelector, { provideCodeActions(): vscode.Command[] { return [ { command: 'test1', title: 'Testing1' }, @@ -642,18 +647,20 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const { validActions: actions } = await getCodeActions(languageFeaturesService.codeActionProvider, model, model.getFullModelRange(), { type: languages.CodeActionTriggerType.Invoke, triggerAction: CodeActionTriggerSource.QuickFix }, Progress.None, CancellationToken.None); + const value = await getCodeActions(languageFeaturesService.codeActionProvider, model, model.getFullModelRange(), { type: languages.CodeActionTriggerType.Invoke, triggerAction: CodeActionTriggerSource.QuickFix }, Progress.None, CancellationToken.None); + const { validActions: actions } = value; assert.strictEqual(actions.length, 2); const [first, second] = actions; assert.strictEqual(first.action.title, 'Testing1'); assert.strictEqual(first.action.command!.id, 'test1'); assert.strictEqual(second.action.title, 'Testing2'); assert.strictEqual(second.action.command!.id, 'test2'); + value.dispose(); }); test('Quick Fix, code action data conversion', async () => { - disposables.push(extHost.registerCodeActionProvider(defaultExtension, defaultSelector, { + disposables.add(extHost.registerCodeActionProvider(defaultExtension, defaultSelector, { provideCodeActions(): vscode.CodeAction[] { return [ { @@ -666,19 +673,21 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const { validActions: actions } = await getCodeActions(languageFeaturesService.codeActionProvider, model, model.getFullModelRange(), { type: languages.CodeActionTriggerType.Invoke, triggerAction: CodeActionTriggerSource.Default }, Progress.None, CancellationToken.None); + const value = await getCodeActions(languageFeaturesService.codeActionProvider, model, model.getFullModelRange(), { type: languages.CodeActionTriggerType.Invoke, triggerAction: CodeActionTriggerSource.Default }, Progress.None, CancellationToken.None); + const { validActions: actions } = value; assert.strictEqual(actions.length, 1); const [first] = actions; assert.strictEqual(first.action.title, 'Testing1'); assert.strictEqual(first.action.command!.title, 'Testing1Command'); assert.strictEqual(first.action.command!.id, 'test1'); assert.strictEqual(first.action.kind, 'test.scope'); + value.dispose(); }); test('Cannot read property \'id\' of undefined, #29469', async () => { - disposables.push(extHost.registerCodeActionProvider(defaultExtension, defaultSelector, new class implements vscode.CodeActionProvider { + disposables.add(extHost.registerCodeActionProvider(defaultExtension, defaultSelector, new class implements vscode.CodeActionProvider { provideCodeActions(): any { return [ undefined, @@ -689,39 +698,43 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const { validActions: actions } = await getCodeActions(languageFeaturesService.codeActionProvider, model, model.getFullModelRange(), { type: languages.CodeActionTriggerType.Invoke, triggerAction: CodeActionTriggerSource.Default }, Progress.None, CancellationToken.None); + const value = await getCodeActions(languageFeaturesService.codeActionProvider, model, model.getFullModelRange(), { type: languages.CodeActionTriggerType.Invoke, triggerAction: CodeActionTriggerSource.Default }, Progress.None, CancellationToken.None); + const { validActions: actions } = value; assert.strictEqual(actions.length, 1); + value.dispose(); }); test('Quick Fix, evil provider', async () => { - disposables.push(extHost.registerCodeActionProvider(defaultExtension, defaultSelector, new class implements vscode.CodeActionProvider { + disposables.add(extHost.registerCodeActionProvider(defaultExtension, defaultSelector, new class implements vscode.CodeActionProvider { provideCodeActions(): any { throw new Error('evil'); } })); - disposables.push(extHost.registerCodeActionProvider(defaultExtension, defaultSelector, new class implements vscode.CodeActionProvider { + disposables.add(extHost.registerCodeActionProvider(defaultExtension, defaultSelector, new class implements vscode.CodeActionProvider { provideCodeActions(): any { return [{ command: 'test', title: 'Testing' }]; } })); await rpcProtocol.sync(); - const { validActions: actions } = await getCodeActions(languageFeaturesService.codeActionProvider, model, model.getFullModelRange(), { type: languages.CodeActionTriggerType.Invoke, triggerAction: CodeActionTriggerSource.QuickFix }, Progress.None, CancellationToken.None); + const value = await getCodeActions(languageFeaturesService.codeActionProvider, model, model.getFullModelRange(), { type: languages.CodeActionTriggerType.Invoke, triggerAction: CodeActionTriggerSource.QuickFix }, Progress.None, CancellationToken.None); + const { validActions: actions } = value; assert.strictEqual(actions.length, 1); + value.dispose(); }); // --- navigate types test('Navigate types, evil provider', async () => { - disposables.push(extHost.registerWorkspaceSymbolProvider(defaultExtension, new class implements vscode.WorkspaceSymbolProvider { + disposables.add(extHost.registerWorkspaceSymbolProvider(defaultExtension, new class implements vscode.WorkspaceSymbolProvider { provideWorkspaceSymbols(): any { throw new Error('evil'); } })); - disposables.push(extHost.registerWorkspaceSymbolProvider(defaultExtension, new class implements vscode.WorkspaceSymbolProvider { + disposables.add(extHost.registerWorkspaceSymbolProvider(defaultExtension, new class implements vscode.WorkspaceSymbolProvider { provideWorkspaceSymbols(): any { return [new types.SymbolInformation('testing', types.SymbolKind.Array, new types.Range(0, 0, 1, 1))]; } @@ -736,19 +749,19 @@ suite('ExtHostLanguageFeatures', function () { test('Navigate types, de-duplicate results', async () => { const uri = URI.from({ scheme: 'foo', path: '/some/path' }); - disposables.push(extHost.registerWorkspaceSymbolProvider(defaultExtension, new class implements vscode.WorkspaceSymbolProvider { + disposables.add(extHost.registerWorkspaceSymbolProvider(defaultExtension, new class implements vscode.WorkspaceSymbolProvider { provideWorkspaceSymbols(): any { return [new types.SymbolInformation('ONE', types.SymbolKind.Array, undefined, new types.Location(uri, new types.Range(0, 0, 1, 1)))]; } })); - disposables.push(extHost.registerWorkspaceSymbolProvider(defaultExtension, new class implements vscode.WorkspaceSymbolProvider { + disposables.add(extHost.registerWorkspaceSymbolProvider(defaultExtension, new class implements vscode.WorkspaceSymbolProvider { provideWorkspaceSymbols(): any { return [new types.SymbolInformation('ONE', types.SymbolKind.Array, undefined, new types.Location(uri, new types.Range(0, 0, 1, 1)))]; // get de-duped } })); - disposables.push(extHost.registerWorkspaceSymbolProvider(defaultExtension, new class implements vscode.WorkspaceSymbolProvider { + disposables.add(extHost.registerWorkspaceSymbolProvider(defaultExtension, new class implements vscode.WorkspaceSymbolProvider { provideWorkspaceSymbols(): any { return [new types.SymbolInformation('ONE', types.SymbolKind.Array, undefined, new types.Location(uri, undefined!))]; // NO dedupe because of resolve } @@ -757,7 +770,7 @@ suite('ExtHostLanguageFeatures', function () { } })); - disposables.push(extHost.registerWorkspaceSymbolProvider(defaultExtension, new class implements vscode.WorkspaceSymbolProvider { + disposables.add(extHost.registerWorkspaceSymbolProvider(defaultExtension, new class implements vscode.WorkspaceSymbolProvider { provideWorkspaceSymbols(): any { return [new types.SymbolInformation('ONE', types.SymbolKind.Struct, undefined, new types.Location(uri, new types.Range(0, 0, 1, 1)))]; // NO dedupe because of kind } @@ -772,7 +785,7 @@ suite('ExtHostLanguageFeatures', function () { test('Rename, evil provider 0/2', async () => { - disposables.push(extHost.registerRenameProvider(defaultExtension, defaultSelector, new class implements vscode.RenameProvider { + disposables.add(extHost.registerRenameProvider(defaultExtension, defaultSelector, new class implements vscode.RenameProvider { provideRenameEdits(): any { throw new class Foo { }; } @@ -790,7 +803,7 @@ suite('ExtHostLanguageFeatures', function () { test('Rename, evil provider 1/2', async () => { - disposables.push(extHost.registerRenameProvider(defaultExtension, defaultSelector, new class implements vscode.RenameProvider { + disposables.add(extHost.registerRenameProvider(defaultExtension, defaultSelector, new class implements vscode.RenameProvider { provideRenameEdits(): any { throw Error('evil'); } @@ -803,13 +816,13 @@ suite('ExtHostLanguageFeatures', function () { test('Rename, evil provider 2/2', async () => { - disposables.push(extHost.registerRenameProvider(defaultExtension, '*', new class implements vscode.RenameProvider { + disposables.add(extHost.registerRenameProvider(defaultExtension, '*', new class implements vscode.RenameProvider { provideRenameEdits(): any { throw Error('evil'); } })); - disposables.push(extHost.registerRenameProvider(defaultExtension, defaultSelector, new class implements vscode.RenameProvider { + disposables.add(extHost.registerRenameProvider(defaultExtension, defaultSelector, new class implements vscode.RenameProvider { provideRenameEdits(): any { const edit = new types.WorkspaceEdit(); edit.replace(model.uri, new types.Range(0, 0, 0, 0), 'testing'); @@ -824,7 +837,7 @@ suite('ExtHostLanguageFeatures', function () { test('Rename, ordering', async () => { - disposables.push(extHost.registerRenameProvider(defaultExtension, '*', new class implements vscode.RenameProvider { + disposables.add(extHost.registerRenameProvider(defaultExtension, '*', new class implements vscode.RenameProvider { provideRenameEdits(): any { const edit = new types.WorkspaceEdit(); edit.replace(model.uri, new types.Range(0, 0, 0, 0), 'testing'); @@ -833,7 +846,7 @@ suite('ExtHostLanguageFeatures', function () { } })); - disposables.push(extHost.registerRenameProvider(defaultExtension, defaultSelector, new class implements vscode.RenameProvider { + disposables.add(extHost.registerRenameProvider(defaultExtension, defaultSelector, new class implements vscode.RenameProvider { provideRenameEdits(): any { return; } @@ -849,7 +862,7 @@ suite('ExtHostLanguageFeatures', function () { const called = [false, false, false, false]; - disposables.push(extHost.registerRenameProvider(defaultExtension, defaultSelector, new class implements vscode.RenameProvider { + disposables.add(extHost.registerRenameProvider(defaultExtension, defaultSelector, new class implements vscode.RenameProvider { prepareRename(document: vscode.TextDocument, position: vscode.Position,): vscode.ProviderResult { called[0] = true; const range = document.getWordRangeAtPosition(position); @@ -862,7 +875,7 @@ suite('ExtHostLanguageFeatures', function () { } })); - disposables.push(extHost.registerRenameProvider(defaultExtension, defaultSelector, new class implements vscode.RenameProvider { + disposables.add(extHost.registerRenameProvider(defaultExtension, defaultSelector, new class implements vscode.RenameProvider { prepareRename(document: vscode.TextDocument, position: vscode.Position,): vscode.ProviderResult { called[2] = true; return Promise.reject('Cannot rename this symbol2.'); @@ -883,7 +896,7 @@ suite('ExtHostLanguageFeatures', function () { const called = [false, false, false]; - disposables.push(extHost.registerRenameProvider(defaultExtension, defaultSelector, new class implements vscode.RenameProvider { + disposables.add(extHost.registerRenameProvider(defaultExtension, defaultSelector, new class implements vscode.RenameProvider { prepareRename(document: vscode.TextDocument, position: vscode.Position,): vscode.ProviderResult { called[0] = true; const range = document.getWordRangeAtPosition(position); @@ -896,7 +909,7 @@ suite('ExtHostLanguageFeatures', function () { } })); - disposables.push(extHost.registerRenameProvider(defaultExtension, defaultSelector, new class implements vscode.RenameProvider { + disposables.add(extHost.registerRenameProvider(defaultExtension, defaultSelector, new class implements vscode.RenameProvider { provideRenameEdits(document: vscode.TextDocument, position: vscode.Position, newName: string,): vscode.ProviderResult { called[2] = true; @@ -915,13 +928,13 @@ suite('ExtHostLanguageFeatures', function () { test('Parameter Hints, order', async () => { - disposables.push(extHost.registerSignatureHelpProvider(defaultExtension, defaultSelector, new class implements vscode.SignatureHelpProvider { + disposables.add(extHost.registerSignatureHelpProvider(defaultExtension, defaultSelector, new class implements vscode.SignatureHelpProvider { provideSignatureHelp(): any { return undefined; } }, [])); - disposables.push(extHost.registerSignatureHelpProvider(defaultExtension, defaultSelector, new class implements vscode.SignatureHelpProvider { + disposables.add(extHost.registerSignatureHelpProvider(defaultExtension, defaultSelector, new class implements vscode.SignatureHelpProvider { provideSignatureHelp(): vscode.SignatureHelp { return { signatures: [], @@ -938,7 +951,7 @@ suite('ExtHostLanguageFeatures', function () { test('Parameter Hints, evil provider', async () => { - disposables.push(extHost.registerSignatureHelpProvider(defaultExtension, defaultSelector, new class implements vscode.SignatureHelpProvider { + disposables.add(extHost.registerSignatureHelpProvider(defaultExtension, defaultSelector, new class implements vscode.SignatureHelpProvider { provideSignatureHelp(): any { throw new Error('evil'); } @@ -953,74 +966,77 @@ suite('ExtHostLanguageFeatures', function () { test('Suggest, order 1/3', async () => { - disposables.push(extHost.registerCompletionItemProvider(defaultExtension, '*', new class implements vscode.CompletionItemProvider { + disposables.add(extHost.registerCompletionItemProvider(defaultExtension, '*', new class implements vscode.CompletionItemProvider { provideCompletionItems(): any { return [new types.CompletionItem('testing1')]; } }, [])); - disposables.push(extHost.registerCompletionItemProvider(defaultExtension, defaultSelector, new class implements vscode.CompletionItemProvider { + disposables.add(extHost.registerCompletionItemProvider(defaultExtension, defaultSelector, new class implements vscode.CompletionItemProvider { provideCompletionItems(): any { return [new types.CompletionItem('testing2')]; } }, [])); await rpcProtocol.sync(); - const { items } = await provideSuggestionItems(languageFeaturesService.completionProvider, model, new EditorPosition(1, 1), new CompletionOptions(undefined, new Set().add(languages.CompletionItemKind.Snippet))); - assert.strictEqual(items.length, 1); - assert.strictEqual(items[0].completion.insertText, 'testing2'); + const value = await provideSuggestionItems(languageFeaturesService.completionProvider, model, new EditorPosition(1, 1), new CompletionOptions(undefined, new Set().add(languages.CompletionItemKind.Snippet))); + assert.strictEqual(value.items.length, 1); + assert.strictEqual(value.items[0].completion.insertText, 'testing2'); + value.disposable.dispose(); }); test('Suggest, order 2/3', async () => { - disposables.push(extHost.registerCompletionItemProvider(defaultExtension, '*', new class implements vscode.CompletionItemProvider { + disposables.add(extHost.registerCompletionItemProvider(defaultExtension, '*', new class implements vscode.CompletionItemProvider { provideCompletionItems(): any { return [new types.CompletionItem('weak-selector')]; // weaker selector but result } }, [])); - disposables.push(extHost.registerCompletionItemProvider(defaultExtension, defaultSelector, new class implements vscode.CompletionItemProvider { + disposables.add(extHost.registerCompletionItemProvider(defaultExtension, defaultSelector, new class implements vscode.CompletionItemProvider { provideCompletionItems(): any { return []; // stronger selector but not a good result; } }, [])); await rpcProtocol.sync(); - const { items } = await provideSuggestionItems(languageFeaturesService.completionProvider, model, new EditorPosition(1, 1), new CompletionOptions(undefined, new Set().add(languages.CompletionItemKind.Snippet))); - assert.strictEqual(items.length, 1); - assert.strictEqual(items[0].completion.insertText, 'weak-selector'); + const value = await provideSuggestionItems(languageFeaturesService.completionProvider, model, new EditorPosition(1, 1), new CompletionOptions(undefined, new Set().add(languages.CompletionItemKind.Snippet))); + assert.strictEqual(value.items.length, 1); + assert.strictEqual(value.items[0].completion.insertText, 'weak-selector'); + value.disposable.dispose(); }); test('Suggest, order 3/3', async () => { - disposables.push(extHost.registerCompletionItemProvider(defaultExtension, defaultSelector, new class implements vscode.CompletionItemProvider { + disposables.add(extHost.registerCompletionItemProvider(defaultExtension, defaultSelector, new class implements vscode.CompletionItemProvider { provideCompletionItems(): any { return [new types.CompletionItem('strong-1')]; } }, [])); - disposables.push(extHost.registerCompletionItemProvider(defaultExtension, defaultSelector, new class implements vscode.CompletionItemProvider { + disposables.add(extHost.registerCompletionItemProvider(defaultExtension, defaultSelector, new class implements vscode.CompletionItemProvider { provideCompletionItems(): any { return [new types.CompletionItem('strong-2')]; } }, [])); await rpcProtocol.sync(); - const { items } = await provideSuggestionItems(languageFeaturesService.completionProvider, model, new EditorPosition(1, 1), new CompletionOptions(undefined, new Set().add(languages.CompletionItemKind.Snippet))); - assert.strictEqual(items.length, 2); - assert.strictEqual(items[0].completion.insertText, 'strong-1'); // sort by label - assert.strictEqual(items[1].completion.insertText, 'strong-2'); + const value = await provideSuggestionItems(languageFeaturesService.completionProvider, model, new EditorPosition(1, 1), new CompletionOptions(undefined, new Set().add(languages.CompletionItemKind.Snippet))); + assert.strictEqual(value.items.length, 2); + assert.strictEqual(value.items[0].completion.insertText, 'strong-1'); // sort by label + assert.strictEqual(value.items[1].completion.insertText, 'strong-2'); + value.disposable.dispose(); }); test('Suggest, evil provider', async () => { - disposables.push(extHost.registerCompletionItemProvider(defaultExtension, defaultSelector, new class implements vscode.CompletionItemProvider { + disposables.add(extHost.registerCompletionItemProvider(defaultExtension, defaultSelector, new class implements vscode.CompletionItemProvider { provideCompletionItems(): any { throw new Error('evil'); } }, [])); - disposables.push(extHost.registerCompletionItemProvider(defaultExtension, defaultSelector, new class implements vscode.CompletionItemProvider { + disposables.add(extHost.registerCompletionItemProvider(defaultExtension, defaultSelector, new class implements vscode.CompletionItemProvider { provideCompletionItems(): any { return [new types.CompletionItem('testing')]; } @@ -1028,13 +1044,15 @@ suite('ExtHostLanguageFeatures', function () { await rpcProtocol.sync(); - const { items } = await provideSuggestionItems(languageFeaturesService.completionProvider, model, new EditorPosition(1, 1), new CompletionOptions(undefined, new Set().add(languages.CompletionItemKind.Snippet))); - assert.strictEqual(items[0].container.incomplete, false); + const value = await provideSuggestionItems(languageFeaturesService.completionProvider, model, new EditorPosition(1, 1), new CompletionOptions(undefined, new Set().add(languages.CompletionItemKind.Snippet))); + assert.strictEqual(value.items[0].container.incomplete, false); + value.disposable.dispose(); + }); test('Suggest, CompletionList', async () => { - disposables.push(extHost.registerCompletionItemProvider(defaultExtension, defaultSelector, new class implements vscode.CompletionItemProvider { + disposables.add(extHost.registerCompletionItemProvider(defaultExtension, defaultSelector, new class implements vscode.CompletionItemProvider { provideCompletionItems(): any { return new types.CompletionList([new types.CompletionItem('hello')], true); } @@ -1043,6 +1061,7 @@ suite('ExtHostLanguageFeatures', function () { await rpcProtocol.sync(); await provideSuggestionItems(languageFeaturesService.completionProvider, model, new EditorPosition(1, 1), new CompletionOptions(undefined, new Set().add(languages.CompletionItemKind.Snippet))).then(model => { assert.strictEqual(model.items[0].container.incomplete, true); + model.disposable.dispose(); }); }); @@ -1055,7 +1074,7 @@ suite('ExtHostLanguageFeatures', function () { }; test('Format Doc, data conversion', async () => { - disposables.push(extHost.registerDocumentFormattingEditProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentFormattingEditProvider { + disposables.add(extHost.registerDocumentFormattingEditProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentFormattingEditProvider { provideDocumentFormattingEdits(): any { return [new types.TextEdit(new types.Range(0, 0, 0, 0), 'testing'), types.TextEdit.setEndOfLine(types.EndOfLine.LF)]; } @@ -1073,7 +1092,7 @@ suite('ExtHostLanguageFeatures', function () { }); test('Format Doc, evil provider', async () => { - disposables.push(extHost.registerDocumentFormattingEditProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentFormattingEditProvider { + disposables.add(extHost.registerDocumentFormattingEditProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentFormattingEditProvider { provideDocumentFormattingEdits(): any { throw new Error('evil'); } @@ -1085,19 +1104,19 @@ suite('ExtHostLanguageFeatures', function () { test('Format Doc, order', async () => { - disposables.push(extHost.registerDocumentFormattingEditProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentFormattingEditProvider { + disposables.add(extHost.registerDocumentFormattingEditProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentFormattingEditProvider { provideDocumentFormattingEdits(): any { return undefined; } })); - disposables.push(extHost.registerDocumentFormattingEditProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentFormattingEditProvider { + disposables.add(extHost.registerDocumentFormattingEditProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentFormattingEditProvider { provideDocumentFormattingEdits(): any { return [new types.TextEdit(new types.Range(0, 0, 0, 0), 'testing')]; } })); - disposables.push(extHost.registerDocumentFormattingEditProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentFormattingEditProvider { + disposables.add(extHost.registerDocumentFormattingEditProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentFormattingEditProvider { provideDocumentFormattingEdits(): any { return undefined; } @@ -1112,7 +1131,7 @@ suite('ExtHostLanguageFeatures', function () { }); test('Format Range, data conversion', async () => { - disposables.push(extHost.registerDocumentRangeFormattingEditProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentRangeFormattingEditProvider { + disposables.add(extHost.registerDocumentRangeFormattingEditProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentRangeFormattingEditProvider { provideDocumentRangeFormattingEdits(): any { return [new types.TextEdit(new types.Range(0, 0, 0, 0), 'testing')]; } @@ -1127,17 +1146,17 @@ suite('ExtHostLanguageFeatures', function () { }); test('Format Range, + format_doc', async () => { - disposables.push(extHost.registerDocumentRangeFormattingEditProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentRangeFormattingEditProvider { + disposables.add(extHost.registerDocumentRangeFormattingEditProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentRangeFormattingEditProvider { provideDocumentRangeFormattingEdits(): any { return [new types.TextEdit(new types.Range(0, 0, 0, 0), 'range')]; } })); - disposables.push(extHost.registerDocumentRangeFormattingEditProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentRangeFormattingEditProvider { + disposables.add(extHost.registerDocumentRangeFormattingEditProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentRangeFormattingEditProvider { provideDocumentRangeFormattingEdits(): any { return [new types.TextEdit(new types.Range(2, 3, 4, 5), 'range2')]; } })); - disposables.push(extHost.registerDocumentFormattingEditProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentFormattingEditProvider { + disposables.add(extHost.registerDocumentFormattingEditProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentFormattingEditProvider { provideDocumentFormattingEdits(): any { return [new types.TextEdit(new types.Range(0, 0, 1, 1), 'doc')]; } @@ -1154,7 +1173,7 @@ suite('ExtHostLanguageFeatures', function () { }); test('Format Range, evil provider', async () => { - disposables.push(extHost.registerDocumentRangeFormattingEditProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentRangeFormattingEditProvider { + disposables.add(extHost.registerDocumentRangeFormattingEditProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentRangeFormattingEditProvider { provideDocumentRangeFormattingEdits(): any { throw new Error('evil'); } @@ -1166,7 +1185,7 @@ suite('ExtHostLanguageFeatures', function () { test('Format on Type, data conversion', async () => { - disposables.push(extHost.registerOnTypeFormattingEditProvider(defaultExtension, defaultSelector, new class implements vscode.OnTypeFormattingEditProvider { + disposables.add(extHost.registerOnTypeFormattingEditProvider(defaultExtension, defaultSelector, new class implements vscode.OnTypeFormattingEditProvider { provideOnTypeFormattingEdits(): any { return [new types.TextEdit(new types.Range(0, 0, 0, 0), arguments[2])]; } @@ -1182,7 +1201,7 @@ suite('ExtHostLanguageFeatures', function () { test('Links, data conversion', async () => { - disposables.push(extHost.registerDocumentLinkProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentLinkProvider { + disposables.add(extHost.registerDocumentLinkProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentLinkProvider { provideDocumentLinks() { const link = new types.DocumentLink(new types.Range(0, 0, 1, 1), URI.parse('foo:bar#3')); link.tooltip = 'tooltip'; @@ -1191,7 +1210,7 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const { links } = await getLinks(languageFeaturesService.linkProvider, model, CancellationToken.None); + const { links } = disposables.add(await getLinks(languageFeaturesService.linkProvider, model, CancellationToken.None)); assert.strictEqual(links.length, 1); const [first] = links; assert.strictEqual(first.url?.toString(), 'foo:bar#3'); @@ -1201,20 +1220,20 @@ suite('ExtHostLanguageFeatures', function () { test('Links, evil provider', async () => { - disposables.push(extHost.registerDocumentLinkProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentLinkProvider { + disposables.add(extHost.registerDocumentLinkProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentLinkProvider { provideDocumentLinks() { return [new types.DocumentLink(new types.Range(0, 0, 1, 1), URI.parse('foo:bar#3'))]; } })); - disposables.push(extHost.registerDocumentLinkProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentLinkProvider { + disposables.add(extHost.registerDocumentLinkProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentLinkProvider { provideDocumentLinks(): any { throw new Error(); } })); await rpcProtocol.sync(); - const { links } = await getLinks(languageFeaturesService.linkProvider, model, CancellationToken.None); + const { links } = disposables.add(await getLinks(languageFeaturesService.linkProvider, model, CancellationToken.None)); assert.strictEqual(links.length, 1); const [first] = links; assert.strictEqual(first.url?.toString(), 'foo:bar#3'); @@ -1223,7 +1242,7 @@ suite('ExtHostLanguageFeatures', function () { test('Document colors, data conversion', async () => { - disposables.push(extHost.registerColorProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentColorProvider { + disposables.add(extHost.registerColorProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentColorProvider { provideDocumentColors(): vscode.ColorInformation[] { return [new types.ColorInformation(new types.Range(0, 0, 0, 20), new types.Color(0.1, 0.2, 0.3, 0.4))]; } @@ -1243,7 +1262,7 @@ suite('ExtHostLanguageFeatures', function () { // -- selection ranges test('Selection Ranges, data conversion', async () => { - disposables.push(extHost.registerSelectionRangeProvider(defaultExtension, defaultSelector, new class implements vscode.SelectionRangeProvider { + disposables.add(extHost.registerSelectionRangeProvider(defaultExtension, defaultSelector, new class implements vscode.SelectionRangeProvider { provideSelectionRanges() { return [ new types.SelectionRange(new types.Range(0, 10, 0, 18), new types.SelectionRange(new types.Range(0, 2, 0, 20))), diff --git a/src/vs/workbench/api/test/browser/extHostNotebook.test.ts b/src/vs/workbench/api/test/browser/extHostNotebook.test.ts index d13779aaede..7de8e1f9e75 100644 --- a/src/vs/workbench/api/test/browser/extHostNotebook.test.ts +++ b/src/vs/workbench/api/test/browser/extHostNotebook.test.ts @@ -26,10 +26,9 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { IExtHostTelemetry } from 'vs/workbench/api/common/extHostTelemetry'; import { ExtHostConsumerFileSystem } from 'vs/workbench/api/common/extHostFileSystemConsumer'; import { ExtHostFileSystemInfo } from 'vs/workbench/api/common/extHostFileSystemInfo'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('NotebookCell#Document', function () { - - let rpcProtocol: TestRPCProtocol; let notebook: ExtHostNotebookDocument; let extHostDocumentsAndEditors: ExtHostDocumentsAndEditors; @@ -45,6 +44,8 @@ suite('NotebookCell#Document', function () { disposables.clear(); }); + ensureNoDisposablesAreLeakedInTestSuite(); + setup(async function () { rpcProtocol = new TestRPCProtocol(); rpcProtocol.set(MainContext.MainThreadCommands, new class extends mock() { @@ -145,7 +146,7 @@ suite('NotebookCell#Document', function () { const p = new Promise((resolve, reject) => { - extHostNotebookDocuments.onDidChangeNotebookDocument(e => { + disposables.add(extHostNotebookDocuments.onDidChangeNotebookDocument(e => { try { assert.strictEqual(e.contentChanges.length, 1); assert.strictEqual(e.contentChanges[0].addedCells.length, 2); @@ -165,7 +166,7 @@ suite('NotebookCell#Document', function () { } catch (err) { reject(err); } - }); + })); }); @@ -357,7 +358,7 @@ suite('NotebookCell#Document', function () { test('Opening a notebook results in VS Code firing the event onDidChangeActiveNotebookEditor twice #118470', function () { let count = 0; - extHostNotebooks.onDidChangeActiveNotebookEditor(() => count += 1); + disposables.add(extHostNotebooks.onDidChangeActiveNotebookEditor(() => count += 1)); extHostNotebooks.$acceptDocumentAndEditorsDelta(new SerializableObjectWithBuffers({ addedEditors: [{ diff --git a/src/vs/workbench/api/test/browser/extHostNotebookKernel.test.ts b/src/vs/workbench/api/test/browser/extHostNotebookKernel.test.ts index 8186540ef63..0fabe6e6ac6 100644 --- a/src/vs/workbench/api/test/browser/extHostNotebookKernel.test.ts +++ b/src/vs/workbench/api/test/browser/extHostNotebookKernel.test.ts @@ -28,9 +28,9 @@ import { mock } from 'vs/workbench/test/common/workbenchTestServices'; import { IExtHostTelemetry } from 'vs/workbench/api/common/extHostTelemetry'; import { ExtHostConsumerFileSystem } from 'vs/workbench/api/common/extHostFileSystemConsumer'; import { ExtHostFileSystemInfo } from 'vs/workbench/api/common/extHostFileSystemInfo'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('NotebookKernel', function () { - let rpcProtocol: TestRPCProtocol; let extHostNotebookKernels: ExtHostNotebookKernels; let notebook: ExtHostNotebookDocument; @@ -52,6 +52,9 @@ suite('NotebookKernel', function () { teardown(function () { disposables.clear(); }); + + ensureNoDisposablesAreLeakedInTestSuite(); + setup(async function () { cellExecuteCreate.length = 0; cellExecuteUpdates.length = 0; @@ -91,7 +94,7 @@ suite('NotebookKernel', function () { override async $unregisterNotebookSerializer() { } }); extHostDocumentsAndEditors = new ExtHostDocumentsAndEditors(rpcProtocol, new NullLogService()); - extHostDocuments = new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors); + extHostDocuments = disposables.add(new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors)); extHostCommands = new ExtHostCommands(rpcProtocol, new NullLogService(), new class extends mock() { override onExtensionError(): boolean { return true; @@ -177,7 +180,7 @@ suite('NotebookKernel', function () { test('update kernel', async function () { - const kernel = extHostNotebookKernels.createNotebookController(nullExtensionDescription, 'foo', '*', 'Foo'); + const kernel = disposables.add(extHostNotebookKernels.createNotebookController(nullExtensionDescription, 'foo', '*', 'Foo')); await rpcProtocol.sync(); assert.ok(kernel); @@ -196,7 +199,7 @@ suite('NotebookKernel', function () { }); test('execute - simple createNotebookCellExecution', function () { - const kernel = extHostNotebookKernels.createNotebookController(nullExtensionDescription, 'foo', '*', 'Foo'); + const kernel = disposables.add(extHostNotebookKernels.createNotebookController(nullExtensionDescription, 'foo', '*', 'Foo')); extHostNotebookKernels.$acceptNotebookAssociation(0, notebook.uri, true); @@ -207,17 +210,18 @@ suite('NotebookKernel', function () { }); test('createNotebookCellExecution, must be selected/associated', function () { - const kernel = extHostNotebookKernels.createNotebookController(nullExtensionDescription, 'foo', '*', 'Foo'); + const kernel = disposables.add(extHostNotebookKernels.createNotebookController(nullExtensionDescription, 'foo', '*', 'Foo')); assert.throws(() => { kernel.createNotebookCellExecution(notebook.apiNotebook.cellAt(0)); }); extHostNotebookKernels.$acceptNotebookAssociation(0, notebook.uri, true); - kernel.createNotebookCellExecution(notebook.apiNotebook.cellAt(0)); + const execution = kernel.createNotebookCellExecution(notebook.apiNotebook.cellAt(0)); + execution.end(true); }); test('createNotebookCellExecution, cell must be alive', function () { - const kernel = extHostNotebookKernels.createNotebookController(nullExtensionDescription, 'foo', '*', 'Foo'); + const kernel = disposables.add(extHostNotebookKernels.createNotebookController(nullExtensionDescription, 'foo', '*', 'Foo')); const cell1 = notebook.apiNotebook.cellAt(0); @@ -242,14 +246,14 @@ suite('NotebookKernel', function () { let interruptCallCount = 0; let tokenCancelCount = 0; - const kernel = extHostNotebookKernels.createNotebookController(nullExtensionDescription, 'foo', '*', 'Foo'); + const kernel = disposables.add(extHostNotebookKernels.createNotebookController(nullExtensionDescription, 'foo', '*', 'Foo')); kernel.interruptHandler = () => { interruptCallCount += 1; }; extHostNotebookKernels.$acceptNotebookAssociation(0, notebook.uri, true); const cell1 = notebook.apiNotebook.cellAt(0); const task = kernel.createNotebookCellExecution(cell1); - task.token.onCancellationRequested(() => tokenCancelCount += 1); + disposables.add(task.token.onCancellationRequested(() => tokenCancelCount += 1)); await extHostNotebookKernels.$cancelCells(0, notebook.uri, [0]); assert.strictEqual(interruptCallCount, 1); @@ -258,11 +262,14 @@ suite('NotebookKernel', function () { await extHostNotebookKernels.$cancelCells(0, notebook.uri, [0]); assert.strictEqual(interruptCallCount, 2); assert.strictEqual(tokenCancelCount, 0); + + // should cancelling the cells end the execution task? + task.end(false); }); test('set outputs on cancel', async function () { - const kernel = extHostNotebookKernels.createNotebookController(nullExtensionDescription, 'foo', '*', 'Foo'); + const kernel = disposables.add(extHostNotebookKernels.createNotebookController(nullExtensionDescription, 'foo', '*', 'Foo')); extHostNotebookKernels.$acceptNotebookAssociation(0, notebook.uri, true); const cell1 = notebook.apiNotebook.cellAt(0); @@ -271,11 +278,13 @@ suite('NotebookKernel', function () { const b = new Barrier(); - task.token.onCancellationRequested(async () => { - await task.replaceOutput(new NotebookCellOutput([NotebookCellOutputItem.text('canceled')])); - task.end(true); - b.open(); // use barrier to signal that cancellation has happened - }); + disposables.add( + task.token.onCancellationRequested(async () => { + await task.replaceOutput(new NotebookCellOutput([NotebookCellOutputItem.text('canceled')])); + task.end(true); + b.open(); // use barrier to signal that cancellation has happened + }) + ); cellExecuteUpdates.length = 0; await extHostNotebookKernels.$cancelCells(0, notebook.uri, [0]); @@ -331,3 +340,4 @@ suite('NotebookKernel', function () { assert.ok(found); }); }); + diff --git a/src/vs/workbench/api/test/browser/extHostTreeViews.test.ts b/src/vs/workbench/api/test/browser/extHostTreeViews.test.ts index df4903ee04a..3f780a03299 100644 --- a/src/vs/workbench/api/test/browser/extHostTreeViews.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTreeViews.test.ts @@ -8,12 +8,9 @@ import * as sinon from 'sinon'; import { Emitter } from 'vs/base/common/event'; import { ExtHostTreeViews } from 'vs/workbench/api/common/extHostTreeViews'; import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; -import { MainThreadTreeViewsShape, MainContext } from 'vs/workbench/api/common/extHost.protocol'; +import { MainThreadTreeViewsShape, MainContext, MainThreadCommandsShape } from 'vs/workbench/api/common/extHost.protocol'; import { TreeDataProvider, TreeItem } from 'vscode'; import { TestRPCProtocol } from 'vs/workbench/api/test/common/testRPCProtocol'; -import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; -import { MainThreadCommands } from 'vs/workbench/api/browser/mainThreadCommands'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { mock } from 'vs/base/test/common/mock'; import { TreeItemCollapsibleState, ITreeItem, IRevealOptions } from 'vs/workbench/common/views'; import { NullLogService } from 'vs/platform/log/common/log'; @@ -21,8 +18,10 @@ import type { IDisposable } from 'vs/base/common/lifecycle'; import { nullExtensionDescription as extensionsDescription } from 'vs/workbench/services/extensions/common/extensions'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; import { IExtHostTelemetry } from 'vs/workbench/api/common/extHostTelemetry'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('ExtHostTreeView', function () { + const store = ensureNoDisposablesAreLeakedInTestSuite(); class RecordingShape extends mock() { @@ -41,6 +40,10 @@ suite('ExtHostTreeView', function () { return Promise.resolve(); } + override $disposeTree(treeViewId: string): Promise { + return Promise.resolve(); + } + } let testObject: ExtHostTreeViews; @@ -50,7 +53,6 @@ suite('ExtHostTreeView', function () { let tree: { [key: string]: any }; let labels: { [key: string]: string }; let nodes: { [key: string]: { key: string } }; - let instantiationService: TestInstantiationService; setup(() => { tree = { @@ -68,16 +70,12 @@ suite('ExtHostTreeView', function () { nodes = {}; const rpcProtocol = new TestRPCProtocol(); - // Use IInstantiationService to get typechecking when instantiating - let inst: IInstantiationService; - { - instantiationService = new TestInstantiationService(); - inst = instantiationService; - } - rpcProtocol.set(MainContext.MainThreadCommands, inst.createInstance(MainThreadCommands, rpcProtocol)); + rpcProtocol.set(MainContext.MainThreadCommands, new class extends mock() { + override $registerCommand() { } + }); target = new RecordingShape(); - testObject = new ExtHostTreeViews(target, new ExtHostCommands( + testObject = store.add(new ExtHostTreeViews(target, new ExtHostCommands( rpcProtocol, new NullLogService(), new class extends mock() { @@ -85,7 +83,7 @@ suite('ExtHostTreeView', function () { return true; } } - ), new NullLogService()); + ), new NullLogService())); onDidChangeTreeNode = new Emitter<{ key: string } | undefined>(); onDidChangeTreeNodeWithId = new Emitter<{ key: string }>(); testObject.createTreeView('testNodeTreeProvider', { treeDataProvider: aNodeTreeDataProvider() }, extensionsDescription); @@ -95,10 +93,6 @@ suite('ExtHostTreeView', function () { return loadCompleteTree('testNodeTreeProvider'); }); - teardown(() => { - instantiationService.dispose(); - }); - test('construct node tree', () => { return testObject.$getChildren('testNodeTreeProvider') .then(elements => { @@ -209,7 +203,7 @@ suite('ExtHostTreeView', function () { 'ba': {} }; let caughtExpectedError = false; - target.onRefresh.event(() => { + store.add(target.onRefresh.event(() => { testObject.$getChildren('testNodeWithIdTreeProvider') .then(elements => { const actuals = elements?.map(e => e.handle); @@ -220,21 +214,21 @@ suite('ExtHostTreeView', function () { .catch(() => caughtExpectedError = true) .finally(() => caughtExpectedError ? done() : assert.fail('Expected duplicate id error not thrown.')); }); - }); + })); onDidChangeTreeNode.fire(undefined); }); test('refresh root', function (done) { - target.onRefresh.event(actuals => { + store.add(target.onRefresh.event(actuals => { assert.strictEqual(undefined, actuals); done(); - }); + })); onDidChangeTreeNode.fire(undefined); }); test('refresh a parent node', () => { return new Promise((c, e) => { - target.onRefresh.event(actuals => { + store.add(target.onRefresh.event(actuals => { assert.deepStrictEqual(['0/0:b'], Object.keys(actuals)); assert.deepStrictEqual(removeUnsetKeys(actuals['0/0:b']), { handle: '0/0:b', @@ -242,13 +236,13 @@ suite('ExtHostTreeView', function () { collapsibleState: TreeItemCollapsibleState.Collapsed }); c(undefined); - }); + })); onDidChangeTreeNode.fire(getNode('b')); }); }); test('refresh a leaf node', function (done) { - target.onRefresh.event(actuals => { + store.add(target.onRefresh.event(actuals => { assert.deepStrictEqual(['0/0:b/0:bb'], Object.keys(actuals)); assert.deepStrictEqual(removeUnsetKeys(actuals['0/0:b/0:bb']), { handle: '0/0:b/0:bb', @@ -257,7 +251,7 @@ suite('ExtHostTreeView', function () { collapsibleState: TreeItemCollapsibleState.None }); done(); - }); + })); onDidChangeTreeNode.fire(getNode('bb')); }); @@ -277,7 +271,7 @@ suite('ExtHostTreeView', function () { test('refresh parent and child node trigger refresh only on parent - scenario 1', async () => { return runWithEventMerging((resolve) => { - target.onRefresh.event(actuals => { + store.add(target.onRefresh.event(actuals => { assert.deepStrictEqual(['0/0:b', '0/0:a/0:aa'], Object.keys(actuals)); assert.deepStrictEqual(removeUnsetKeys(actuals['0/0:b']), { handle: '0/0:b', @@ -291,7 +285,7 @@ suite('ExtHostTreeView', function () { collapsibleState: TreeItemCollapsibleState.None }); resolve(); - }); + })); onDidChangeTreeNode.fire(getNode('b')); onDidChangeTreeNode.fire(getNode('aa')); onDidChangeTreeNode.fire(getNode('bb')); @@ -300,7 +294,7 @@ suite('ExtHostTreeView', function () { test('refresh parent and child node trigger refresh only on parent - scenario 2', async () => { return runWithEventMerging((resolve) => { - target.onRefresh.event(actuals => { + store.add(target.onRefresh.event(actuals => { assert.deepStrictEqual(['0/0:a/0:aa', '0/0:b'], Object.keys(actuals)); assert.deepStrictEqual(removeUnsetKeys(actuals['0/0:b']), { handle: '0/0:b', @@ -314,7 +308,7 @@ suite('ExtHostTreeView', function () { collapsibleState: TreeItemCollapsibleState.None }); resolve(); - }); + })); onDidChangeTreeNode.fire(getNode('bb')); onDidChangeTreeNode.fire(getNode('aa')); onDidChangeTreeNode.fire(getNode('b')); @@ -323,7 +317,7 @@ suite('ExtHostTreeView', function () { test('refresh an element for label change', function (done) { labels['a'] = 'aa'; - target.onRefresh.event(actuals => { + store.add(target.onRefresh.event(actuals => { assert.deepStrictEqual(['0/0:a'], Object.keys(actuals)); assert.deepStrictEqual(removeUnsetKeys(actuals['0/0:a']), { handle: '0/0:aa', @@ -331,16 +325,16 @@ suite('ExtHostTreeView', function () { collapsibleState: TreeItemCollapsibleState.Collapsed }); done(); - }); + })); onDidChangeTreeNode.fire(getNode('a')); }); test('refresh calls are throttled on roots', () => { return runWithEventMerging((resolve) => { - target.onRefresh.event(actuals => { + store.add(target.onRefresh.event(actuals => { assert.strictEqual(undefined, actuals); resolve(); - }); + })); onDidChangeTreeNode.fire(undefined); onDidChangeTreeNode.fire(undefined); onDidChangeTreeNode.fire(undefined); @@ -350,10 +344,10 @@ suite('ExtHostTreeView', function () { test('refresh calls are throttled on elements', () => { return runWithEventMerging((resolve) => { - target.onRefresh.event(actuals => { + store.add(target.onRefresh.event(actuals => { assert.deepStrictEqual(['0/0:a', '0/0:b'], Object.keys(actuals)); resolve(); - }); + })); onDidChangeTreeNode.fire(getNode('a')); onDidChangeTreeNode.fire(getNode('b')); @@ -364,10 +358,10 @@ suite('ExtHostTreeView', function () { test('refresh calls are throttled on unknown elements', () => { return runWithEventMerging((resolve) => { - target.onRefresh.event(actuals => { + store.add(target.onRefresh.event(actuals => { assert.deepStrictEqual(['0/0:a', '0/0:b'], Object.keys(actuals)); resolve(); - }); + })); onDidChangeTreeNode.fire(getNode('a')); onDidChangeTreeNode.fire(getNode('b')); @@ -378,10 +372,10 @@ suite('ExtHostTreeView', function () { test('refresh calls are throttled on unknown elements and root', () => { return runWithEventMerging((resolve) => { - target.onRefresh.event(actuals => { + store.add(target.onRefresh.event(actuals => { assert.strictEqual(undefined, actuals); resolve(); - }); + })); onDidChangeTreeNode.fire(getNode('a')); onDidChangeTreeNode.fire(getNode('b')); @@ -392,10 +386,10 @@ suite('ExtHostTreeView', function () { test('refresh calls are throttled on elements and root', () => { return runWithEventMerging((resolve) => { - target.onRefresh.event(actuals => { + store.add(target.onRefresh.event(actuals => { assert.strictEqual(undefined, actuals); resolve(); - }); + })); onDidChangeTreeNode.fire(getNode('a')); onDidChangeTreeNode.fire(getNode('b')); @@ -409,13 +403,13 @@ suite('ExtHostTreeView', function () { 'a/0:b': {} }; - target.onRefresh.event(() => { + store.add(target.onRefresh.event(() => { testObject.$getChildren('testNodeTreeProvider') .then(elements => { assert.deepStrictEqual(elements?.map(e => e.handle), ['0/0:a//0:b']); done(); }); - }); + })); onDidChangeTreeNode.fire(undefined); }); @@ -451,7 +445,7 @@ suite('ExtHostTreeView', function () { tree['f'] = {}; tree[dupItems['adup2']] = {}; - target.onRefresh.event(() => { + store.add(target.onRefresh.event(() => { testObject.$getChildren('testNodeTreeProvider') .then(elements => { const actuals = elements?.map(e => e.handle); @@ -463,7 +457,7 @@ suite('ExtHostTreeView', function () { done(); }); }); - }); + })); onDidChangeTreeNode.fire(undefined); }); @@ -473,13 +467,13 @@ suite('ExtHostTreeView', function () { 'c': {} }; - target.onRefresh.event(() => { + store.add(target.onRefresh.event(() => { testObject.$getChildren('testNodeTreeProvider') .then(elements => { assert.deepStrictEqual(elements?.map(e => e.handle), ['0/0:c']); done(); }); - }); + })); onDidChangeTreeNode.fire(undefined); }); diff --git a/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts b/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts index 14ef427ac59..3d66d2dffa6 100644 --- a/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts @@ -31,6 +31,7 @@ import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/te import { TextModel } from 'vs/editor/common/model/textModel'; import { LanguageService } from 'vs/editor/common/services/languageService'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('MainThreadDocumentsAndEditors', () => { @@ -120,10 +121,12 @@ suite('MainThreadDocumentsAndEditors', () => { disposables.dispose(); }); + ensureNoDisposablesAreLeakedInTestSuite(); + test('Model#add', () => { deltas.length = 0; - modelService.createModel('farboo', null); + disposables.add(modelService.createModel('farboo', null)); assert.strictEqual(deltas.length, 1); const [delta] = deltas; @@ -143,6 +146,7 @@ suite('MainThreadDocumentsAndEditors', () => { TextModel._MODEL_SYNC_LIMIT = largeModelString.length / 2; const model = modelService.createModel(largeModelString, null); + disposables.add(model); assert.ok(model.isTooLargeForSyncing()); assert.strictEqual(deltas.length, 1); @@ -172,6 +176,7 @@ suite('MainThreadDocumentsAndEditors', () => { deltas.length = 0; assert.strictEqual(deltas.length, 0); editor.dispose(); + model.dispose(); } finally { TextModel._MODEL_SYNC_LIMIT = oldLimit; @@ -182,6 +187,7 @@ suite('MainThreadDocumentsAndEditors', () => { this.timeout(1000 * 60); // increase timeout for this one test const model = modelService.createModel('test', null, undefined, true); + disposables.add(model); assert.ok(model.isForSimpleWidget); assert.strictEqual(deltas.length, 1); @@ -227,10 +233,10 @@ suite('MainThreadDocumentsAndEditors', () => { assert.strictEqual(second.newActiveEditor, undefined); editor.dispose(); + model.dispose(); }); test('editor with dispos-ed/-ing model', () => { - modelService.createModel('foobar', null); const model = modelService.createModel('farboo', null); const editor = myCreateTestCodeEditor(model); @@ -248,5 +254,6 @@ suite('MainThreadDocumentsAndEditors', () => { assert.strictEqual(first.addedEditors, undefined); editor.dispose(); + model.dispose(); }); }); diff --git a/src/vs/workbench/browser/actions/quickAccessActions.ts b/src/vs/workbench/browser/actions/quickAccessActions.ts index b501e78d5ea..6e8d0459e6c 100644 --- a/src/vs/workbench/browser/actions/quickAccessActions.ts +++ b/src/vs/workbench/browser/actions/quickAccessActions.ts @@ -14,6 +14,7 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation import { inQuickPickContext, defaultQuickAccessContext, getQuickNavigateHandler } from 'vs/workbench/browser/quickaccess'; import { ILocalizedString } from 'vs/platform/action/common/action'; import { AnythingQuickAccessProviderRunOptions } from 'vs/platform/quickinput/common/quickAccess'; +import { Codicon } from 'vs/base/common/codicons'; //#region Quick access management commands and keys @@ -155,8 +156,9 @@ registerAction2(class QuickAccessAction extends Action2 { super({ id: 'workbench.action.quickOpenWithModes', title: localize('quickOpenWithModes', "Quick Open"), + icon: Codicon.search, menu: { - id: MenuId.CommandCenter, + id: MenuId.CommandCenterCenter, order: 100 } }); diff --git a/src/vs/workbench/browser/iframe.ts b/src/vs/workbench/browser/iframe.ts deleted file mode 100644 index cd6bfa2be82..00000000000 --- a/src/vs/workbench/browser/iframe.ts +++ /dev/null @@ -1,26 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -/** - * Returns a sha-256 composed of `parentOrigin` and `salt` converted to base 32 - */ -export async function parentOriginHash(parentOrigin: string, salt: string): Promise { - // This same code is also inlined at `src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html` - if (!crypto.subtle) { - throw new Error(`'crypto.subtle' is not available so webviews will not work. This is likely because the editor is not running in a secure context (https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts).`); - } - - const strData = JSON.stringify({ parentOrigin, salt }); - const encoder = new TextEncoder(); - const arrData = encoder.encode(strData); - const hash = await crypto.subtle.digest('sha-256', arrData); - return sha256AsBase32(hash); -} - -function sha256AsBase32(bytes: ArrayBuffer): string { - const array = Array.from(new Uint8Array(bytes)); - const hexArray = array.map(b => b.toString(16).padStart(2, '0')).join(''); - // sha256 has 256 bits, so we need at most ceil(lg(2^256-1)/lg(32)) = 52 chars to represent it in base 32 - return BigInt(`0x${hexArray}`).toString(32).padStart(52, '0'); -} diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts index 79d341b32d0..86b9a9721b0 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts @@ -39,6 +39,7 @@ import { IOutline } from 'vs/workbench/services/outline/browser/outline'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { Codicon } from 'vs/base/common/codicons'; import { defaultBreadcrumbsWidgetStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { Emitter } from 'vs/base/common/event'; class OutlineItem extends BreadcrumbsItem { @@ -501,6 +502,59 @@ export class BreadcrumbsControl { } } +export class BreadcrumbsControlFactory { + + private readonly _disposables = new DisposableStore(); + + private _control: BreadcrumbsControl | undefined; + get control() { return this._control; } + + private readonly _onDidEnablementChange = this._disposables.add(new Emitter()); + get onDidEnablementChange() { return this._onDidEnablementChange.event; } + + constructor( + container: HTMLElement, + editorGroup: IEditorGroupView, + options: IBreadcrumbsControlOptions, + @IConfigurationService configurationService: IConfigurationService, + @IInstantiationService instantiationService: IInstantiationService, + @IFileService fileService: IFileService + ) { + const config = this._disposables.add(BreadcrumbsConfig.IsEnabled.bindTo(configurationService)); + this._disposables.add(config.onDidChange(() => { + const value = config.getValue(); + if (!value && this._control) { + this._control.dispose(); + this._control = undefined; + this._onDidEnablementChange.fire(); + } else if (value && !this._control) { + this._control = instantiationService.createInstance(BreadcrumbsControl, container, options, editorGroup); + this._control.update(); + this._onDidEnablementChange.fire(); + } + })); + + if (config.getValue()) { + this._control = instantiationService.createInstance(BreadcrumbsControl, container, options, editorGroup); + } + + this._disposables.add(fileService.onDidChangeFileSystemProviderRegistrations(e => { + if (this._control?.model && this._control.model.resource.scheme !== e.scheme) { + // ignore if the scheme of the breadcrumbs resource is not affected + return; + } + if (this._control?.update()) { + this._onDidEnablementChange.fire(); + } + })); + } + + dispose(): void { + this._disposables.dispose(); + this._control?.dispose(); + } +} + //#region commands // toggle command diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index 36f68774d5f..42870c2ccdc 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -66,7 +66,6 @@ import { Codicon } from 'vs/base/common/codicons'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { UntitledTextEditorInputSerializer, UntitledTextEditorWorkingCopyEditorHandler } from 'vs/workbench/services/untitled/common/untitledTextEditorHandler'; import { DynamicEditorConfigurations } from 'vs/workbench/browser/parts/editor/editorConfiguration'; -import { AccessibilityStatus } from 'vs/workbench/browser/parts/editor/accessibilityStatus'; import { ToggleTabsVisibilityAction } from 'vs/workbench/browser/actions/layoutActions'; //#region Editor Registrations @@ -126,7 +125,6 @@ Registry.as(EditorExtensions.EditorFactory).registerEdit Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(EditorAutoSave, LifecyclePhase.Ready); Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(EditorStatus, LifecyclePhase.Ready); -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(AccessibilityStatus, LifecyclePhase.Ready); Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(UntitledTextEditorWorkingCopyEditorHandler, LifecyclePhase.Ready); Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DynamicEditorConfigurations, LifecyclePhase.Ready); diff --git a/src/vs/workbench/browser/parts/editor/editor.ts b/src/vs/workbench/browser/parts/editor/editor.ts index c8cdf695fcb..8764654ad5a 100644 --- a/src/vs/workbench/browser/parts/editor/editor.ts +++ b/src/vs/workbench/browser/parts/editor/editor.ts @@ -118,7 +118,7 @@ export interface IEditorGroupTitleHeight { /** * The height offset to e.g. use when drawing drop overlays. * This number may be smaller than `height` if the title control - * decides to have an `offset` that is within the title area + * decides to have an `offset` that is within the title control * (e.g. when breadcrumbs are enabled). */ offset: number; diff --git a/src/vs/workbench/browser/parts/editor/editorAutoSave.ts b/src/vs/workbench/browser/parts/editor/editorAutoSave.ts index 1de67c853f8..5fed6253090 100644 --- a/src/vs/workbench/browser/parts/editor/editorAutoSave.ts +++ b/src/vs/workbench/browser/parts/editor/editorAutoSave.ts @@ -191,7 +191,7 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution const handle = setTimeout(() => { // Clear disposable - this.pendingAutoSavesAfterDelay.delete(workingCopy); + this.discardAutoSave(workingCopy); // Save if dirty if (workingCopy.isDirty()) { diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 01b92a4b7e2..f013b140ff4 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -11,7 +11,7 @@ import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { Emitter, Relay } from 'vs/base/common/event'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { Dimension, trackFocus, addDisposableListener, EventType, EventHelper, findParentWithClass, clearNode, isAncestor, IDomNodePagePosition } from 'vs/base/browser/dom'; +import { Dimension, trackFocus, addDisposableListener, EventType, EventHelper, findParentWithClass, isAncestor, IDomNodePagePosition } from 'vs/base/browser/dom'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; @@ -19,7 +19,6 @@ import { IThemeService, Themable } from 'vs/platform/theme/common/themeService'; import { editorBackground, contrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { EDITOR_GROUP_HEADER_TABS_BACKGROUND, EDITOR_GROUP_HEADER_NO_TABS_BACKGROUND, EDITOR_GROUP_EMPTY_BACKGROUND, EDITOR_GROUP_HEADER_BORDER } from 'vs/workbench/common/theme'; import { ICloseEditorsFilter, GroupsOrder, ICloseEditorOptions, ICloseAllEditorsOptions, IEditorReplacement } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { TabsTitleControl } from 'vs/workbench/browser/parts/editor/tabsTitleControl'; import { EditorPanes } from 'vs/workbench/browser/parts/editor/editorPanes'; import { IEditorProgressService } from 'vs/platform/progress/common/progress'; import { EditorProgressIndicator } from 'vs/workbench/services/progress/browser/progressIndicator'; @@ -29,12 +28,10 @@ import { MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { DeferredPromise, Promises, RunOnceWorker } from 'vs/base/common/async'; import { EventType as TouchEventType, GestureEvent } from 'vs/base/browser/touch'; -import { TitleControl } from 'vs/workbench/browser/parts/editor/titleControl'; import { IEditorGroupsAccessor, IEditorGroupView, fillActiveEditorViewState, EditorServiceImpl, IEditorGroupTitleHeight, IInternalEditorOpenOptions, IInternalMoveCopyOptions, IInternalEditorCloseOptions, IInternalEditorTitleControlOptions } from 'vs/workbench/browser/parts/editor/editor'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IAction } from 'vs/base/common/actions'; -import { NoTabsTitleControl } from 'vs/workbench/browser/parts/editor/noTabsTitleControl'; import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; @@ -55,6 +52,7 @@ import { TelemetryTrustedValue } from 'vs/platform/telemetry/common/telemetryUti import { defaultProgressBarStyles } from 'vs/platform/theme/browser/defaultStyles'; import { IBoundarySashes } from 'vs/base/browser/ui/sash/sash'; import { EditorGroupWatermark } from 'vs/workbench/browser/parts/editor/editorGroupWatermark'; +import { EditorTitleControl } from 'vs/workbench/browser/parts/editor/editorTitleControl'; export class EditorGroupView extends Themable implements IEditorGroupView { @@ -118,7 +116,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { private readonly scopedInstantiationService: IInstantiationService; private readonly titleContainer: HTMLElement; - private titleAreaControl: TitleControl; + private readonly titleControl: EditorTitleControl; private readonly progressBar: ProgressBar; @@ -200,7 +198,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.element.appendChild(this.titleContainer); // Title control - this.titleAreaControl = this.createTitleAreaControl(); + this.titleControl = this._register(this.scopedInstantiationService.createInstance(EditorTitleControl, this.titleContainer, this.accessor, this)); // Editor container this.editorContainer = document.createElement('div'); @@ -458,24 +456,6 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.titleContainer.classList.toggle('show-file-icons', this.accessor.partOptions.showIcons); } - private createTitleAreaControl(): TitleControl { - - // Clear old if existing - if (this.titleAreaControl) { - this.titleAreaControl.dispose(); - clearNode(this.titleContainer); - } - - // Create new based on options - if (this.accessor.partOptions.showTabs) { - this.titleAreaControl = this.scopedInstantiationService.createInstance(TabsTitleControl, this.titleContainer, this.accessor, this); - } else { - this.titleAreaControl = this.scopedInstantiationService.createInstance(NoTabsTitleControl, this.titleContainer, this.accessor, this); - } - - return this.titleAreaControl; - } - private restoreEditors(from: IEditorGroupView | ISerializedEditorGroupModel | null): Promise | undefined { if (this.count === 0) { return; // nothing to show @@ -702,26 +682,21 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Title container this.updateTitleContainer(); + // Title control + this.titleControl.updateOptions(event.oldPartOptions, event.newPartOptions); + // Title control Switch between showing tabs <=> not showing tabs if (event.oldPartOptions.showTabs !== event.newPartOptions.showTabs) { - // Recreate title control - this.createTitleAreaControl(); - // Re-layout this.relayout(); // Ensure to show active editor if any if (this.model.activeEditor) { - this.titleAreaControl.openEditor(this.model.activeEditor); + this.titleControl.openEditor(this.model.activeEditor); } } - // Just update title control - else { - this.titleAreaControl.updateOptions(event.oldPartOptions, event.newPartOptions); - } - // Styles this.updateStyles(); @@ -739,13 +714,13 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.pinEditor(editor); // Forward to title control - this.titleAreaControl.updateEditorDirty(editor); + this.titleControl.updateEditorDirty(editor); } private onDidChangeEditorLabel(editor: EditorInput): void { // Forward to title control - this.titleAreaControl.updateEditorLabel(editor); + this.titleControl.updateEditorLabel(editor); } private onDidVisibilityChange(visible: boolean): void { @@ -780,7 +755,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } get titleHeight(): IEditorGroupTitleHeight { - return this.titleAreaControl.getHeight(); + return this.titleControl.getHeight(); } notifyIndexChanged(newIndex: number): void { @@ -798,7 +773,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.element.classList.toggle('inactive', !isActive); // Update title control - this.titleAreaControl.setActive(isActive); + this.titleControl.setActive(isActive); // Update styles this.updateStyles(); @@ -925,7 +900,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Forward to title control if (editor) { - this.titleAreaControl.pinEditor(editor); + this.titleControl.pinEditor(editor); } } } @@ -952,14 +927,14 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // title control and also make sure to emit this as an event const newIndexOfEditor = this.getIndexOfEditor(editor); if (newIndexOfEditor !== oldIndexOfEditor) { - this.titleAreaControl.moveEditor(editor, oldIndexOfEditor, newIndexOfEditor); + this.titleControl.moveEditor(editor, oldIndexOfEditor, newIndexOfEditor); } // Forward sticky state to title control if (sticky) { - this.titleAreaControl.stickEditor(editor); + this.titleControl.stickEditor(editor); } else { - this.titleAreaControl.unstickEditor(editor); + this.titleControl.unstickEditor(editor); } } } @@ -1119,7 +1094,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Show in title control after editor control because some actions depend on it // but respect the internal options in case title control updates should skip. if (!internalOptions?.skipTitleUpdate) { - this.titleAreaControl.openEditor(editor); + this.titleControl.openEditor(editor); } return openEditorPromise; @@ -1169,7 +1144,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { })); // Update the title control all at once with all editors - this.titleAreaControl.openEditors(inactiveEditors.map(({ editor }) => editor)); + this.titleControl.openEditors(inactiveEditors.map(({ editor }) => editor)); // Opening many editors at once can put any editor to be // the active one depending on options. As such, we simply @@ -1200,8 +1175,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // in source and target if the title update was skipped if (internalOptions.skipTitleUpdate) { const movedEditors = editors.map(({ editor }) => editor); - target.titleAreaControl.openEditors(movedEditors); - this.titleAreaControl.closeEditors(movedEditors); + target.titleControl.openEditors(movedEditors); + this.titleControl.closeEditors(movedEditors); } } @@ -1241,9 +1216,9 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.model.moveEditor(editor, moveToIndex); this.model.pin(editor); - // Forward to title area - this.titleAreaControl.moveEditor(editor, currentIndex, moveToIndex); - this.titleAreaControl.pinEditor(editor); + // Forward to title control + this.titleControl.moveEditor(editor, currentIndex, moveToIndex); + this.titleControl.pinEditor(editor); } private doMoveOrCopyEditorAcrossGroups(editor: EditorInput, target: EditorGroupView, openOptions?: IEditorOpenOptions, internalOptions?: IInternalMoveCopyOptions): void { @@ -1299,7 +1274,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // in target if the title update was skipped if (internalOptions.skipTitleUpdate) { const copiedEditors = editors.map(({ editor }) => editor); - target.titleAreaControl.openEditors(copiedEditors); + target.titleControl.openEditors(copiedEditors); } } @@ -1346,7 +1321,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Forward to title control unless skipped via internal options if (!internalOptions?.skipTitleUpdate) { - this.titleAreaControl.beforeCloseEditor(editor); + this.titleControl.beforeCloseEditor(editor); } // Closing the active editor of the group is a bit more work @@ -1361,7 +1336,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Forward to title control unless skipped via internal options if (!internalOptions?.skipTitleUpdate) { - this.titleAreaControl.closeEditor(editor); + this.titleControl.closeEditor(editor); } } @@ -1711,7 +1686,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Forward to title control if (editors.length) { - this.titleAreaControl.closeEditors(editors); + this.titleControl.closeEditors(editors); } } @@ -1763,7 +1738,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Forward to title control if (editorsToClose.length) { - this.titleAreaControl.closeEditors(editorsToClose); + this.titleControl.closeEditors(editorsToClose); } } @@ -1922,16 +1897,16 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.lastLayout = { width, height, top, left }; this.element.classList.toggle('max-height-478px', height <= 478); - // Layout the title area first to receive the size it occupies - const titleAreaSize = this.titleAreaControl.layout({ + // Layout the title control first to receive the size it occupies + const titleControlSize = this.titleControl.layout({ container: new Dimension(width, height), available: new Dimension(width, height - this.editorPane.minimumHeight) }); // Pass the container width and remaining height to the editor layout - const editorHeight = Math.max(0, height - titleAreaSize.height); + const editorHeight = Math.max(0, height - titleControlSize.height); this.editorContainer.style.height = `${editorHeight}px`; - this.editorPane.layout({ width, height: editorHeight, top: top + titleAreaSize.height, left }); + this.editorPane.layout({ width, height: editorHeight, top: top + titleControlSize.height, left }); } relayout(): void { @@ -1956,8 +1931,6 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this._onWillDispose.fire(); - this.titleAreaControl.dispose(); - super.dispose(); } } diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index a73ebcd2053..5b9869395f9 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -42,7 +42,7 @@ import { IPreferencesService } from 'vs/workbench/services/preferences/common/pr import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; import { getIconClassesForLanguageId } from 'vs/editor/common/services/getIconClasses'; import { Promises, timeout } from 'vs/base/common/async'; -import { Event } from 'vs/base/common/event'; +import { Emitter, Event } from 'vs/base/common/event'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment, IStatusbarEntry } from 'vs/workbench/services/statusbar/browser/statusbar'; import { IMarker, IMarkerService, MarkerSeverity, IMarkerData } from 'vs/platform/markers/common/markers'; @@ -54,7 +54,7 @@ import { Action2 } from 'vs/platform/actions/common/actions'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { TabFocusMode } from 'vs/workbench/browser/parts/editor/tabFocus'; +import { TabFocus } from 'vs/editor/browser/config/tabFocus'; class SideBySideEditorEncodingSupport implements IEncodingSupport { constructor(private primary: IEncodingSupport, private secondary: IEncodingSupport) { } @@ -278,6 +278,36 @@ class State { } } +class TabFocusMode extends Disposable { + + private readonly _onDidChange = this._register(new Emitter()); + readonly onDidChange = this._onDidChange.event; + + constructor(@IConfigurationService private readonly configurationService: IConfigurationService) { + super(); + + this.registerListeners(); + + const tabFocusModeConfig = configurationService.getValue('editor.tabFocusMode') === true ? true : false; + TabFocus.setTabFocusMode(tabFocusModeConfig); + + this._onDidChange.fire(tabFocusModeConfig); + } + + private registerListeners(): void { + this._register(TabFocus.onDidChangeTabFocus(tabFocusMode => this._onDidChange.fire(tabFocusMode))); + + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('editor.tabFocusMode')) { + const tabFocusModeConfig = this.configurationService.getValue('editor.tabFocusMode') === true ? true : false; + TabFocus.setTabFocusMode(tabFocusModeConfig); + + this._onDidChange.fire(tabFocusModeConfig); + } + })); + } +} + const nlsSingleSelectionRange = localize('singleSelectionRange', "Ln {0}, Col {1} ({2} selected)"); const nlsSingleSelection = localize('singleSelection', "Ln {0}, Col {1}"); const nlsMultiSelectionRange = localize('multiSelectionRange', "{0} selections ({1} characters selected)"); diff --git a/src/vs/workbench/browser/parts/editor/titleControl.ts b/src/vs/workbench/browser/parts/editor/editorTabsControl.ts similarity index 83% rename from src/vs/workbench/browser/parts/editor/titleControl.ts rename to src/vs/workbench/browser/parts/editor/editorTabsControl.ts index 22187e0daee..67e142dea05 100644 --- a/src/vs/workbench/browser/parts/editor/titleControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorTabsControl.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./media/titlecontrol'; +import 'vs/css!./media/editortabscontrol'; import { localize } from 'vs/nls'; import { applyDragImage, DataTransfers } from 'vs/base/browser/dnd'; import { addDisposableListener, Dimension, EventType } from 'vs/base/browser/dom'; @@ -11,10 +11,9 @@ import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { ActionsOrientation, IActionViewItem, prepareActions } from 'vs/base/browser/ui/actionbar/actionbar'; import { IAction, SubmenuAction, ActionRunner } from 'vs/base/common/actions'; import { ResolvedKeybinding } from 'vs/base/common/keybindings'; -import { dispose, DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { createActionViewItem, createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -25,14 +24,11 @@ import { listActiveSelectionBackground, listActiveSelectionForeground } from 'vs import { IThemeService, Themable } from 'vs/platform/theme/common/themeService'; import { DraggedEditorGroupIdentifier, DraggedEditorIdentifier, fillEditorsDragData } from 'vs/workbench/browser/dnd'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; -import { BreadcrumbsConfig } from 'vs/workbench/browser/parts/editor/breadcrumbs'; -import { BreadcrumbsControl, IBreadcrumbsControlOptions } from 'vs/workbench/browser/parts/editor/breadcrumbsControl'; -import { IEditorGroupsAccessor, IEditorGroupTitleHeight, IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; +import { IEditorGroupsAccessor, IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; import { IEditorCommandsContext, EditorResourceAccessor, IEditorPartOptions, SideBySideEditor, EditorsOrder, EditorInputCapabilities } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { ResourceContextKey, ActiveEditorPinnedContext, ActiveEditorStickyContext, ActiveEditorGroupLockedContext, ActiveEditorCanSplitInGroupContext, SideBySideEditorActiveContext, ActiveEditorLastInGroupContext, ActiveEditorFirstInGroupContext, ActiveEditorAvailableEditorIdsContext, applyAvailableEditorIds } from 'vs/workbench/common/contextkeys'; import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; -import { IFileService } from 'vs/platform/files/common/files'; import { assertIsDefined } from 'vs/base/common/types'; import { isFirefox } from 'vs/base/browser/browser'; import { isCancellationError } from 'vs/base/common/errors'; @@ -41,24 +37,11 @@ import { WorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; import { LocalSelectionTransfer } from 'vs/platform/dnd/browser/dnd'; import { DraggedTreeItemsIdentifier } from 'vs/editor/common/services/treeViewsDnd'; import { IEditorResolverService } from 'vs/workbench/services/editor/common/editorResolverService'; +import { IEditorTitleControlDimensions } from 'vs/workbench/browser/parts/editor/editorTitleControl'; export interface IToolbarActions { - primary: IAction[]; - secondary: IAction[]; -} - -export interface ITitleControlDimensions { - - /** - * The size of the parent container the title control is layed out in. - */ - container: Dimension; - - /** - * The maximum size the title control is allowed to consume based on - * other controls that are positioned inside the container. - */ - available: Dimension; + readonly primary: IAction[]; + readonly secondary: IAction[]; } export class EditorCommandsContextActionRunner extends ActionRunner { @@ -87,19 +70,17 @@ export class EditorCommandsContextActionRunner extends ActionRunner { } } -export abstract class TitleControl extends Themable { +export abstract class EditorTabsControl extends Themable { protected readonly editorTransfer = LocalSelectionTransfer.getInstance(); protected readonly groupTransfer = LocalSelectionTransfer.getInstance(); protected readonly treeItemsTransfer = LocalSelectionTransfer.getInstance(); - private static readonly EDITOR_TITLE_HEIGHT = { - normal: 35, - compact: 22 + private static readonly EDITOR_TAB_HEIGHT = { + normal: 35 as const, + compact: 22 as const }; - protected breadcrumbsControl: BreadcrumbsControl | undefined = undefined; - private editorActionsToolbar: WorkbenchToolBar | undefined; private resourceContext: ResourceContextKey; @@ -131,8 +112,6 @@ export abstract class TitleControl extends Themable { @IMenuService private readonly menuService: IMenuService, @IQuickInputService protected quickInputService: IQuickInputService, @IThemeService themeService: IThemeService, - @IConfigurationService protected configurationService: IConfigurationService, - @IFileService private readonly fileService: IFileService, @IEditorResolverService private readonly editorResolverService: IEditorResolverService ) { super(themeService); @@ -156,41 +135,9 @@ export abstract class TitleControl extends Themable { } protected create(parent: HTMLElement): void { - this.updateTitleHeight(); + this.updateTabHeight(); } - protected createBreadcrumbsControl(container: HTMLElement, options: IBreadcrumbsControlOptions): void { - const config = this._register(BreadcrumbsConfig.IsEnabled.bindTo(this.configurationService)); - this._register(config.onDidChange(() => { - const value = config.getValue(); - if (!value && this.breadcrumbsControl) { - this.breadcrumbsControl.dispose(); - this.breadcrumbsControl = undefined; - this.handleBreadcrumbsEnablementChange(); - } else if (value && !this.breadcrumbsControl) { - this.breadcrumbsControl = this.instantiationService.createInstance(BreadcrumbsControl, container, options, this.group); - this.breadcrumbsControl.update(); - this.handleBreadcrumbsEnablementChange(); - } - })); - - if (config.getValue()) { - this.breadcrumbsControl = this.instantiationService.createInstance(BreadcrumbsControl, container, options, this.group); - } - - this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(e => { - if (this.breadcrumbsControl?.model && this.breadcrumbsControl.model.resource.scheme !== e.scheme) { - // ignore if the scheme of the breadcrumbs resource is not affected - return; - } - if (this.breadcrumbsControl?.update()) { - this.handleBreadcrumbsEnablementChange(); - } - })); - } - - protected abstract handleBreadcrumbsEnablementChange(): void; - protected createEditorActionsToolBar(container: HTMLElement): void { const context: IEditorCommandsContext = { groupId: this.group.id }; @@ -429,24 +376,25 @@ export abstract class TitleControl extends Themable { return keybinding ? keybinding.getLabel() ?? undefined : undefined; } - protected get titleHeight() { - return this.accessor.partOptions.tabHeight !== 'compact' ? TitleControl.EDITOR_TITLE_HEIGHT.normal : TitleControl.EDITOR_TITLE_HEIGHT.compact; + protected get tabHeight() { + return this.accessor.partOptions.tabHeight !== 'compact' ? EditorTabsControl.EDITOR_TAB_HEIGHT.normal : EditorTabsControl.EDITOR_TAB_HEIGHT.compact; } - protected updateTitleHeight(): void { - this.parent.style.setProperty('--editor-group-title-height', `${this.titleHeight}px`); + protected updateTabHeight(): void { + this.parent.style.setProperty('--editor-group-tab-height', `${this.tabHeight}px`); } updateOptions(oldOptions: IEditorPartOptions, newOptions: IEditorPartOptions): void { - // Update title height + + // Update tab height if (oldOptions.tabHeight !== newOptions.tabHeight) { - this.updateTitleHeight(); + this.updateTabHeight(); } } - abstract openEditor(editor: EditorInput): void; + abstract openEditor(editor: EditorInput): boolean; - abstract openEditors(editors: EditorInput[]): void; + abstract openEditors(editors: EditorInput[]): boolean; abstract beforeCloseEditor(editor: EditorInput): void; @@ -468,14 +416,7 @@ export abstract class TitleControl extends Themable { abstract updateEditorDirty(editor: EditorInput): void; - abstract layout(dimensions: ITitleControlDimensions): Dimension; + abstract layout(dimensions: IEditorTitleControlDimensions): Dimension; - abstract getHeight(): IEditorGroupTitleHeight; - - override dispose(): void { - dispose(this.breadcrumbsControl); - this.breadcrumbsControl = undefined; - - super.dispose(); - } + abstract getHeight(): number; } diff --git a/src/vs/workbench/browser/parts/editor/editorTitleControl.ts b/src/vs/workbench/browser/parts/editor/editorTitleControl.ts new file mode 100644 index 00000000000..ecdc4f67a57 --- /dev/null +++ b/src/vs/workbench/browser/parts/editor/editorTitleControl.ts @@ -0,0 +1,207 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./media/editortitlecontrol'; +import { Dimension, clearNode } from 'vs/base/browser/dom'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IThemeService, Themable } from 'vs/platform/theme/common/themeService'; +import { BreadcrumbsControl, BreadcrumbsControlFactory } from 'vs/workbench/browser/parts/editor/breadcrumbsControl'; +import { IEditorGroupsAccessor, IEditorGroupTitleHeight, IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; +import { EditorTabsControl } from 'vs/workbench/browser/parts/editor/editorTabsControl'; +import { MultiEditorTabsControl } from 'vs/workbench/browser/parts/editor/multiEditorTabsControl'; +import { SingleEditorTabsControl } from 'vs/workbench/browser/parts/editor/singleEditorTabsControl'; +import { IEditorPartOptions } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { DisposableStore } from 'vs/base/common/lifecycle'; + +export interface IEditorTitleControlDimensions { + + /** + * The size of the parent container the title control is layed out in. + */ + readonly container: Dimension; + + /** + * The maximum size the title control is allowed to consume based on + * other controls that are positioned inside the container. + */ + readonly available: Dimension; +} + +export class EditorTitleControl extends Themable { + + private editorTabsControl: EditorTabsControl; + private editorTabsControlDisposable = this._register(new DisposableStore()); + + private breadcrumbsControlFactory: BreadcrumbsControlFactory | undefined; + private breadcrumbsControlDisposables = this._register(new DisposableStore()); + private get breadcrumbsControl() { return this.breadcrumbsControlFactory?.control; } + + constructor( + private parent: HTMLElement, + private accessor: IEditorGroupsAccessor, + private group: IEditorGroupView, + @IInstantiationService private instantiationService: IInstantiationService, + @IThemeService themeService: IThemeService + ) { + super(themeService); + + this.editorTabsControl = this.createEditorTabsControl(); + this.breadcrumbsControlFactory = this.createBreadcrumbsControl(); + } + + private createEditorTabsControl(): EditorTabsControl { + let control: EditorTabsControl; + if (this.accessor.partOptions.showTabs) { + control = this.instantiationService.createInstance(MultiEditorTabsControl, this.parent, this.accessor, this.group); + } else { + control = this.instantiationService.createInstance(SingleEditorTabsControl, this.parent, this.accessor, this.group); + } + + return this.editorTabsControlDisposable.add(control); + } + + private createBreadcrumbsControl(): BreadcrumbsControlFactory | undefined { + if (!this.accessor.partOptions.showTabs) { + return undefined; // single tabs have breadcrumbs inlined + } + + // Breadcrumbs container + const breadcrumbsContainer = document.createElement('div'); + breadcrumbsContainer.classList.add('breadcrumbs-below-tabs'); + this.parent.appendChild(breadcrumbsContainer); + + const breadcrumbsControlFactory = this.breadcrumbsControlDisposables.add(this.instantiationService.createInstance(BreadcrumbsControlFactory, breadcrumbsContainer, this.group, { + showFileIcons: true, + showSymbolIcons: true, + showDecorationColors: false, + showPlaceholder: true + })); + this.breadcrumbsControlDisposables.add(breadcrumbsControlFactory.onDidEnablementChange(() => this.handleBreadcrumbsEnablementChange())); + + return breadcrumbsControlFactory; + } + + private handleBreadcrumbsEnablementChange(): void { + this.group.relayout(); // relayout when breadcrumbs are enable/disabled + } + + openEditor(editor: EditorInput): void { + const didChange = this.editorTabsControl.openEditor(editor); + + this.handleOpenedEditors(didChange); + } + + openEditors(editors: EditorInput[]): void { + const didChange = this.editorTabsControl.openEditors(editors); + + this.handleOpenedEditors(didChange); + } + + private handleOpenedEditors(didChange: boolean): void { + if (didChange) { + this.breadcrumbsControl?.update(); + } else { + this.breadcrumbsControl?.revealLast(); + } + } + + beforeCloseEditor(editor: EditorInput): void { + return this.editorTabsControl.beforeCloseEditor(editor); + } + + closeEditor(editor: EditorInput): void { + this.editorTabsControl.closeEditor(editor); + + this.handleClosedEditors(); + } + + closeEditors(editors: EditorInput[]): void { + this.editorTabsControl.closeEditors(editors); + + this.handleClosedEditors(); + } + + private handleClosedEditors(): void { + if (!this.group.activeEditor) { + this.breadcrumbsControl?.update(); + } + } + + moveEditor(editor: EditorInput, fromIndex: number, targetIndex: number): void { + return this.editorTabsControl.moveEditor(editor, fromIndex, targetIndex); + } + + pinEditor(editor: EditorInput): void { + return this.editorTabsControl.pinEditor(editor); + } + + stickEditor(editor: EditorInput): void { + return this.editorTabsControl.stickEditor(editor); + } + + unstickEditor(editor: EditorInput): void { + return this.editorTabsControl.unstickEditor(editor); + } + + setActive(isActive: boolean): void { + return this.editorTabsControl.setActive(isActive); + } + + updateEditorLabel(editor: EditorInput): void { + return this.editorTabsControl.updateEditorLabel(editor); + } + + updateEditorDirty(editor: EditorInput): void { + return this.editorTabsControl.updateEditorDirty(editor); + } + + updateOptions(oldOptions: IEditorPartOptions, newOptions: IEditorPartOptions): void { + + // Update editor tabs control if options changed + if (oldOptions.showTabs !== newOptions.showTabs) { + + // Clear old + this.editorTabsControlDisposable.clear(); + this.breadcrumbsControlDisposables.clear(); + clearNode(this.parent); + + // Create new + this.editorTabsControl = this.createEditorTabsControl(); + this.breadcrumbsControlFactory = this.createBreadcrumbsControl(); + } + + // Forward into editor tabs control + this.editorTabsControl.updateOptions(oldOptions, newOptions); + } + + layout(dimensions: IEditorTitleControlDimensions): Dimension { + + // Layout tabs control + const tabsControlDimension = this.editorTabsControl.layout(dimensions); + + // Layout breadcrumbs if visible + let breadcrumbsControlDimension: Dimension | undefined = undefined; + if (this.breadcrumbsControl?.isHidden() === false) { + breadcrumbsControlDimension = new Dimension(dimensions.container.width, BreadcrumbsControl.HEIGHT); + this.breadcrumbsControl.layout(breadcrumbsControlDimension); + } + + return new Dimension( + dimensions.container.width, + tabsControlDimension.height + (breadcrumbsControlDimension ? breadcrumbsControlDimension.height : 0) + ); + } + + getHeight(): IEditorGroupTitleHeight { + const tabsControlHeight = this.editorTabsControl.getHeight(); + const breadcrumbsControlHeight = this.breadcrumbsControl?.isHidden() === false ? BreadcrumbsControl.HEIGHT : 0; + + return { + total: tabsControlHeight + breadcrumbsControlHeight, + offset: tabsControlHeight + }; + } +} diff --git a/src/vs/workbench/browser/parts/editor/media/titlecontrol.css b/src/vs/workbench/browser/parts/editor/media/editortabscontrol.css similarity index 95% rename from src/vs/workbench/browser/parts/editor/media/titlecontrol.css rename to src/vs/workbench/browser/parts/editor/media/editortabscontrol.css index 0b22793aacd..bf44c02aee2 100644 --- a/src/vs/workbench/browser/parts/editor/media/titlecontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/editortabscontrol.css @@ -30,7 +30,7 @@ } .monaco-workbench .part.editor > .content .editor-group-container > .title .monaco-icon-label::before { - height: var(--editor-group-title-height); /* tweak the icon size of the editor labels when icons are enabled */ + height: var(--editor-group-tab-height); /* tweak the icon size of the editor labels when icons are enabled */ } .monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .monaco-icon-label::after, diff --git a/src/vs/workbench/browser/parts/editor/media/editortitlecontrol.css b/src/vs/workbench/browser/parts/editor/media/editortitlecontrol.css new file mode 100644 index 00000000000..a24f76131a8 --- /dev/null +++ b/src/vs/workbench/browser/parts/editor/media/editortitlecontrol.css @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* Breadcrumbs (below multiple editor tabs) */ + +.monaco-workbench .part.editor > .content .editor-group-container > .title .breadcrumbs-below-tabs .breadcrumbs-control { + flex: 1 100%; + height: 22px; + cursor: default; +} + +.monaco-workbench .part.editor > .content .editor-group-container > .title .breadcrumbs-below-tabs .breadcrumbs-control .monaco-icon-label { + height: 22px; + line-height: 22px; +} + +.monaco-workbench .part.editor > .content .editor-group-container > .title .breadcrumbs-below-tabs .breadcrumbs-control .monaco-icon-label::before { + height: 22px; /* tweak the icon size of the editor labels when icons are enabled */ +} + +.monaco-workbench .part.editor > .content .editor-group-container > .title .breadcrumbs-below-tabs .breadcrumbs-control .outline-element-icon { + padding-right: 3px; + height: 22px; /* tweak the icon size of the editor labels when icons are enabled */ + line-height: 22px; +} + +.monaco-workbench .part.editor > .content .editor-group-container > .title .breadcrumbs-below-tabs .breadcrumbs-control .monaco-breadcrumb-item { + max-width: 80%; +} + +.monaco-workbench .part.editor > .content .editor-group-container > .title .breadcrumbs-below-tabs .breadcrumbs-control .monaco-breadcrumb-item::before { + width: 16px; + height: 22px; + display: flex; + align-items: center; + justify-content: center; +} + +.monaco-workbench .part.editor > .content .editor-group-container > .title .breadcrumbs-below-tabs .breadcrumbs-control .monaco-breadcrumb-item:last-child { + padding-right: 8px; +} + +.monaco-workbench .part.editor > .content .editor-group-container > .title .breadcrumbs-below-tabs .breadcrumbs-control .monaco-breadcrumb-item:last-child .codicon:last-child { + display: none; /* hides chevrons when last item */ +} diff --git a/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css b/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css similarity index 92% rename from src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css rename to src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css index f5b39db85fc..d51863edf23 100644 --- a/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css @@ -71,7 +71,7 @@ .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container { display: flex; - height: var(--editor-group-title-height); + height: var(--editor-group-tab-height); scrollbar-width: none; /* Firefox: hide scrollbar */ } @@ -97,7 +97,7 @@ display: flex; white-space: nowrap; cursor: pointer; - height: var(--editor-group-title-height); + height: var(--editor-group-tab-height); box-sizing: border-box; padding-left: 10px; } @@ -265,7 +265,7 @@ .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab .tab-label { margin-top: auto; margin-bottom: auto; - line-height: var(--editor-group-title-height); /* aligns icon and label vertically centered in the tab */ + line-height: var(--editor-group-tab-height); /* aligns icon and label vertically centered in the tab */ } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink .tab-label, @@ -424,56 +424,13 @@ pointer-events: none; /* don't allow tab actions to be clicked when running without tab actions */ } -/* Breadcrumbs */ - -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control { - flex: 1 100%; - height: 22px; - cursor: default; -} - -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-icon-label { - height: 22px; - line-height: 22px; -} - -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-icon-label::before { - height: 22px; /* tweak the icon size of the editor labels when icons are enabled */ -} - -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .outline-element-icon { - padding-right: 3px; - height: 22px; /* tweak the icon size of the editor labels when icons are enabled */ - line-height: 22px; -} - -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item { - max-width: 80%; -} - -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item::before { - width: 16px; - height: 22px; - display: flex; - align-items: center; - justify-content: center; -} - -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item:last-child { - padding-right: 8px; -} - -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item:last-child .codicon:last-child { - display: none; /* hides chevrons when last item */ -} - /* Editor Actions Toolbar */ .monaco-workbench .part.editor > .content .editor-group-container > .title .editor-actions { cursor: default; flex: initial; padding: 0 8px 0 4px; - height: var(--editor-group-title-height); + height: var(--editor-group-tab-height); } .monaco-workbench .part.editor > .content .editor-group-container > .title .editor-actions .action-item { diff --git a/src/vs/workbench/browser/parts/editor/media/notabstitlecontrol.css b/src/vs/workbench/browser/parts/editor/media/singleeditortabscontrol.css similarity index 96% rename from src/vs/workbench/browser/parts/editor/media/notabstitlecontrol.css rename to src/vs/workbench/browser/parts/editor/media/singleeditortabscontrol.css index 81562a81281..893920c71c5 100644 --- a/src/vs/workbench/browser/parts/editor/media/notabstitlecontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/singleeditortabscontrol.css @@ -6,7 +6,7 @@ /* Title Label */ .monaco-workbench .part.editor > .content .editor-group-container > .title > .label-container { - height: var(--editor-group-title-height); + height: var(--editor-group-tab-height); display: flex; justify-content: flex-start; align-items: center; @@ -15,7 +15,7 @@ } .monaco-workbench .part.editor > .content .editor-group-container > .title > .label-container > .title-label { - line-height: var(--editor-group-title-height); + line-height: var(--editor-group-tab-height); overflow: hidden; text-overflow: ellipsis; position: relative; @@ -26,7 +26,7 @@ flex: initial; /* helps to show decorations right next to the label and not at the end while still preserving text overflow ellipsis */ } -/* Breadcrumbs */ +/* Breadcrumbs (inline next to single editor tab) */ .monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .no-tabs.title-label { flex: none; @@ -94,7 +94,7 @@ flex: initial; opacity: 0.5; padding-right: 8px; - height: var(--editor-group-title-height); + height: var(--editor-group-tab-height); } .monaco-workbench .part.editor > .content .editor-group-container > .title > .title-actions .action-item { diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts similarity index 94% rename from src/vs/workbench/browser/parts/editor/tabsTitleControl.ts rename to src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts index 5d6b14d633a..e3e4de2bc0e 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./media/tabstitlecontrol'; +import 'vs/css!./media/multieditortabscontrol'; import { isMacintosh, isWindows } from 'vs/base/common/platform'; import { shorten } from 'vs/base/common/labels'; import { EditorResourceAccessor, GroupIdentifier, Verbosity, IEditorPartOptions, SideBySideEditor, DEFAULT_EDITOR_ASSOCIATION, EditorInputCapabilities, IUntypedEditorInput, preventEditorClose, EditorCloseMethod } from 'vs/workbench/common/editor'; @@ -19,7 +19,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; -import { EditorCommandsContextActionRunner, ITitleControlDimensions, IToolbarActions, TitleControl } from 'vs/workbench/browser/parts/editor/titleControl'; +import { EditorCommandsContextActionRunner, IToolbarActions, EditorTabsControl } from 'vs/workbench/browser/parts/editor/editorTabsControl'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IDisposable, dispose, DisposableStore, combinedDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; @@ -34,11 +34,8 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { MergeGroupMode, IMergeGroupOptions, GroupsArrangement, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { addDisposableListener, EventType, EventHelper, Dimension, scheduleAtNextAnimationFrame, findParentWithClass, clearNode, DragAndDropObserver } from 'vs/base/browser/dom'; import { localize } from 'vs/nls'; -import { IEditorGroupsAccessor, IEditorGroupView, EditorServiceImpl, IEditorGroupTitleHeight } from 'vs/workbench/browser/parts/editor/editor'; +import { IEditorGroupsAccessor, IEditorGroupView, EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor'; import { CloseOneEditorAction, UnpinEditorAction } from 'vs/workbench/browser/parts/editor/editorActions'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { BreadcrumbsControl } from 'vs/workbench/browser/parts/editor/breadcrumbsControl'; -import { IFileService } from 'vs/platform/files/common/files'; import { assertAllDefined, assertIsDefined } from 'vs/base/common/types'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { basenameOrAuthority } from 'vs/base/common/resources'; @@ -55,46 +52,47 @@ import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { ITreeViewsDnDService } from 'vs/editor/common/services/treeViewsDndService'; import { DraggedTreeItemsIdentifier } from 'vs/editor/common/services/treeViewsDnd'; import { IEditorResolverService } from 'vs/workbench/services/editor/common/editorResolverService'; +import { IEditorTitleControlDimensions } from 'vs/workbench/browser/parts/editor/editorTitleControl'; interface IEditorInputLabel { - editor: EditorInput; + readonly editor: EditorInput; - name?: string; + readonly name?: string; description?: string; - forceDescription?: boolean; - title?: string; - ariaLabel?: string; + readonly forceDescription?: boolean; + readonly title?: string; + readonly ariaLabel?: string; } -interface ITabsTitleControlLayoutOptions { +interface IMultiEditorTabsControlLayoutOptions { /** * Whether to force revealing the active tab, even when * the dimensions have not changed. This can be the case * when a tab was made active and needs to be revealed. */ - forceRevealActiveTab?: true; + readonly forceRevealActiveTab?: true; } -interface IScheduledTabsTitleControlLayout extends IDisposable { +interface IScheduledMultiEditorTabsControlLayout extends IDisposable { /** * Associated options with the layout call. */ - options?: ITabsTitleControlLayoutOptions; + options?: IMultiEditorTabsControlLayoutOptions; } -export class TabsTitleControl extends TitleControl { +export class MultiEditorTabsControl extends EditorTabsControl { private static readonly SCROLLBAR_SIZES = { - default: 3, - large: 10 + default: 3 as const, + large: 10 as const }; private static readonly TAB_WIDTH = { - compact: 38, - shrink: 80, - fit: 120 + compact: 38 as const, + shrink: 80 as const, + fit: 120 as const }; private static readonly DRAG_OVER_OPEN_TAB_THRESHOLD = 1500; @@ -119,12 +117,12 @@ export class TabsTitleControl extends TitleControl { private tabActionBars: ActionBar[] = []; private tabDisposables: IDisposable[] = []; - private dimensions: ITitleControlDimensions & { used?: Dimension } = { + private dimensions: IEditorTitleControlDimensions & { used?: Dimension } = { container: Dimension.None, available: Dimension.None }; - private readonly layoutScheduler = this._register(new MutableDisposable()); + private readonly layoutScheduler = this._register(new MutableDisposable()); private blockRevealActiveTab: boolean | undefined; private path: IPath = isWindows ? win32 : posix; @@ -144,15 +142,13 @@ export class TabsTitleControl extends TitleControl { @IMenuService menuService: IMenuService, @IQuickInputService quickInputService: IQuickInputService, @IThemeService themeService: IThemeService, - @IConfigurationService configurationService: IConfigurationService, - @IFileService fileService: IFileService, @IEditorService private readonly editorService: EditorServiceImpl, @IPathService private readonly pathService: IPathService, @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @ITreeViewsDnDService private readonly treeViewsDragAndDropService: ITreeViewsDnDService, @IEditorResolverService editorResolverService: IEditorResolverService ) { - super(parent, accessor, group, contextMenuService, instantiationService, contextKeyService, keybindingService, notificationService, menuService, quickInputService, themeService, configurationService, fileService, editorResolverService); + super(parent, accessor, group, contextMenuService, instantiationService, contextKeyService, keybindingService, notificationService, menuService, quickInputService, themeService, editorResolverService); // Resolve the correct path library for the OS we are on // If we are connected to remote, this accounts for the @@ -197,12 +193,6 @@ export class TabsTitleControl extends TitleControl { // Editor Actions Toolbar this.createEditorActionsToolBar(this.editorToolbarContainer); - - // Breadcrumbs - const breadcrumbsContainer = document.createElement('div'); - breadcrumbsContainer.classList.add('tabs-breadcrumbs'); - this.titleContainer.appendChild(breadcrumbsContainer); - this.createBreadcrumbsControl(breadcrumbsContainer, { showFileIcons: true, showSymbolIcons: true, showDecorationColors: false, showPlaceholder: true }); } private createTabsScrollbar(scrollable: HTMLElement): ScrollableElement { @@ -271,10 +261,10 @@ export class TabsTitleControl extends TitleControl { private getTabsScrollbarSizing(): number { if (this.accessor.partOptions.titleScrollbarSizing !== 'large') { - return TabsTitleControl.SCROLLBAR_SIZES.default; + return MultiEditorTabsControl.SCROLLBAR_SIZES.default; } - return TabsTitleControl.SCROLLBAR_SIZES.large; + return MultiEditorTabsControl.SCROLLBAR_SIZES.large; } private registerTabsContainerListeners(tabsContainer: HTMLElement, tabsScrollbar: ScrollableElement): void { @@ -421,7 +411,7 @@ export class TabsTitleControl extends TitleControl { // The restriction is relaxed according to the absolute value of `deltaX` and `deltaY` // to support discrete (mouse wheel) and contiguous scrolling (touchpad) equally well const now = Date.now(); - if (now - this.lastMouseWheelEventTime < TabsTitleControl.MOUSE_WHEEL_EVENT_THRESHOLD - 2 * (Math.abs(e.deltaX) + Math.abs(e.deltaY))) { + if (now - this.lastMouseWheelEventTime < MultiEditorTabsControl.MOUSE_WHEEL_EVENT_THRESHOLD - 2 * (Math.abs(e.deltaX) + Math.abs(e.deltaY))) { return; } @@ -429,9 +419,9 @@ export class TabsTitleControl extends TitleControl { // Figure out scrolling direction but ignore it if too subtle let tabSwitchDirection: number; - if (e.deltaX + e.deltaY < - TabsTitleControl.MOUSE_WHEEL_DISTANCE_THRESHOLD) { + if (e.deltaX + e.deltaY < - MultiEditorTabsControl.MOUSE_WHEEL_DISTANCE_THRESHOLD) { tabSwitchDirection = -1; - } else if (e.deltaX + e.deltaY > TabsTitleControl.MOUSE_WHEEL_DISTANCE_THRESHOLD) { + } else if (e.deltaX + e.deltaY > MultiEditorTabsControl.MOUSE_WHEEL_DISTANCE_THRESHOLD) { tabSwitchDirection = 1; } else { return; @@ -490,15 +480,15 @@ export class TabsTitleControl extends TitleControl { this.layout(this.dimensions); } - openEditor(editor: EditorInput): void { - this.handleOpenedEditors(); + openEditor(editor: EditorInput): boolean { + return this.handleOpenedEditors(); } - openEditors(editors: EditorInput[]): void { - this.handleOpenedEditors(); + openEditors(editors: EditorInput[]): boolean { + return this.handleOpenedEditors(); } - private handleOpenedEditors(): void { + private handleOpenedEditors(): boolean { // Create tabs as needed const [tabsContainer, tabsScrollbar] = assertAllDefined(this.tabsContainer, this.tabsScrollbar); @@ -508,7 +498,7 @@ export class TabsTitleControl extends TitleControl { // Make sure to recompute tab labels and detect // if a label change occurred that requires a - // redraw of tabs and update of breadcrumbs. + // redraw of tabs. const activeEditorChanged = this.didActiveEditorChange(); const oldActiveTabLabel = this.activeTabLabel; @@ -516,20 +506,22 @@ export class TabsTitleControl extends TitleControl { this.computeTabLabels(); // Redraw and update in these cases + let didChange = false; if ( activeEditorChanged || // active editor changed oldTabLabelsLength !== this.tabLabels.length || // number of tabs changed !this.equalsEditorInputLabel(oldActiveTabLabel, this.activeTabLabel) // active editor label changed ) { this.redraw({ forceRevealActiveTab: true }); - this.breadcrumbsControl?.update(); + didChange = true; } - // Otherwise only layout for revealing in tabs and breadcrumbs + // Otherwise only layout for revealing else { this.layout(this.dimensions, { forceRevealActiveTab: true }); - this.breadcrumbsControl?.revealLast(); } + + return didChange; } private didActiveEditorChange(): boolean { @@ -617,8 +609,6 @@ export class TabsTitleControl extends TitleControl { this.tabActionBars = []; this.clearEditorActionsToolbar(); - - this.breadcrumbsControl?.update(); } } @@ -808,13 +798,13 @@ export class TabsTitleControl extends TitleControl { const tabActionRunner = new EditorCommandsContextActionRunner({ groupId: this.group.id, editorIndex: index }); const tabActionBar = new ActionBar(tabActionsContainer, { ariaLabel: localize('ariaLabelTabActions', "Tab actions"), actionRunner: tabActionRunner }); - tabActionBar.onWillRun(e => { + const tabActionListener = tabActionBar.onWillRun(e => { if (e.action.id === this.closeEditorAction.id) { this.blockRevealActiveTabOnce(); } }); - const tabActionBarDisposable = combinedDisposable(tabActionBar, toDisposable(insert(this.tabActionBars, tabActionBar))); + const tabActionBarDisposable = combinedDisposable(tabActionBar, tabActionListener, toDisposable(insert(this.tabActionBars, tabActionBar))); // Tab Border Bottom const tabBorderBottomContainer = document.createElement('div'); @@ -1052,7 +1042,7 @@ export class TabsTitleControl extends TitleControl { }, onDragOver: (_, dragDuration) => { - if (dragDuration >= TabsTitleControl.DRAG_OVER_OPEN_TAB_THRESHOLD) { + if (dragDuration >= MultiEditorTabsControl.DRAG_OVER_OPEN_TAB_THRESHOLD) { const draggedOverTab = this.group.getEditorByIndex(index); if (draggedOverTab && this.group.activeEditor !== draggedOverTab) { this.group.openEditor(draggedOverTab, { preserveFocus: true }); @@ -1089,7 +1079,7 @@ export class TabsTitleControl extends TitleControl { if (Array.isArray(data)) { const group = data[0]; if (group.identifier === this.group.id) { - return false; // groups cannot be dropped on title area it originates from + return false; // groups cannot be dropped on group it originates from } } @@ -1251,7 +1241,7 @@ export class TabsTitleControl extends TitleControl { } } - private redraw(options?: ITabsTitleControlLayoutOptions): void { + private redraw(options?: IMultiEditorTabsControlLayoutOptions): void { // Border below tabs if any with explicit high contrast support if (this.tabsAndActionsContainer) { @@ -1322,10 +1312,10 @@ export class TabsTitleControl extends TitleControl { let stickyTabWidth = 0; switch (options.pinnedTabSizing) { case 'compact': - stickyTabWidth = TabsTitleControl.TAB_WIDTH.compact; + stickyTabWidth = MultiEditorTabsControl.TAB_WIDTH.compact; break; case 'shrink': - stickyTabWidth = TabsTitleControl.TAB_WIDTH.shrink; + stickyTabWidth = MultiEditorTabsControl.TAB_WIDTH.shrink; break; } @@ -1525,15 +1515,11 @@ export class TabsTitleControl extends TitleControl { } } - getHeight(): IEditorGroupTitleHeight { - const showsBreadcrumbs = this.breadcrumbsControl && !this.breadcrumbsControl.isHidden(); + getHeight(): number { // Return quickly if our used dimensions are known if (this.dimensions.used) { - return { - total: this.dimensions.used.height, - offset: showsBreadcrumbs ? this.dimensions.used.height - BreadcrumbsControl.HEIGHT : this.dimensions.used.height - }; + return this.dimensions.used.height; } // Otherwise compute via browser APIs @@ -1542,28 +1528,21 @@ export class TabsTitleControl extends TitleControl { } } - private computeHeight(): IEditorGroupTitleHeight { - let total: number; + private computeHeight(): number { + let height: number; // Wrap: we need to ask `offsetHeight` to get // the real height of the title area with wrapping. if (this.accessor.partOptions.wrapTabs && this.tabsAndActionsContainer?.classList.contains('wrapping')) { - total = this.tabsAndActionsContainer.offsetHeight; + height = this.tabsAndActionsContainer.offsetHeight; } else { - total = this.titleHeight; + height = this.tabHeight; } - const offset = total; - - // Account for breadcrumbs if visible - if (this.breadcrumbsControl && !this.breadcrumbsControl.isHidden()) { - total += BreadcrumbsControl.HEIGHT; // Account for breadcrumbs if visible - } - - return { total, offset }; + return height; } - layout(dimensions: ITitleControlDimensions, options?: ITabsTitleControlLayoutOptions): Dimension { + layout(dimensions: IEditorTitleControlDimensions, options?: IMultiEditorTabsControlLayoutOptions): Dimension { // Remember dimensions that we get Object.assign(this.dimensions, dimensions); @@ -1591,21 +1570,18 @@ export class TabsTitleControl extends TitleControl { // First time layout: compute the dimensions and store it if (!this.dimensions.used) { - this.dimensions.used = new Dimension(dimensions.container.width, this.computeHeight().total); + this.dimensions.used = new Dimension(dimensions.container.width, this.computeHeight()); } return this.dimensions.used; } - private doLayout(dimensions: ITitleControlDimensions, options?: ITabsTitleControlLayoutOptions): void { + private doLayout(dimensions: IEditorTitleControlDimensions, options?: IMultiEditorTabsControlLayoutOptions): void { // Only layout if we have valid tab index and dimensions const activeTabAndIndex = this.group.activeEditor ? this.getTabAndIndex(this.group.activeEditor) : undefined; if (activeTabAndIndex && dimensions.container !== Dimension.None && dimensions.available !== Dimension.None) { - // Breadcrumbs - this.doLayoutBreadcrumbs(dimensions); - // Tabs const [activeTab, activeIndex] = activeTabAndIndex; this.doLayoutTabs(activeTab, activeIndex, dimensions, options); @@ -1615,7 +1591,7 @@ export class TabsTitleControl extends TitleControl { // return it fast from the `layout` call without having to // compute it over and over again const oldDimension = this.dimensions.used; - const newDimension = this.dimensions.used = new Dimension(dimensions.container.width, this.computeHeight().total); + const newDimension = this.dimensions.used = new Dimension(dimensions.container.width, this.computeHeight()); // In case the height of the title control changed from before // (currently only possible if wrapping changed on/off), we need @@ -1626,17 +1602,7 @@ export class TabsTitleControl extends TitleControl { } } - protected handleBreadcrumbsEnablementChange(): void { - this.group.relayout(); // relayout when breadcrumbs are enable/disabled - } - - private doLayoutBreadcrumbs(dimensions: ITitleControlDimensions): void { - if (this.breadcrumbsControl && !this.breadcrumbsControl.isHidden()) { - this.breadcrumbsControl.layout(new Dimension(dimensions.container.width, BreadcrumbsControl.HEIGHT)); - } - } - - private doLayoutTabs(activeTab: HTMLElement, activeIndex: number, dimensions: ITitleControlDimensions, options?: ITabsTitleControlLayoutOptions): void { + private doLayoutTabs(activeTab: HTMLElement, activeIndex: number, dimensions: IEditorTitleControlDimensions, options?: IMultiEditorTabsControlLayoutOptions): void { // Always first layout tabs with wrapping support even if wrapping // is disabled. The result indicates if tabs wrap and if not, we @@ -1649,7 +1615,7 @@ export class TabsTitleControl extends TitleControl { } } - private doLayoutTabsWrapping(dimensions: ITitleControlDimensions): boolean { + private doLayoutTabsWrapping(dimensions: IEditorTitleControlDimensions): boolean { const [tabsAndActionsContainer, tabsContainer, editorToolbarContainer, tabsScrollbar] = assertAllDefined(this.tabsAndActionsContainer, this.tabsContainer, this.editorToolbarContainer, this.tabsScrollbar); // Handle wrapping tabs according to setting: @@ -1707,9 +1673,9 @@ export class TabsTitleControl extends TitleControl { // Tabs wrap multiline: remove wrapping under certain size constraint conditions if (tabsWrapMultiLine) { if ( - (tabsContainer.offsetHeight > dimensions.available.height) || // if height exceeds available height - (allTabsWidth === visibleTabsWidth && tabsContainer.offsetHeight === this.titleHeight) || // if wrapping is not needed anymore - (!lastTabFitsWrapped()) // if last tab does not fit anymore + (tabsContainer.offsetHeight > dimensions.available.height) || // if height exceeds available height + (allTabsWidth === visibleTabsWidth && tabsContainer.offsetHeight === this.tabHeight) || // if wrapping is not needed anymore + (!lastTabFitsWrapped()) // if last tab does not fit anymore ) { updateTabsWrapping(false); } @@ -1778,7 +1744,7 @@ export class TabsTitleControl extends TitleControl { return tabsWrapMultiLine; } - private doLayoutTabsNonWrapping(activeTab: HTMLElement, activeIndex: number, options?: ITabsTitleControlLayoutOptions): void { + private doLayoutTabsNonWrapping(activeTab: HTMLElement, activeIndex: number, options?: IMultiEditorTabsControlLayoutOptions): void { const [tabsContainer, tabsScrollbar] = assertAllDefined(this.tabsContainer, this.tabsScrollbar); // @@ -1809,10 +1775,10 @@ export class TabsTitleControl extends TitleControl { let stickyTabWidth = 0; switch (this.accessor.partOptions.pinnedTabSizing) { case 'compact': - stickyTabWidth = TabsTitleControl.TAB_WIDTH.compact; + stickyTabWidth = MultiEditorTabsControl.TAB_WIDTH.compact; break; case 'shrink': - stickyTabWidth = TabsTitleControl.TAB_WIDTH.shrink; + stickyTabWidth = MultiEditorTabsControl.TAB_WIDTH.shrink; break; } @@ -1827,7 +1793,7 @@ export class TabsTitleControl extends TitleControl { // is little enough that we need to disable sticky tabs sticky positioning // so that tabs can be scrolled at naturally. let availableTabsContainerWidth = visibleTabsWidth - stickyTabsWidth; - if (this.group.stickyCount > 0 && availableTabsContainerWidth < TabsTitleControl.TAB_WIDTH.fit) { + if (this.group.stickyCount > 0 && availableTabsContainerWidth < MultiEditorTabsControl.TAB_WIDTH.fit) { tabsContainer.classList.add('disable-sticky-tabs'); availableTabsContainerWidth = visibleTabsWidth; @@ -2153,7 +2119,7 @@ registerThemingParticipant((theme, collector) => { // Hover Border // // Unfortunately we need to copy a lot of CSS over from the - // tabsTitleControl.css because we want to reuse the same + // multiEditorTabsControl.css because we want to reuse the same // styles we already have for the normal bottom-border. const tabHoverBorder = theme.getColor(TAB_HOVER_BORDER); if (tabHoverBorder) { diff --git a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/singleEditorTabsControl.ts similarity index 88% rename from src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts rename to src/vs/workbench/browser/parts/editor/singleEditorTabsControl.ts index 0772d57a3ce..9912e00a295 100644 --- a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/singleEditorTabsControl.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./media/notabstitlecontrol'; +import 'vs/css!./media/singleeditortabscontrol'; import { EditorResourceAccessor, Verbosity, IEditorPartOptions, SideBySideEditor, preventEditorClose, EditorCloseMethod } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; -import { TitleControl, IToolbarActions, ITitleControlDimensions } from 'vs/workbench/browser/parts/editor/titleControl'; +import { EditorTabsControl, IToolbarActions } from 'vs/workbench/browser/parts/editor/editorTabsControl'; import { ResourceLabel, IResourceLabel } from 'vs/workbench/browser/labels'; import { TAB_ACTIVE_FOREGROUND, TAB_UNFOCUSED_ACTIVE_FOREGROUND } from 'vs/workbench/common/theme'; import { EventType as TouchEventType, GestureEvent, Gesture } from 'vs/base/browser/touch'; @@ -14,31 +14,33 @@ import { addDisposableListener, EventType, EventHelper, Dimension, isAncestor } import { CLOSE_EDITOR_COMMAND_ID, UNLOCK_GROUP_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; import { Color } from 'vs/base/common/color'; import { assertIsDefined, assertAllDefined } from 'vs/base/common/types'; -import { IEditorGroupTitleHeight } from 'vs/workbench/browser/parts/editor/editor'; import { equals } from 'vs/base/common/objects'; import { toDisposable } from 'vs/base/common/lifecycle'; import { defaultBreadcrumbsWidgetStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { IEditorTitleControlDimensions } from 'vs/workbench/browser/parts/editor/editorTitleControl'; +import { BreadcrumbsControlFactory } from 'vs/workbench/browser/parts/editor/breadcrumbsControl'; interface IRenderedEditorLabel { - editor?: EditorInput; - pinned: boolean; + readonly editor?: EditorInput; + readonly pinned: boolean; } -export class NoTabsTitleControl extends TitleControl { - - private static readonly HEIGHT = 35; +export class SingleEditorTabsControl extends EditorTabsControl { private titleContainer: HTMLElement | undefined; private editorLabel: IResourceLabel | undefined; private activeLabel: IRenderedEditorLabel = Object.create(null); + private breadcrumbsControlFactory: BreadcrumbsControlFactory | undefined; + private get breadcrumbsControl() { return this.breadcrumbsControlFactory?.control; } + protected override create(parent: HTMLElement): void { super.create(parent); const titleContainer = this.titleContainer = parent; titleContainer.draggable = true; - //Container listeners + // Container listeners this.registerContainerListeners(titleContainer); // Gesture Support @@ -53,7 +55,14 @@ export class NoTabsTitleControl extends TitleControl { this._register(addDisposableListener(this.editorLabel.element, EventType.CLICK, e => this.onTitleLabelClick(e))); // Breadcrumbs - this.createBreadcrumbsControl(labelContainer, { showFileIcons: false, showSymbolIcons: true, showDecorationColors: false, widgetStyles: { ...defaultBreadcrumbsWidgetStyles, breadcrumbsBackground: Color.transparent.toString() }, showPlaceholder: false }); + this.breadcrumbsControlFactory = this._register(this.instantiationService.createInstance(BreadcrumbsControlFactory, labelContainer, this.group, { + showFileIcons: false, + showSymbolIcons: true, + showDecorationColors: false, + widgetStyles: { ...defaultBreadcrumbsWidgetStyles, breadcrumbsBackground: Color.transparent.toString() }, + showPlaceholder: false + })); + this._register(this.breadcrumbsControlFactory.onDidEnablementChange(() => this.handleBreadcrumbsEnablementChange())); titleContainer.classList.toggle('breadcrumbs', Boolean(this.breadcrumbsControl)); this._register(toDisposable(() => titleContainer.classList.remove('breadcrumbs'))); // important to remove because the container is a shared dom node @@ -132,19 +141,21 @@ export class NoTabsTitleControl extends TitleControl { setTimeout(() => this.quickInputService.quickAccess.show(), 50); } - openEditor(editor: EditorInput): void { - this.doHandleOpenEditor(); + openEditor(editor: EditorInput): boolean { + return this.doHandleOpenEditor(); } - openEditors(editors: EditorInput[]): void { - this.doHandleOpenEditor(); + openEditors(editors: EditorInput[]): boolean { + return this.doHandleOpenEditor(); } - private doHandleOpenEditor(): void { + private doHandleOpenEditor(): boolean { const activeEditorChanged = this.ifActiveEditorChanged(() => this.redraw()); if (!activeEditorChanged) { this.ifActiveEditorPropertiesChanged(() => this.redraw()); } + + return activeEditorChanged; } beforeCloseEditor(editor: EditorInput): void { @@ -350,16 +361,13 @@ export class NoTabsTitleControl extends TitleControl { } } - getHeight(): IEditorGroupTitleHeight { - return { - total: NoTabsTitleControl.HEIGHT, - offset: 0 - }; + getHeight(): number { + return this.tabHeight; } - layout(dimensions: ITitleControlDimensions): Dimension { + layout(dimensions: IEditorTitleControlDimensions): Dimension { this.breadcrumbsControl?.layout(undefined); - return new Dimension(dimensions.container.width, this.getHeight().total); + return new Dimension(dimensions.container.width, this.getHeight()); } } diff --git a/src/vs/workbench/browser/parts/editor/tabFocus.ts b/src/vs/workbench/browser/parts/editor/tabFocus.ts deleted file mode 100644 index 79ad04a21dd..00000000000 --- a/src/vs/workbench/browser/parts/editor/tabFocus.ts +++ /dev/null @@ -1,29 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Emitter } from 'vs/base/common/event'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { TabFocus } from 'vs/editor/browser/config/tabFocus'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; - -export class TabFocusMode extends Disposable { - private readonly _onDidChange = this._register(new Emitter()); - readonly onDidChange = this._onDidChange.event; - - constructor(@IConfigurationService configurationService: IConfigurationService) { - super(); - TabFocus.onDidChangeTabFocus((tabFocusMode) => this._onDidChange.fire(tabFocusMode)); - const editorConfig: boolean = configurationService.getValue('editor.tabFocusMode'); - TabFocus.setTabFocusMode(editorConfig); - this._onDidChange.fire(editorConfig); - this._register(configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('editor.tabFocusMode')) { - const value: boolean = configurationService.getValue('editor.tabFocusMode'); - TabFocus.setTabFocusMode(value); - this._onDidChange.fire(value); - } - })); - } -} diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts b/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts index eff2421efd0..8e943eec515 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts @@ -41,7 +41,6 @@ export class StatusbarViewModel extends Disposable { this.restoreState(); this.registerListeners(); - } private restoreState(): void { diff --git a/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts b/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts index 82d9a3dd1d6..1306b617009 100644 --- a/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts +++ b/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts @@ -8,21 +8,18 @@ import { BaseActionViewItem, IBaseActionViewItemOptions } from 'vs/base/browser/ import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; import { setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; -import { IAction } from 'vs/base/common/actions'; +import { IAction, SubmenuAction } from 'vs/base/common/actions'; import { Codicon } from 'vs/base/common/codicons'; -import { Color } from 'vs/base/common/color'; import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { HiddenItemStrategy, MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; -import { MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; +import { HiddenItemStrategy, MenuWorkbenchToolBar, WorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; +import { MenuId, MenuRegistry, SubmenuItemAction } from 'vs/platform/actions/common/actions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; -import * as colors from 'vs/platform/theme/common/colorRegistry'; import { WindowTitle } from 'vs/workbench/browser/parts/titlebar/windowTitle'; -import { MENUBAR_SELECTION_BACKGROUND, MENUBAR_SELECTION_FOREGROUND, TITLE_BAR_ACTIVE_FOREGROUND, TITLE_BAR_INACTIVE_FOREGROUND } from 'vs/workbench/common/theme'; export class CommandCenterControl { @@ -50,68 +47,8 @@ export class CommandCenterControl { }, telemetrySource: 'commandCenter', actionViewItemProvider: (action) => { - - if (action instanceof MenuItemAction && action.id === 'workbench.action.quickOpenWithModes') { - - class CommandCenterViewItem extends BaseActionViewItem { - - constructor(action: IAction, options: IBaseActionViewItemOptions) { - super(undefined, action, options); - } - - override render(container: HTMLElement): void { - super.render(container); - container.classList.add('command-center'); - - // icon (search) - const searchIcon = renderIcon(Codicon.search); - searchIcon.classList.add('search-icon'); - - // label: just workspace name and optional decorations - const label = this._getLabel(); - const labelElement = document.createElement('span'); - labelElement.classList.add('search-label'); - labelElement.innerText = label; - reset(container, searchIcon, labelElement); - - const hover = this._store.add(setupCustomHover(hoverDelegate, container, this.getTooltip())); - - // update label & tooltip when window title changes - this._store.add(windowTitle.onDidChange(() => { - hover.update(this.getTooltip()); - labelElement.innerText = this._getLabel(); - })); - } - - private _getLabel(): string { - const { prefix, suffix } = windowTitle.getTitleDecorations(); - let label = windowTitle.isCustomTitleFormat() ? windowTitle.getWindowTitle() : windowTitle.workspaceName; - if (!label) { - label = localize('label.dfl', "Search"); - } - if (prefix) { - label = localize('label1', "{0} {1}", prefix, label); - } - if (suffix) { - label = localize('label2', "{0} {1}", label, suffix); - } - return label; - } - - protected override getTooltip() { - - // tooltip: full windowTitle - const kb = keybindingService.lookupKeybinding(action.id)?.getLabel(); - const title = kb - ? localize('title', "Search {0} ({1}) \u2014 {2}", windowTitle.workspaceName, kb, windowTitle.value) - : localize('title2', "Search {0} \u2014 {1}", windowTitle.workspaceName, windowTitle.value); - - return title; - } - } - - return instantiationService.createInstance(CommandCenterViewItem, action, {}); - + if (action instanceof SubmenuItemAction && action.item.submenu === MenuId.CommandCenterCenter) { + return instantiationService.createInstance(CommandCenterCenterViewItem, action, windowTitle, hoverDelegate, {}); } else { return createActionViewItem(instantiationService, action, { hoverDelegate }); } @@ -134,56 +71,143 @@ export class CommandCenterControl { } -// --- theme colors +class CommandCenterCenterViewItem extends BaseActionViewItem { -// foreground (inactive and active) -colors.registerColor( - 'commandCenter.foreground', - { dark: TITLE_BAR_ACTIVE_FOREGROUND, hcDark: TITLE_BAR_ACTIVE_FOREGROUND, light: TITLE_BAR_ACTIVE_FOREGROUND, hcLight: TITLE_BAR_ACTIVE_FOREGROUND }, - localize('commandCenter-foreground', "Foreground color of the command center"), - false -); -colors.registerColor( - 'commandCenter.activeForeground', - { dark: MENUBAR_SELECTION_FOREGROUND, hcDark: MENUBAR_SELECTION_FOREGROUND, light: MENUBAR_SELECTION_FOREGROUND, hcLight: MENUBAR_SELECTION_FOREGROUND }, - localize('commandCenter-activeForeground', "Active foreground color of the command center"), - false -); -colors.registerColor( - 'commandCenter.inactiveForeground', - { dark: TITLE_BAR_INACTIVE_FOREGROUND, hcDark: TITLE_BAR_INACTIVE_FOREGROUND, light: TITLE_BAR_INACTIVE_FOREGROUND, hcLight: TITLE_BAR_INACTIVE_FOREGROUND }, - localize('commandCenter-inactiveForeground', "Foreground color of the command center when the window is inactive"), - false -); -// background (inactive and active) -colors.registerColor( - 'commandCenter.background', - { - dark: Color.white.transparent(0.05), hcDark: null, light: Color.black.transparent(0.05), hcLight: null - }, - localize('commandCenter-background', "Background color of the command center"), - false -); -colors.registerColor( - 'commandCenter.activeBackground', - { dark: Color.white.transparent(0.08), hcDark: MENUBAR_SELECTION_BACKGROUND, light: Color.black.transparent(0.08), hcLight: MENUBAR_SELECTION_BACKGROUND }, - localize('commandCenter-activeBackground', "Active background color of the command center"), - false -); -// border: active and inactive. defaults to active background -colors.registerColor( - 'commandCenter.border', { dark: colors.transparent(TITLE_BAR_ACTIVE_FOREGROUND, .20), hcDark: colors.transparent(TITLE_BAR_ACTIVE_FOREGROUND, .60), light: colors.transparent(TITLE_BAR_ACTIVE_FOREGROUND, .20), hcLight: colors.transparent(TITLE_BAR_ACTIVE_FOREGROUND, .60) }, - localize('commandCenter-border', "Border color of the command center"), - false -); -colors.registerColor( - 'commandCenter.activeBorder', { dark: colors.transparent(TITLE_BAR_ACTIVE_FOREGROUND, .30), hcDark: TITLE_BAR_ACTIVE_FOREGROUND, light: colors.transparent(TITLE_BAR_ACTIVE_FOREGROUND, .30), hcLight: TITLE_BAR_ACTIVE_FOREGROUND }, - localize('commandCenter-activeBorder', "Active border color of the command center"), - false -); -// border: defaults to active background -colors.registerColor( - 'commandCenter.inactiveBorder', { dark: colors.transparent(TITLE_BAR_INACTIVE_FOREGROUND, .25), hcDark: colors.transparent(TITLE_BAR_INACTIVE_FOREGROUND, .25), light: colors.transparent(TITLE_BAR_INACTIVE_FOREGROUND, .25), hcLight: colors.transparent(TITLE_BAR_INACTIVE_FOREGROUND, .25) }, - localize('commandCenter-inactiveBorder', "Border color of the command center when the window is inactive"), - false -); + private static readonly _quickOpenCommandId = 'workbench.action.quickOpenWithModes'; + + constructor( + private readonly _submenu: SubmenuItemAction, + private readonly _windowTitle: WindowTitle, + private readonly _hoverDelegate: IHoverDelegate, + options: IBaseActionViewItemOptions, + @IKeybindingService private _keybindingService: IKeybindingService, + @IInstantiationService private _instaService: IInstantiationService, + ) { + super(undefined, _submenu.actions[0], options); + } + + override render(container: HTMLElement): void { + super.render(container); + container.classList.add('command-center-center'); + container.classList.toggle('multiple', (this._submenu.actions.length > 1)); + + const hover = this._store.add(setupCustomHover(this._hoverDelegate, container, this.getTooltip())); + + // update label & tooltip when window title changes + this._store.add(this._windowTitle.onDidChange(() => { + hover.update(this.getTooltip()); + })); + + const groups: (readonly IAction[])[] = []; + for (const action of this._submenu.actions) { + if (action instanceof SubmenuAction) { + groups.push(action.actions); + } else { + groups.push([action]); + } + } + + + for (let i = 0; i < groups.length; i++) { + const group = groups[i]; + + // nested toolbar + const toolbar = this._instaService.createInstance(WorkbenchToolBar, container, { + hiddenItemStrategy: HiddenItemStrategy.NoHide, + telemetrySource: 'commandCenterCenter', + actionViewItemProvider: (action, options) => { + options = { + ...options, + hoverDelegate: this._hoverDelegate, + }; + + if (action.id !== CommandCenterCenterViewItem._quickOpenCommandId) { + return createActionViewItem(this._instaService, action, options); + } + + const that = this; + + return this._instaService.createInstance(class CommandCenterQuickPickItem extends BaseActionViewItem { + + constructor() { + super(undefined, action, options); + } + + override render(container: HTMLElement): void { + super.render(container); + container.classList.toggle('command-center-quick-pick'); + + const action = this.action; + + // icon (search) + const searchIcon = document.createElement('span'); + searchIcon.className = action.class ?? ''; + searchIcon.classList.add('search-icon'); + + // label: just workspace name and optional decorations + const label = this._getLabel(); + const labelElement = document.createElement('span'); + labelElement.classList.add('search-label'); + labelElement.innerText = label; + reset(container, searchIcon, labelElement); + + const hover = this._store.add(setupCustomHover(that._hoverDelegate, container, this.getTooltip())); + + // update label & tooltip when window title changes + this._store.add(that._windowTitle.onDidChange(() => { + hover.update(this.getTooltip()); + labelElement.innerText = this._getLabel(); + })); + } + + protected override getTooltip() { + return that.getTooltip(); + } + + private _getLabel(): string { + const { prefix, suffix } = that._windowTitle.getTitleDecorations(); + let label = that._windowTitle.isCustomTitleFormat() ? that._windowTitle.getWindowTitle() : that._windowTitle.workspaceName; + if (!label) { + label = localize('label.dfl', "Search"); + } + if (prefix) { + label = localize('label1', "{0} {1}", prefix, label); + } + if (suffix) { + label = localize('label2', "{0} {1}", label, suffix); + } + return label; + } + }); + } + }); + toolbar.setActions(group); + this._store.add(toolbar); + + // spacer + if (i < groups.length - 1) { + const icon = renderIcon(Codicon.circleSmallFilled); + icon.classList.add('spacer'); + container.appendChild(icon); + } + } + } + + protected override getTooltip() { + + // tooltip: full windowTitle + const kb = this._keybindingService.lookupKeybinding(this.action.id)?.getLabel(); + const title = kb + ? localize('title', "Search {0} ({1}) \u2014 {2}", this._windowTitle.workspaceName, kb, this._windowTitle.value) + : localize('title2', "Search {0} \u2014 {1}", this._windowTitle.workspaceName, this._windowTitle.value); + + return title; + } +} + +MenuRegistry.appendMenuItem(MenuId.CommandCenter, { + submenu: MenuId.CommandCenterCenter, + title: localize('title3', "Command Center"), + icon: Codicon.shield, + order: 101, +}); diff --git a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css index 315429f1f85..b0c29642ec2 100644 --- a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css +++ b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css @@ -134,19 +134,19 @@ visibility: hidden; } -.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item > .action-label { +.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center > .monaco-toolbar > .monaco-action-bar > .actions-container > .action-item > .action-label { color: var(--vscode-titleBar-activeForeground); } -.monaco-workbench .part.titlebar.inactive > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item > .action-label { +.monaco-workbench .part.titlebar.inactive > .titlebar-container > .titlebar-center > .window-title > .command-center > .monaco-toolbar > .monaco-action-bar > .actions-container > .action-item > .action-label { color: var(--vscode-titleBar-inactiveForeground); } -.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .codicon { +.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center > .monaco-toolbar > .monaco-action-bar > .actions-container > .action-item > .action-label { color: inherit; } -.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center { +.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center { display: flex; align-items: stretch; color: var(--vscode-commandCenter-foreground); @@ -163,27 +163,37 @@ max-width: 600px; } -.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center:only-child { - margin-left: 0; /* no margin if there is only the command center, without nav buttons */ +.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center .action-item.command-center-quick-pick { + display: flex; } -.monaco-workbench .part.titlebar.inactive > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center { - color: var(--vscode-titleBar-inactiveForeground); - border-color: var(--vscode-commandCenter-inactiveBorder) !important; -} - -.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center .search-icon { +.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center .action-item.command-center-quick-pick .search-icon { font-size: 14px; opacity: .8; margin: auto 3px; } -.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center .search-label { +.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center .action-item.command-center-quick-pick .search-label { overflow: hidden; text-overflow: ellipsis; } -.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center:HOVER { +.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center.multiple .spacer { + height: 100%; + padding: 0 6px; + opacity: 0.5; +} + +.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center:only-child { + margin-left: 0; /* no margin if there is only the command center, without nav buttons */ +} + +.monaco-workbench .part.titlebar.inactive > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center { + color: var(--vscode-titleBar-inactiveForeground); + border-color: var(--vscode-commandCenter-inactiveBorder) !important; +} + +.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center:HOVER { color: var(--vscode-commandCenter-activeForeground); background-color: var(--vscode-commandCenter-activeBackground); border-color: var(--vscode-commandCenter-activeBorder); diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 22fa7481b12..b15e2930425 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -19,7 +19,7 @@ import { ThemeIcon } from 'vs/base/common/themables'; import { TITLE_BAR_ACTIVE_BACKGROUND, TITLE_BAR_ACTIVE_FOREGROUND, TITLE_BAR_INACTIVE_FOREGROUND, TITLE_BAR_INACTIVE_BACKGROUND, TITLE_BAR_BORDER, WORKBENCH_BACKGROUND } from 'vs/workbench/common/theme'; import { isMacintosh, isWindows, isLinux, isWeb, isNative, platformLocale } from 'vs/base/common/platform'; import { Color } from 'vs/base/common/color'; -import { EventType, EventHelper, Dimension, isAncestor, append, $, addDisposableListener, prepend, reset } from 'vs/base/browser/dom'; +import { EventType, EventHelper, Dimension, append, $, addDisposableListener, prepend, reset } from 'vs/base/browser/dom'; import { CustomMenubarControl } from 'vs/workbench/browser/parts/titlebar/menubarControl'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { Emitter, Event } from 'vs/base/common/event'; @@ -77,7 +77,6 @@ export class TitlebarPart extends Part implements ITitleService { private appIconBadge: HTMLElement | undefined; protected menubar?: HTMLElement; protected layoutControls: HTMLElement | undefined; - private layoutToolbar: MenuWorkbenchToolBar | undefined; protected lastLayoutDimensions: Dimension | undefined; private hoverDelegate: IHoverDelegate; @@ -273,18 +272,17 @@ export class TitlebarPart extends Part implements ITitleService { this.title = append(this.centerContent, $('div.window-title')); this.updateTitle(); - if (this.titleBarStyle !== 'native') { this.layoutControls = append(this.rightContent, $('div.layout-controls-container')); this.layoutControls.classList.toggle('show-layout-control', this.layoutControlEnabled); - this.layoutToolbar = this.instantiationService.createInstance(MenuWorkbenchToolBar, this.layoutControls, MenuId.LayoutControlMenu, { + this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, this.layoutControls, MenuId.LayoutControlMenu, { contextMenu: MenuId.TitleBarContext, toolbarOptions: { primaryGroup: () => true }, actionViewItemProvider: action => { return createActionViewItem(this.instantiationService, action, { hoverDelegate: this.hoverDelegate }); } - }); + })); } let primaryControlLocation = isMacintosh ? 'left' : 'right'; @@ -310,29 +308,6 @@ export class TitlebarPart extends Part implements ITitleService { })); }); - // Since the title area is used to drag the window, we do not want to steal focus from the - // currently active element. So we restore focus after a timeout back to where it was. - this._register(addDisposableListener(this.element, EventType.MOUSE_DOWN, e => { - if (e.target && this.menubar && isAncestor(e.target as HTMLElement, this.menubar)) { - return; - } - - if (e.target && this.layoutToolbar && isAncestor(e.target as HTMLElement, this.layoutToolbar.getElement())) { - return; - } - - if (e.target && isAncestor(e.target as HTMLElement, this.title)) { - return; - } - - const active = document.activeElement; - setTimeout(() => { - if (active instanceof HTMLElement) { - active.focus(); - } - }, 0 /* need a timeout because we are in capture phase */); - }, true /* use capture to know the currently active element properly */)); - this.updateStyles(); const that = this; @@ -347,7 +322,7 @@ export class TitlebarPart extends Part implements ITitleService { }); } - run(accessor: ServicesAccessor, ...args: any[]): void { + run(): void { if (that.customMenubar) { that.customMenubar.toggleFocus(); } else { diff --git a/src/vs/workbench/browser/parts/views/checkbox.ts b/src/vs/workbench/browser/parts/views/checkbox.ts index f5c3b4241a5..849966bbe33 100644 --- a/src/vs/workbench/browser/parts/views/checkbox.ts +++ b/src/vs/workbench/browser/parts/views/checkbox.ts @@ -62,6 +62,7 @@ export class TreeItemCheckbox extends Disposable { this.setHover(node.checkbox); this.setAccessibilityInformation(node.checkbox); this.toggle.domNode.classList.add(TreeItemCheckbox.checkboxClass); + this.toggle.domNode.tabIndex = 1; DOM.append(this.checkboxContainer, this.toggle.domNode); this.registerListener(node); } diff --git a/src/vs/workbench/browser/parts/views/viewFilter.ts b/src/vs/workbench/browser/parts/views/viewFilter.ts index 9986e46f084..cf47403d4cc 100644 --- a/src/vs/workbench/browser/parts/views/viewFilter.ts +++ b/src/vs/workbench/browser/parts/views/viewFilter.ts @@ -102,6 +102,8 @@ export class FilterWidget extends Widget { this.element = DOM.$('.viewpane-filter'); [this.filterInputBox, this.focusTracker] = this.createInput(this.element); + this._register(this.filterInputBox); + this._register(this.focusTracker); const controlsContainer = DOM.append(this.element, DOM.$('.viewpane-filter-controls')); this.filterBadge = this.createBadge(controlsContainer); diff --git a/src/vs/workbench/browser/parts/views/viewPane.ts b/src/vs/workbench/browser/parts/views/viewPane.ts index 844c4bf5a96..11f2a5447c0 100644 --- a/src/vs/workbench/browser/parts/views/viewPane.ts +++ b/src/vs/workbench/browser/parts/views/viewPane.ts @@ -112,7 +112,7 @@ class ViewWelcomeController { @IContextKeyService private contextKeyService: IContextKeyService, ) { contextKeyService.onDidChangeContext(this.onDidChangeContext, this, this.disposables); - Event.filter(viewsRegistry.onDidChangeViewWelcomeContent, id => id === this.id)(this.onDidChangeViewWelcomeContent, this, this.disposables); + this.disposables.add(Event.filter(viewsRegistry.onDidChangeViewWelcomeContent, id => id === this.id)(this.onDidChangeViewWelcomeContent, this, this.disposables)); this.onDidChangeViewWelcomeContent(); } @@ -240,7 +240,7 @@ export abstract class ViewPane extends Pane implements IView { this.menuActions = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService])).createInstance(CompositeMenuActions, options.titleMenuId ?? MenuId.ViewTitle, MenuId.ViewTitleContext, { shouldForwardArgs: !options.donotForwardArgs })); this._register(this.menuActions.onDidChange(() => this.updateActions())); - this.viewWelcomeController = new ViewWelcomeController(this.id, contextKeyService); + this.viewWelcomeController = this._register(new ViewWelcomeController(this.id, contextKeyService)); } override get headerVisible(): boolean { diff --git a/src/vs/workbench/browser/quickaccess.ts b/src/vs/workbench/browser/quickaccess.ts index 7c4ff4fa763..afc3f54369b 100644 --- a/src/vs/workbench/browser/quickaccess.ts +++ b/src/vs/workbench/browser/quickaccess.ts @@ -24,6 +24,7 @@ export interface IWorkbenchQuickAccessConfiguration { readonly experimental: { readonly suggestCommands: boolean; readonly enableNaturalLanguageSearch: boolean; + readonly askChatLocation: 'quickChat' | 'chatView'; }; }; readonly quickOpen: { diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index b3a23711702..83aa1e53241 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -95,11 +95,11 @@ const registry = Registry.as(ConfigurationExtensions.Con key: 'untitledLabelFormat' }, "Controls the format of the label for an untitled editor."), }, - 'workbench.editor.untitled.hint': { + 'workbench.editor.empty.hint': { 'type': 'string', 'enum': ['text', 'hidden'], 'default': 'text', - 'markdownDescription': localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'untitledHint' }, "Controls if the untitled text hint should be visible in the editor.") + 'markdownDescription': localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'emptyEditorHint' }, "Controls if the empty editor text hint should be visible in the editor.") }, 'workbench.editor.languageDetection': { type: 'boolean', @@ -387,6 +387,17 @@ const registry = Registry.as(ConfigurationExtensions.Con 'description': localize('suggestCommands', "Controls whether the command palette should have a list of commonly used commands."), 'default': false }, + 'workbench.commandPalette.experimental.askChatLocation': { + 'type': 'string', + tags: ['experimental'], + 'description': localize('askChatLocation', "Controls where the command palette should ask chat questions."), + 'default': 'chatView', + enum: ['chatView', 'quickChat'], + enumDescriptions: [ + localize('askChatLocation.chatView', "Ask chat questions in the Chat view."), + localize('askChatLocation.quickChat', "Ask chat questions in Quick Chat.") + ] + }, 'workbench.commandPalette.experimental.enableNaturalLanguageSearch': { 'type': 'boolean', tags: ['experimental'], diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index a4a46546750..9d31beaefc2 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -875,6 +875,59 @@ export const MENUBAR_SELECTION_BORDER = registerColor('menubar.selectionBorder', hcLight: activeContrastBorder, }, localize('menubarSelectionBorder', "Border color of the selected menu item in the menubar.")); +// < --- Command Center --- > + +// foreground (inactive and active) +export const COMMAND_CENTER_FOREGROUND = registerColor( + 'commandCenter.foreground', + { dark: TITLE_BAR_ACTIVE_FOREGROUND, hcDark: TITLE_BAR_ACTIVE_FOREGROUND, light: TITLE_BAR_ACTIVE_FOREGROUND, hcLight: TITLE_BAR_ACTIVE_FOREGROUND }, + localize('commandCenter-foreground', "Foreground color of the command center"), + false +); +export const COMMAND_CENTER_ACTIVEFOREGROUND = registerColor( + 'commandCenter.activeForeground', + { dark: MENUBAR_SELECTION_FOREGROUND, hcDark: MENUBAR_SELECTION_FOREGROUND, light: MENUBAR_SELECTION_FOREGROUND, hcLight: MENUBAR_SELECTION_FOREGROUND }, + localize('commandCenter-activeForeground', "Active foreground color of the command center"), + false +); +export const COMMAND_CENTER_INACTIVEFOREGROUND = registerColor( + 'commandCenter.inactiveForeground', + { dark: TITLE_BAR_INACTIVE_FOREGROUND, hcDark: TITLE_BAR_INACTIVE_FOREGROUND, light: TITLE_BAR_INACTIVE_FOREGROUND, hcLight: TITLE_BAR_INACTIVE_FOREGROUND }, + localize('commandCenter-inactiveForeground', "Foreground color of the command center when the window is inactive"), + false +); +// background (inactive and active) +export const COMMAND_CENTER_BACKGROUND = registerColor( + 'commandCenter.background', + { dark: Color.white.transparent(0.05), hcDark: null, light: Color.black.transparent(0.05), hcLight: null }, + localize('commandCenter-background', "Background color of the command center"), + false +); +export const COMMAND_CENTER_ACTIVEBACKGROUND = registerColor( + 'commandCenter.activeBackground', + { dark: Color.white.transparent(0.08), hcDark: MENUBAR_SELECTION_BACKGROUND, light: Color.black.transparent(0.08), hcLight: MENUBAR_SELECTION_BACKGROUND }, + localize('commandCenter-activeBackground', "Active background color of the command center"), + false +); +// border: active and inactive. defaults to active background +export const COMMAND_CENTER_BORDER = registerColor( + 'commandCenter.border', { dark: transparent(TITLE_BAR_ACTIVE_FOREGROUND, .20), hcDark: transparent(TITLE_BAR_ACTIVE_FOREGROUND, .60), light: transparent(TITLE_BAR_ACTIVE_FOREGROUND, .20), hcLight: transparent(TITLE_BAR_ACTIVE_FOREGROUND, .60) }, + localize('commandCenter-border', "Border color of the command center"), + false +); +export const COMMAND_CENTER_ACTIVEBORDER = registerColor( + 'commandCenter.activeBorder', { dark: transparent(TITLE_BAR_ACTIVE_FOREGROUND, .30), hcDark: TITLE_BAR_ACTIVE_FOREGROUND, light: transparent(TITLE_BAR_ACTIVE_FOREGROUND, .30), hcLight: TITLE_BAR_ACTIVE_FOREGROUND }, + localize('commandCenter-activeBorder', "Active border color of the command center"), + false +); +// border: defaults to active background +export const COMMAND_CENTER_INACTIVEBORDER = registerColor( + 'commandCenter.inactiveBorder', { dark: transparent(TITLE_BAR_INACTIVE_FOREGROUND, .25), hcDark: transparent(TITLE_BAR_INACTIVE_FOREGROUND, .25), light: transparent(TITLE_BAR_INACTIVE_FOREGROUND, .25), hcLight: transparent(TITLE_BAR_INACTIVE_FOREGROUND, .25) }, + localize('commandCenter-inactiveBorder', "Border color of the command center when the window is inactive"), + false +); + + // < --- Notifications --- > export const NOTIFICATIONS_CENTER_BORDER = registerColor('notificationCenter.border', { diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts b/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts index 4725fbf0d9d..de7ddcd7c0c 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts @@ -11,6 +11,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IAccessibleViewService, AccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; import { UnfocusedViewDimmingContribution } from 'vs/workbench/contrib/accessibility/browser/unfocusedViewDimmingContribution'; import { EditorAccessibilityHelpContribution, HoverAccessibleViewContribution, InlineCompletionsAccessibleViewContribution, NotificationAccessibleViewContribution } from 'vs/workbench/contrib/accessibility/browser/accessibilityContributions'; +import { AccessibilityStatus } from 'vs/workbench/contrib/accessibility/browser/accessibilityStatus'; registerAccessibilityConfiguration(); registerSingleton(IAccessibleViewService, AccessibleViewService, InstantiationType.Delayed); @@ -23,3 +24,4 @@ const workbenchContributionsRegistry = Registry.as('accessibleViewI export const accessibleViewSupportsNavigation = new RawContextKey('accessibleViewSupportsNavigation', false, true); export const accessibleViewVerbosityEnabled = new RawContextKey('accessibleViewVerbosityEnabled', false, true); export const accessibleViewGoToSymbolSupported = new RawContextKey('accessibleViewGoToSymbolSupported', false, true); +export const accessibleViewOnLastLine = new RawContextKey('accessibleViewOnLastLine', false, true); export const accessibleViewCurrentProviderId = new RawContextKey('accessibleViewCurrentProviderId', undefined, undefined); /** @@ -42,7 +43,7 @@ export const enum AccessibilityVerbositySettingId { Editor = 'accessibility.verbosity.editor', Hover = 'accessibility.verbosity.hover', Notification = 'accessibility.verbosity.notification', - EditorUntitledHint = 'accessibility.verbosity.untitledHint' + EmptyEditorHint = 'accessibility.verbosity.emptyEditorHint' } export const enum AccessibleViewProviderId { @@ -56,7 +57,7 @@ export const enum AccessibleViewProviderId { Editor = 'editor', Hover = 'hover', Notification = 'notification', - EditorUntitledHint = 'editor.untitledHint' + EmptyEditorHint = 'emptyEditorHint' } const baseProperty: object = { @@ -106,8 +107,8 @@ const configuration: IConfigurationNode = { description: localize('verbosity.notification', 'Provide information about how to open the notification in an accessible view.'), ...baseProperty }, - [AccessibilityVerbositySettingId.EditorUntitledHint]: { - description: localize('verbosity.untitledhint', 'Provide information about relevant actions in an untitled text editor.'), + [AccessibilityVerbositySettingId.EmptyEditorHint]: { + description: localize('verbosity.emptyEditorHint', 'Provide information about relevant actions in an empty text editor.'), ...baseProperty } } diff --git a/src/vs/workbench/browser/parts/editor/accessibilityStatus.ts b/src/vs/workbench/contrib/accessibility/browser/accessibilityStatus.ts similarity index 100% rename from src/vs/workbench/browser/parts/editor/accessibilityStatus.ts rename to src/vs/workbench/contrib/accessibility/browser/accessibilityStatus.ts diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index 1f82f5c1e9b..bbde0f86f2c 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts @@ -18,6 +18,7 @@ import { URI } from 'vs/base/common/uri'; import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget'; +import { Position } from 'vs/editor/common/core/position'; import { ITextModel } from 'vs/editor/common/model'; import { IModelService } from 'vs/editor/common/services/model'; import { AccessibilityHelpNLS } from 'vs/editor/common/standaloneStrings'; @@ -36,7 +37,7 @@ import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IPickerQuickAccessItem } from 'vs/platform/quickinput/browser/pickerQuickAccess'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; -import { AccessibilityVerbositySettingId, accessibilityHelpIsShown, accessibleViewCurrentProviderId, accessibleViewGoToSymbolSupported, accessibleViewIsShown, accessibleViewSupportsNavigation, accessibleViewVerbosityEnabled } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { AccessibilityVerbositySettingId, accessibilityHelpIsShown, accessibleViewCurrentProviderId, accessibleViewGoToSymbolSupported, accessibleViewIsShown, accessibleViewOnLastLine, accessibleViewSupportsNavigation, accessibleViewVerbosityEnabled } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; @@ -73,6 +74,9 @@ export interface IAccessibleViewService { previous(): void; goToSymbol(): void; disableHint(): void; + getPosition(): Position | undefined; + setPosition(position: Position, reveal?: boolean): void; + getLastPosition(): Position | undefined; /** * If the setting is enabled, provides the open accessible view hint as a localized string. * @param verbositySettingKey The setting key for the verbosity of the feature @@ -85,6 +89,11 @@ export const enum AccessibleViewType { View = 'view' } +export const enum NavigationType { + Previous = 'previous', + Next = 'next' +} + export interface IAccessibleViewOptions { readMoreUrl?: string; /** @@ -94,10 +103,11 @@ export interface IAccessibleViewOptions { type: AccessibleViewType; } -class AccessibleView extends Disposable { +export class AccessibleView extends Disposable { private _editorWidget: CodeEditorWidget; private _accessiblityHelpIsShown: IContextKey; + private _onLastLine: IContextKey; private _accessibleViewIsShown: IContextKey; private _accessibleViewSupportsNavigation: IContextKey; private _accessibleViewVerbosityEnabled: IContextKey; @@ -132,6 +142,7 @@ class AccessibleView extends Disposable { this._accessibleViewVerbosityEnabled = accessibleViewVerbosityEnabled.bindTo(this._contextKeyService); this._accessibleViewGoToSymbolSupported = accessibleViewGoToSymbolSupported.bindTo(this._contextKeyService); this._accessibleViewCurrentProviderId = accessibleViewCurrentProviderId.bindTo(this._contextKeyService); + this._onLastLine = accessibleViewOnLastLine.bindTo(this._contextKeyService); this._container = document.createElement('div'); this._container.classList.add('accessible-view'); @@ -182,6 +193,9 @@ class AccessibleView extends Disposable { } })); this._register(this._editorWidget.onDidDispose(() => this._resetContextKeys())); + this._register(this._editorWidget.onDidChangeCursorPosition(() => { + this._onLastLine.set(this._editorWidget.getPosition()?.lineNumber === this._editorWidget.getModel()?.getLineCount()); + })); } private _resetContextKeys(): void { @@ -242,11 +256,23 @@ class AccessibleView extends Disposable { if (!this._currentProvider || !this._currentContent) { return; } - const tokens = this._currentProvider.options.language && this._currentProvider.options.language !== 'markdown' ? this._currentProvider.getSymbols?.() : marked.lexer(this._currentContent); - if (!tokens) { + const symbols: IAccessibleViewSymbol[] = this._currentProvider.getSymbols?.() || []; + if (symbols?.length) { + return symbols; + } + if (this._currentProvider.options.language && this._currentProvider.options.language !== 'markdown') { + // Symbols haven't been provided and we cannot parse this language return; } - const symbols: IAccessibleViewSymbol[] = []; + const markdownTokens: marked.TokensList | undefined = marked.lexer(this._currentContent); + if (!markdownTokens) { + return; + } + this._convertTokensToSymbols(markdownTokens, symbols); + return symbols.length ? symbols : undefined; + } + + private _convertTokensToSymbols(tokens: marked.TokensList, symbols: IAccessibleViewSymbol[]): void { let firstListItem: string | undefined; for (const token of tokens) { let label: string | undefined = undefined; @@ -267,27 +293,39 @@ class AccessibleView extends Disposable { break; } } - } else { - label = token.label; } if (label) { - symbols.push({ info: label, label: localize('symbolLabel', "({0}) {1}", token.type, label), ariaLabel: localize('symbolLabelAria', "({0}) {1}", token.type, label), firstListItem }); + symbols.push({ markdownToParse: label, label: localize('symbolLabel', "({0}) {1}", token.type, label), ariaLabel: localize('symbolLabelAria', "({0}) {1}", token.type, label), firstListItem }); firstListItem = undefined; } } - return symbols; } showSymbol(provider: IAccessibleContentProvider, symbol: IAccessibleViewSymbol): void { if (!this._currentContent) { return; } - const index = this._currentContent.split('\n').findIndex(line => line.includes(symbol.info.split('\n')[0]) || (symbol.firstListItem && line.includes(symbol.firstListItem))) ?? -1; - if (index >= 0) { - this.show(provider); - this._editorWidget.revealLine(index + 1); - this._editorWidget.setSelection({ startLineNumber: index + 1, startColumn: 1, endLineNumber: index + 1, endColumn: 1 }); + let lineNumber: number | undefined = symbol.lineNumber; + const markdownToParse = symbol.markdownToParse; + if (lineNumber === undefined && markdownToParse === undefined) { + // No symbols provided and we cannot parse this language + return; } + + if (lineNumber === undefined && markdownToParse) { + // Note that this scales poorly, thus isn't used for worst case scenarios like the terminal, for which a line number will always be provided. + // Parse the markdown to find the line number + const index = this._currentContent.split('\n').findIndex(line => line.includes(markdownToParse.split('\n')[0]) || (symbol.firstListItem && line.includes(symbol.firstListItem))) ?? -1; + if (index >= 0) { + lineNumber = index + 1; + } + } + if (lineNumber === undefined) { + return; + } + this.show(provider); + this._editorWidget.revealLine(lineNumber); + this._editorWidget.setSelection({ startLineNumber: lineNumber, startColumn: 1, endLineNumber: lineNumber, endColumn: 1 }); this._updateContextKeys(provider, true); } @@ -345,7 +383,7 @@ class AccessibleView extends Disposable { message += '\n'; } } - this._currentContent = message + provider.provideContent() + readMoreLink + disableHelpHint + localize('exit-tip', '\nExit this dialog via the Escape key.'); + this._currentContent = message + provider.provideContent() + readMoreLink + disableHelpHint; this._updateContextKeys(provider, true); this._getTextModel(URI.from({ path: `accessible-view-${provider.verbositySettingKey}`, scheme: 'accessible-view', fragment: this._currentContent })).then((model) => { @@ -449,7 +487,7 @@ class AccessibleView extends Disposable { if (!this._currentProvider) { return false; } - return this._currentProvider.options.type === AccessibleViewType.Help || this._currentProvider.options.language === 'markdown' || this._currentProvider.options.language === undefined || !!this._currentProvider.getSymbols; + return this._currentProvider.options.type === AccessibleViewType.Help || this._currentProvider.options.language === 'markdown' || this._currentProvider.options.language === undefined || !!this._currentProvider.getSymbols?.(); } public showAccessibleViewHelp(): void { @@ -544,7 +582,6 @@ export class AccessibleViewService extends Disposable implements IAccessibleView this._accessibleView = this._register(this._instantiationService.createInstance(AccessibleView)); } this._accessibleView.show(provider); - } next(): void { this._accessibleView?.next(); @@ -574,9 +611,22 @@ export class AccessibleViewService extends Disposable implements IAccessibleView showAccessibleViewHelp(): void { this._accessibleView?.showAccessibleViewHelp(); } + getPosition(): Position | undefined { + return this._accessibleView?.editorWidget.getPosition() ?? undefined; + } + getLastPosition(): Position | undefined { + const lastLine = this._accessibleView?.editorWidget.getModel()?.getLineCount(); + return lastLine !== undefined && lastLine > 0 ? new Position(lastLine, 1) : undefined; + } + setPosition(position: Position, reveal?: boolean): void { + const editorWidget = this._accessibleView?.editorWidget; + editorWidget?.setPosition(position); + if (reveal) { + editorWidget?.revealLine(position.lineNumber); + } + } } - class AccessibleViewSymbolQuickPick { constructor(private _accessibleView: AccessibleView, @IQuickInputService private readonly _quickInputService: IQuickInputService) { @@ -612,7 +662,8 @@ class AccessibleViewSymbolQuickPick { } } -interface IAccessibleViewSymbol extends IPickerQuickAccessItem { - info: string; +export interface IAccessibleViewSymbol extends IPickerQuickAccessItem { + markdownToParse?: string; firstListItem?: string; + lineNumber?: number; } diff --git a/src/vs/workbench/contrib/accessibility/browser/unfocusedViewDimmingContribution.ts b/src/vs/workbench/contrib/accessibility/browser/unfocusedViewDimmingContribution.ts index 0d9c1bec943..0cd7d898147 100644 --- a/src/vs/workbench/contrib/accessibility/browser/unfocusedViewDimmingContribution.ts +++ b/src/vs/workbench/contrib/accessibility/browser/unfocusedViewDimmingContribution.ts @@ -47,7 +47,7 @@ export class UnfocusedViewDimmingContribution extends Disposable implements IWor // Text editors rules.add(`.monaco-workbench .editor-instance:not(:focus-within) .monaco-editor { ${filterRule} }`); // Breadcrumbs - rules.add(`.monaco-workbench .editor-instance:not(:focus-within) .tabs-breadcrumbs { ${filterRule} }`); + rules.add(`.monaco-workbench .editor-instance:not(:focus-within) .breadcrumbs-below-tabs { ${filterRule} }`); // Terminal editors rules.add(`.monaco-workbench .editor-instance:not(:focus-within) .terminal-wrapper { ${filterRule} }`); // Settings editor diff --git a/src/vs/workbench/contrib/bulkEdit/test/browser/bulkEditPreview.test.ts b/src/vs/workbench/contrib/bulkEdit/test/browser/bulkEditPreview.test.ts index bd00c579e85..a05e30502cc 100644 --- a/src/vs/workbench/contrib/bulkEdit/test/browser/bulkEditPreview.test.ts +++ b/src/vs/workbench/contrib/bulkEdit/test/browser/bulkEditPreview.test.ts @@ -15,9 +15,11 @@ import { URI } from 'vs/base/common/uri'; import { BulkFileOperations } from 'vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview'; import { Range } from 'vs/editor/common/core/range'; import { ResourceFileEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('BulkEditPreview', function () { + const store = ensureNoDisposablesAreLeakedInTestSuite(); let instaService: IInstantiationService; @@ -53,6 +55,7 @@ suite('BulkEditPreview', function () { ]; const ops = await instaService.invokeFunction(BulkFileOperations.create, edits); + store.add(ops); assert.strictEqual(ops.fileOperations.length, 1); assert.strictEqual(ops.checked.isChecked(edits[0]), false); }); @@ -66,6 +69,7 @@ suite('BulkEditPreview', function () { const ops = await instaService.invokeFunction(BulkFileOperations.create, edits); + store.add(ops); assert.strictEqual(ops.categories.length, 2); assert.strictEqual(ops.categories[0].metadata.label, 'uri1'); // unconfirmed! assert.strictEqual(ops.categories[1].metadata.label, 'uri2'); @@ -79,6 +83,7 @@ suite('BulkEditPreview', function () { ]; const ops = await instaService.invokeFunction(BulkFileOperations.create, edits); + store.add(ops); assert.strictEqual(ops.categories.length, 1); assert.strictEqual(ops.categories[0].metadata.label, 'uri1'); // unconfirmed! assert.strictEqual(ops.categories[0].metadata.label, 'uri1'); @@ -93,6 +98,7 @@ suite('BulkEditPreview', function () { const ops = await instaService.invokeFunction(BulkFileOperations.create, edits); + store.add(ops); assert.strictEqual(ops.checked.isChecked(edits[0]), true); assert.strictEqual(ops.checked.isChecked(edits[1]), true); @@ -119,6 +125,7 @@ suite('BulkEditPreview', function () { ]; const ops = await instaService.invokeFunction(BulkFileOperations.create, edits); + store.add(ops); assert.strictEqual(ops.checked.isChecked(edits[0]), false); assert.strictEqual(ops.checked.isChecked(edits[1]), false); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts index f8048ff34b9..c757c26fa1a 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts @@ -56,13 +56,12 @@ function descriptionForCommand(commandId: string, msg: string, noKbMsg: string, return format(noKbMsg, commandId); } -export async function runAccessibilityHelpAction(accessor: ServicesAccessor, editor: ICodeEditor, type: 'panelChat' | 'inlineChat'): Promise { +export async function runAccessibilityHelpAction(accessor: ServicesAccessor, editor: ICodeEditor | undefined, type: 'panelChat' | 'inlineChat'): Promise { const widgetService = accessor.get(IChatWidgetService); const accessibleViewService = accessor.get(IAccessibleViewService); const inputEditor: ICodeEditor | undefined = type === 'panelChat' ? widgetService.lastFocusedWidget?.inputEditor : editor; - const editorUri = editor.getModel()?.uri; - if (!inputEditor || !editorUri) { + if (!inputEditor) { return; } const domNode = inputEditor.getDomNode() ?? undefined; @@ -81,7 +80,9 @@ export async function runAccessibilityHelpAction(accessor: ServicesAccessor, edi inputEditor.setPosition(cachedPosition); inputEditor.focus(); } else if (type === 'inlineChat') { - InlineChatController.get(editor)?.focus(); + if (editor) { + InlineChatController.get(editor)?.focus(); + } } }, options: { type: AccessibleViewType.Help } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 8b783da509f..3655eba1cde 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -23,7 +23,7 @@ import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatEditor'; import { ChatEditorInput } from 'vs/workbench/contrib/chat/browser/chatEditorInput'; import { ChatViewPane } from 'vs/workbench/contrib/chat/browser/chatViewPane'; -import { CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION, CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION, CONTEXT_PROVIDER_EXISTS, CONTEXT_REQUEST, CONTEXT_RESPONSE } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatDetail, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatWidgetHistoryService } from 'vs/workbench/contrib/chat/common/chatWidgetHistoryService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -32,8 +32,47 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle import { AccessibilityHelpAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; export const CHAT_CATEGORY = { value: localize('chat.category', "Chat"), original: 'Chat' }; +export const CHAT_OPEN_ACTION_ID = 'workbench.action.chat.open'; + +class QuickChatGlobalAction extends Action2 { + constructor() { + super({ + id: CHAT_OPEN_ACTION_ID, + title: { value: localize('quickChat', "Quick Chat"), original: 'Quick Chat' }, + precondition: CONTEXT_PROVIDER_EXISTS, + icon: Codicon.commentDiscussion, + f1: false, + category: CHAT_CATEGORY, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyI, + mac: { + primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KeyI + } + } + }); + } + + override async run(accessor: ServicesAccessor, query?: string): Promise { + const chatService = accessor.get(IChatService); + const chatWidgetService = accessor.get(IChatWidgetService); + const providers = chatService.getProviderInfos(); + if (!providers.length) { + return; + } + const chatWidget = await chatWidgetService.revealViewForProvider(providers[0].id); + if (!chatWidget) { + return; + } + if (query) { + chatWidget.acceptInput(query); + } + chatWidget.focusInput(); + } +} export function registerChatActions() { + registerAction2(QuickChatGlobalAction); registerEditorAction(class ChatAcceptInput extends EditorAction { constructor() { super({ @@ -107,11 +146,8 @@ export function registerChatActions() { super(); this._register(AccessibilityHelpAction.addImplementation(105, 'panelChat', async accessor => { const codeEditor = accessor.get(ICodeEditorService).getActiveCodeEditor() || accessor.get(ICodeEditorService).getFocusedCodeEditor(); - if (!codeEditor) { - return; - } - runAccessibilityHelpAction(accessor, codeEditor, 'panelChat'); - }, CONTEXT_IN_CHAT_SESSION)); + runAccessibilityHelpAction(accessor, codeEditor ?? undefined, 'panelChat'); + }, ContextKeyExpr.or(CONTEXT_IN_CHAT_SESSION, CONTEXT_RESPONSE, CONTEXT_REQUEST))); } } diff --git a/src/vs/workbench/contrib/chat/test/common/chatVariables.test.ts b/src/vs/workbench/contrib/chat/test/common/chatVariables.test.ts index db9c3f69a74..f67df48598e 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatVariables.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatVariables.test.ts @@ -5,6 +5,7 @@ import * as assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; suite('ChatVariables', function () { @@ -15,10 +16,12 @@ suite('ChatVariables', function () { service = new ChatVariablesService(); }); + ensureNoDisposablesAreLeakedInTestSuite(); test('ChatVariables - resolveVariables', async function () { - service.registerVariable({ name: 'foo', description: 'bar' }, async () => ([{ level: 'full', value: 'farboo' }])); - service.registerVariable({ name: 'far', description: 'boo' }, async () => ([{ level: 'full', value: 'farboo' }])); + + const v1 = service.registerVariable({ name: 'foo', description: 'bar' }, async () => ([{ level: 'full', value: 'farboo' }])); + const v2 = service.registerVariable({ name: 'far', description: 'boo' }, async () => ([{ level: 'full', value: 'farboo' }])); { const data = await service.resolveVariables('Hello @foo and@far', null!, CancellationToken.None); @@ -59,5 +62,8 @@ suite('ChatVariables', function () { assert.deepEqual(Object.keys(data.variables).sort(), ['far', 'foo']); assert.strictEqual(data.prompt, 'Hello [@foo](values:foo) and [@far](values:far) [@foo](values:foo) @unknown'); } + + v1.dispose(); + v2.dispose(); }); }); diff --git a/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts b/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts index 4c8f65b1588..b858435378c 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts @@ -20,6 +20,6 @@ import './toggleMultiCursorModifier'; import './toggleRenderControlCharacter'; import './toggleRenderWhitespace'; import './toggleWordWrap'; -import './untitledTextEditorHint/untitledTextEditorHint'; +import './emptyTextEditorHint/emptyTextEditorHint'; import './workbenchReferenceSearch'; import './editorLineNumberMenu'; diff --git a/src/vs/workbench/contrib/codeEditor/browser/untitledTextEditorHint/untitledTextEditorHint.css b/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.css similarity index 80% rename from src/vs/workbench/contrib/codeEditor/browser/untitledTextEditorHint/untitledTextEditorHint.css rename to src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.css index 538b988c4b3..649545a875c 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/untitledTextEditorHint/untitledTextEditorHint.css +++ b/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.css @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-editor .contentWidgets .untitled-hint { +.monaco-editor .contentWidgets .empty-editor-hint { color: var(--vscode-input-placeholderForeground); } -.monaco-editor .contentWidgets .untitled-hint a { +.monaco-editor .contentWidgets .empty-editor-hint a { color: var(--vscode-textLink-foreground) } diff --git a/src/vs/workbench/contrib/codeEditor/browser/untitledTextEditorHint/untitledTextEditorHint.ts b/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts similarity index 79% rename from src/vs/workbench/contrib/codeEditor/browser/untitledTextEditorHint/untitledTextEditorHint.ts rename to src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts index b2dd6a3cdea..5b715e5385e 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/untitledTextEditorHint/untitledTextEditorHint.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./untitledTextEditorHint'; +import 'vs/css!./emptyTextEditorHint'; import * as dom from 'vs/base/browser/dom'; import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; @@ -30,16 +30,36 @@ import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLa import { OS } from 'vs/base/common/platform'; import { status } from 'vs/base/browser/ui/aria/aria'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { Extensions, IConfigurationMigrationRegistry } from 'vs/workbench/common/configuration'; +import { LOG_MODE_ID, OUTPUT_MODE_ID } from 'vs/workbench/services/output/common/output'; +import { SEARCH_RESULT_LANGUAGE_ID } from 'vs/workbench/services/search/common/search'; +import { GHOST_TEXT_DESCRIPTION } from 'vs/editor/contrib/inlineCompletions/browser/ghostTextWidget'; const $ = dom.$; -const untitledTextEditorHintSetting = 'workbench.editor.untitled.hint'; -export class UntitledTextEditorHintContribution implements IEditorContribution { +// TODO@joyceerhl remove this after a few iterations +Registry.as(Extensions.ConfigurationMigration) + .registerConfigurationMigrations([{ + key: 'workbench.editor.untitled.hint', + migrateFn: (value, _accessor) => ([ + [emptyTextEditorHintSetting, { value }], + ]) + }, + { + key: 'accessibility.verbosity.untitledHint', + migrateFn: (value, _accessor) => ([ + [AccessibilityVerbositySettingId.EmptyEditorHint, { value }], + ]) + }]); - public static readonly ID = 'editor.contrib.untitledTextEditorHint'; +const emptyTextEditorHintSetting = 'workbench.editor.empty.hint'; +export class EmptyTextEditorHintContribution implements IEditorContribution { + + public static readonly ID = 'editor.contrib.emptyTextEditorHint'; private toDispose: IDisposable[]; - private untitledTextHintContentWidget: UntitledTextEditorHintContentWidget | undefined; + private textHintContentWidget: EmptyTextEditorHintContentWidget | undefined; constructor( private readonly editor: ICodeEditor, @@ -56,13 +76,13 @@ export class UntitledTextEditorHintContribution implements IEditorContribution { this.toDispose.push(this.editor.onDidChangeModel(() => this.update())); this.toDispose.push(this.editor.onDidChangeModelLanguage(() => this.update())); this.toDispose.push(this.configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(untitledTextEditorHintSetting)) { + if (e.affectsConfiguration(emptyTextEditorHintSetting)) { this.update(); } })); this.toDispose.push(inlineChatSessionService.onWillStartSession(editor => { if (this.editor === editor) { - this.untitledTextHintContentWidget?.dispose(); + this.textHintContentWidget?.dispose(); } })); this.toDispose.push(inlineChatSessionService.onDidEndSession(editor => { @@ -72,16 +92,42 @@ export class UntitledTextEditorHintContribution implements IEditorContribution { })); } - private update(): void { - this.untitledTextHintContentWidget?.dispose(); - const configValue = this.configurationService.getValue(untitledTextEditorHintSetting); + private _shouldRenderHint() { + const configValue = this.configurationService.getValue(emptyTextEditorHintSetting); + if (configValue === 'hidden') { + return false; + } + + if (this.editor.getOption(EditorOption.readOnly)) { + return false; + } + + const hasGhostText = this.editor.getLineDecorations(0)?.find((d) => d.options.description === GHOST_TEXT_DESCRIPTION); + if (hasGhostText) { + return false; + } + const model = this.editor.getModel(); + const languageId = model?.getLanguageId(); + if (languageId === OUTPUT_MODE_ID || languageId === LOG_MODE_ID || languageId === SEARCH_RESULT_LANGUAGE_ID) { + return false; + } + + const isNotebookCell = model?.uri.scheme === Schemas.vscodeNotebookCell; + if (isNotebookCell) { + return false; + } const inlineChatProviders = [...this.inlineChatService.getAllProvider()]; - const shouldRenderEitherDefaultOrInlineChatHint = model?.getLanguageId() === PLAINTEXT_LANGUAGE_ID && !inlineChatProviders.length || inlineChatProviders.length > 0; + const shouldRenderDefaultHint = model?.uri.scheme === Schemas.untitled && languageId === PLAINTEXT_LANGUAGE_ID && !inlineChatProviders.length; + return inlineChatProviders.length > 0 || shouldRenderDefaultHint; + } - if (model && model.uri.scheme === Schemas.untitled && shouldRenderEitherDefaultOrInlineChatHint && configValue === 'text') { - this.untitledTextHintContentWidget = new UntitledTextEditorHintContentWidget( + private update(): void { + this.textHintContentWidget?.dispose(); + + if (this._shouldRenderHint()) { + this.textHintContentWidget = new EmptyTextEditorHintContentWidget( this.editor, this.editorGroupsService, this.commandService, @@ -96,13 +142,13 @@ export class UntitledTextEditorHintContribution implements IEditorContribution { dispose(): void { dispose(this.toDispose); - this.untitledTextHintContentWidget?.dispose(); + this.textHintContentWidget?.dispose(); } } -class UntitledTextEditorHintContentWidget implements IContentWidget { +class EmptyTextEditorHintContentWidget implements IContentWidget { - private static readonly ID = 'editor.widget.untitledHint'; + private static readonly ID = 'editor.widget.emptyHint'; private domNode: HTMLElement | undefined; private toDispose: DisposableStore; @@ -129,7 +175,7 @@ class UntitledTextEditorHintContentWidget implements IContentWidget { })); const onDidFocusEditorText = Event.debounce(this.editor.onDidFocusEditorText, () => undefined, 500); this.toDispose.add(onDidFocusEditorText(() => { - if (this.editor.hasTextFocus() && this.isVisible && this.ariaLabel && this.configurationService.getValue(AccessibilityVerbositySettingId.EditorUntitledHint)) { + if (this.editor.hasTextFocus() && this.isVisible && this.ariaLabel && this.configurationService.getValue(AccessibilityVerbositySettingId.EmptyEditorHint)) { status(this.ariaLabel); } })); @@ -147,7 +193,7 @@ class UntitledTextEditorHintContentWidget implements IContentWidget { } getId(): string { - return UntitledTextEditorHintContentWidget.ID; + return EmptyTextEditorHintContentWidget.ID; } private _getHintInlineChat(providers: IInlineChatSessionProvider[]) { @@ -175,14 +221,14 @@ class UntitledTextEditorHintContentWidget implements IContentWidget { } }; - const hintElement = $('untitled-hint-text'); + const hintElement = $('empty-hint-text'); hintElement.style.display = 'block'; const keybindingHint = this.keybindingService.lookupKeybinding(inlineChatId); const keybindingHintLabel = keybindingHint?.getLabel(); if (keybindingHint && keybindingHintLabel) { - const actionPart = localize('untitledText', 'Press {0} to ask {1} to do something. ', keybindingHintLabel, providerName); + const actionPart = localize('emptyHintText', 'Press {0} to ask {1} to do something. ', keybindingHintLabel, providerName); const [before, after] = actionPart.split(keybindingHintLabel).map((fragment) => { const hintPart = $('a', undefined, fragment); @@ -203,7 +249,7 @@ class UntitledTextEditorHintContentWidget implements IContentWidget { hintElement.appendChild(after); - const typeToDismiss = localize('untitledText2', 'Start typing to dismiss.'); + const typeToDismiss = localize('emptyHintTextDismiss', 'Start typing to dismiss.'); const textHint2 = $('span', undefined, typeToDismiss); textHint2.style.fontStyle = 'italic'; hintElement.appendChild(textHint2); @@ -284,7 +330,7 @@ class UntitledTextEditorHintContentWidget implements IContentWidget { }; const dontShowOnClickOrTap = () => { - this.configurationService.updateValue(untitledTextEditorHintSetting, 'hidden'); + this.configurationService.updateValue(emptyTextEditorHintSetting, 'hidden'); this.dispose(); this.editor.focus(); }; @@ -318,14 +364,14 @@ class UntitledTextEditorHintContentWidget implements IContentWidget { getDomNode(): HTMLElement { if (!this.domNode) { - this.domNode = $('.untitled-hint'); + this.domNode = $('.empty-editor-hint'); this.domNode.style.width = 'max-content'; this.domNode.style.paddingLeft = '4px'; const inlineChatProviders = [...this.inlineChatService.getAllProvider()]; const { hintElement, ariaLabel } = !inlineChatProviders.length ? this._getHintDefault() : this._getHintInlineChat(inlineChatProviders); this.domNode.append(hintElement); - this.ariaLabel = ariaLabel.concat(localize('disableHint', ' Toggle {0} in settings to disable this hint.', AccessibilityVerbositySettingId.EditorUntitledHint)); + this.ariaLabel = ariaLabel.concat(localize('disableHint', ' Toggle {0} in settings to disable this hint.', AccessibilityVerbositySettingId.EmptyEditorHint)); this.toDispose.add(dom.addDisposableListener(this.domNode, 'click', () => { this.editor.focus(); @@ -350,4 +396,4 @@ class UntitledTextEditorHintContentWidget implements IContentWidget { } } -registerEditorContribution(UntitledTextEditorHintContribution.ID, UntitledTextEditorHintContribution, EditorContributionInstantiation.Eager); // eager because it needs to render a help message +registerEditorContribution(EmptyTextEditorHintContribution.ID, EmptyTextEditorHintContribution, EditorContributionInstantiation.Eager); // eager because it needs to render a help message diff --git a/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts b/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts index 9c2bf728cef..f8960304793 100644 --- a/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts +++ b/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts @@ -8,7 +8,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { FinalNewLineParticipant, TrimFinalNewLinesParticipant, TrimWhitespaceParticipant } from 'vs/workbench/contrib/codeEditor/browser/saveParticipants'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench/test/browser/workbenchTestServices'; -import { toResource } from 'vs/base/test/common/utils'; +import { ensureNoDisposablesAreLeakedInTestSuite, toResource } from 'vs/base/test/common/utils'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; @@ -19,23 +19,22 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; suite('Save Participants', function () { - let disposables: DisposableStore; + const disposables = new DisposableStore(); let instantiationService: IInstantiationService; let accessor: TestServiceAccessor; setup(() => { - disposables = new DisposableStore(); instantiationService = workbenchInstantiationService(undefined, disposables); accessor = instantiationService.createInstance(TestServiceAccessor); + disposables.add(accessor.textFileService.files); }); teardown(() => { - (accessor.textFileService.files).dispose(); - disposables.dispose(); + disposables.clear(); }); test('insert final new line', async function () { - const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/final_new_line.txt'), 'utf8', undefined) as IResolvedTextFileEditorModel; + const model: IResolvedTextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/final_new_line.txt'), 'utf8', undefined) as IResolvedTextFileEditorModel); await model.resolve(); const configService = new TestConfigurationService(); @@ -68,7 +67,7 @@ suite('Save Participants', function () { }); test('trim final new lines', async function () { - const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/trim_final_new_line.txt'), 'utf8', undefined) as IResolvedTextFileEditorModel; + const model: IResolvedTextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/trim_final_new_line.txt'), 'utf8', undefined) as IResolvedTextFileEditorModel); await model.resolve(); const configService = new TestConfigurationService(); @@ -103,7 +102,7 @@ suite('Save Participants', function () { }); test('trim final new lines bug#39750', async function () { - const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/trim_final_new_line.txt'), 'utf8', undefined) as IResolvedTextFileEditorModel; + const model: IResolvedTextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/trim_final_new_line.txt'), 'utf8', undefined) as IResolvedTextFileEditorModel); await model.resolve(); const configService = new TestConfigurationService(); @@ -130,7 +129,7 @@ suite('Save Participants', function () { }); test('trim final new lines bug#46075', async function () { - const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/trim_final_new_line.txt'), 'utf8', undefined) as IResolvedTextFileEditorModel; + const model: IResolvedTextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/trim_final_new_line.txt'), 'utf8', undefined) as IResolvedTextFileEditorModel); await model.resolve(); const configService = new TestConfigurationService(); @@ -157,7 +156,7 @@ suite('Save Participants', function () { }); test('trim whitespace', async function () { - const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/trim_final_new_line.txt'), 'utf8', undefined) as IResolvedTextFileEditorModel; + const model: IResolvedTextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/trim_final_new_line.txt'), 'utf8', undefined) as IResolvedTextFileEditorModel); await model.resolve(); const configService = new TestConfigurationService(); @@ -175,4 +174,6 @@ suite('Save Participants', function () { // confirm trimming assert.strictEqual(snapshotToString(model.createSnapshot()!), `${textContent}`); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/contrib/comments/browser/comments.contribution.ts b/src/vs/workbench/contrib/comments/browser/comments.contribution.ts index 803aa80a18e..bec1b69199e 100644 --- a/src/vs/workbench/contrib/comments/browser/comments.contribution.ts +++ b/src/vs/workbench/contrib/comments/browser/comments.contribution.ts @@ -24,8 +24,8 @@ Registry.as(ConfigurationExtensions.Configuration).regis markdownDeprecationMessage: nls.localize('comments.openPanel.deprecated', "This setting is deprecated in favor of `comments.openView`.") }, 'comments.openView': { - enum: ['never', 'file', 'firstFile'], - enumDescriptions: [nls.localize('comments.openView.never', "The comments view will never be opened."), nls.localize('comments.openView.file', "The comments view will open when a file with comments is active."), nls.localize('comments.openView.firstFile', "If the comments view has not been opened yet during this session it will open the first time during a session that a file with comments is active.")], + enum: ['never', 'file', 'firstFile', 'firstFileUnresolved'], + enumDescriptions: [nls.localize('comments.openView.never', "The comments view will never be opened."), nls.localize('comments.openView.file', "The comments view will open when a file with comments is active."), nls.localize('comments.openView.firstFile', "If the comments view has not been opened yet during this session it will open the first time during a session that a file with comments is active."), nls.localize('comments.openView.firstFileUnresolved', "If the comments view has not been opened yet during this session and the comment is not resolved, it will open the first time during a session that a file with comments is active.")], default: 'firstFile', description: nls.localize('comments.openView', "Controls when the comments view should open."), restricted: false diff --git a/src/vs/workbench/contrib/comments/browser/commentsController.ts b/src/vs/workbench/contrib/comments/browser/commentsController.ts index 9fac6af0a65..de414ee854f 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsController.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsController.ts @@ -727,9 +727,10 @@ export class CommentController implements IEditorContribution { private async openCommentsView(thread: languages.CommentThread) { if (thread.comments && (thread.comments.length > 0)) { - if (this.configurationService.getValue(COMMENTS_SECTION).openView === 'file') { + const openViewState = this.configurationService.getValue(COMMENTS_SECTION).openView; + if (openViewState === 'file') { return this.viewsService.openView(COMMENTS_VIEW_ID); - } else if (this.configurationService.getValue(COMMENTS_SECTION).openView === 'firstFile') { + } else if (openViewState === 'firstFile' || (openViewState === 'firstFileUnresolved' && thread.state === languages.CommentThreadState.Unresolved)) { const hasShownView = this.viewsService.getViewWithId(COMMENTS_VIEW_ID)?.hasRendered; if (!hasShownView) { return this.viewsService.openView(COMMENTS_VIEW_ID); @@ -740,7 +741,7 @@ export class CommentController implements IEditorContribution { } private displayCommentThread(owner: string, thread: languages.CommentThread, pendingComment: string | undefined, pendingEdits: { [key: number]: string } | undefined): void { - if (!this.editor) { + if (!this.editor?.getModel()) { return; } if (this.isEditorInlineOriginal(this.editor)) { diff --git a/src/vs/workbench/contrib/comments/common/commentsConfiguration.ts b/src/vs/workbench/contrib/comments/common/commentsConfiguration.ts index efc9d016415..c996a07373b 100644 --- a/src/vs/workbench/contrib/comments/common/commentsConfiguration.ts +++ b/src/vs/workbench/contrib/comments/common/commentsConfiguration.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ export interface ICommentsConfiguration { - openView: 'never' | 'file' | 'firstFile'; + openView: 'never' | 'file' | 'firstFile' | 'firstFileUnresolved'; useRelativeTime: boolean; visible: boolean; maxHeight: boolean; diff --git a/src/vs/workbench/contrib/comments/test/browser/commentsView.test.ts b/src/vs/workbench/contrib/comments/test/browser/commentsView.test.ts index 83d2bf1ccea..21dff60d1b9 100644 --- a/src/vs/workbench/contrib/comments/test/browser/commentsView.test.ts +++ b/src/vs/workbench/contrib/comments/test/browser/commentsView.test.ts @@ -16,6 +16,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; class TestCommentThread implements CommentThread { isDocumentCommentThread(): this is CommentThread { @@ -70,6 +71,13 @@ export class TestViewDescriptorService implements Partial { + instantiationService.dispose(); + commentService.dispose(); + disposables.dispose(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); let disposables: DisposableStore; let instantiationService: TestInstantiationService; @@ -85,10 +93,7 @@ suite('Comments View', function () { instantiationService.stub(ICommentService, commentService); }); - teardown(() => { - commentService.dispose(); - disposables.dispose(); - }); + test('collapse all', async function () { const view = instantiationService.createInstance(CommentsPanel, { id: 'comments', title: 'Comments' }); diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index 2aaf7c47254..fc1a0120f48 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -161,7 +161,7 @@ export class BreakpointsView extends ViewPane { } else if (element instanceof DataBreakpoint) { await this.debugService.removeDataBreakpoints(element.getId()); } else if (element instanceof InstructionBreakpoint) { - await this.debugService.removeInstructionBreakpoints(element.instructionReference); + await this.debugService.removeInstructionBreakpoints(element.instructionReference, element.offset); } }); @@ -180,7 +180,7 @@ export class BreakpointsView extends ViewPane { if (e.element instanceof InstructionBreakpoint) { const disassemblyView = await this.editorService.openEditor(DisassemblyViewInput.instance); // Focus on double click - (disassemblyView as DisassemblyView).goToAddress(e.element.instructionReference, e.browserEvent instanceof MouseEvent && e.browserEvent.detail === 2); + (disassemblyView as DisassemblyView).goToInstructionAndOffset(e.element.instructionReference, e.element.offset, e.browserEvent instanceof MouseEvent && e.browserEvent.detail === 2); } if (e.browserEvent instanceof MouseEvent && e.browserEvent.detail === 2 && e.element instanceof FunctionBreakpoint && e.element !== this.inputBoxData?.breakpoint) { // double click @@ -788,7 +788,8 @@ class InstructionBreakpointsRenderer implements IListRenderer { - // TODO: add disassembly F9 - if (editor.hasModel()) { - const debugService = accessor.get(IDebugService); + async run(accessor: ServicesAccessor): Promise { + const editorService = accessor.get(IEditorService); + const debugService = accessor.get(IDebugService); + + const activePane = editorService.activeEditorPane; + if (activePane instanceof DisassemblyView) { + const location = activePane.focusedAddressAndOffset; + if (location) { + const bps = debugService.getModel().getInstructionBreakpoints(); + const toRemove = bps.find(bp => bp.address === location.address); + if (toRemove) { + debugService.removeInstructionBreakpoints(toRemove.instructionReference, toRemove.offset); + } else { + debugService.addInstructionBreakpoint(location.reference, location.offset, location.address); + } + } + return; + } + + const codeEditorService = accessor.get(ICodeEditorService); + const editor = codeEditorService.getFocusedCodeEditor() || codeEditorService.getActiveCodeEditor(); + if (editor?.hasModel()) { const modelUri = editor.getModel().uri; const canSet = debugService.canSetBreakpointsIn(editor.getModel()); // Does not account for multi line selections, Set to remove multiple cursor on the same line const lineNumbers = [...new Set(editor.getSelections().map(s => s.getPosition().lineNumber))]; + const bps = debugService.getModel().getBreakpoints(); await Promise.all(lineNumbers.map(async line => { - const bps = debugService.getModel().getBreakpoints({ lineNumber: line, uri: modelUri }); if (bps.length) { await Promise.all(bps.map(bp => debugService.removeBreakpoints(bp.getId()))); } else if (canSet) { @@ -210,7 +232,7 @@ class OpenDisassemblyViewAction extends EditorAction2 { runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ...args: any[]): void { if (editor.hasModel()) { const editorService = accessor.get(IEditorService); - editorService.openEditor(DisassemblyViewInput.instance, { pinned: true }); + editorService.openEditor(DisassemblyViewInput.instance, { pinned: true, revealIfOpened: true }); } } } @@ -251,7 +273,7 @@ export class RunToCursorAction extends EditorAction { id: RunToCursorAction.ID, label: RunToCursorAction.LABEL, alias: 'Debug: Run to Cursor', - precondition: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, PanelFocusContext.toNegated(), EditorContextKeys.editorTextFocus), + precondition: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, PanelFocusContext.toNegated(), ContextKeyExpr.or(EditorContextKeys.editorTextFocus, CONTEXT_DISASSEMBLY_VIEW_FOCUS)), contextMenuOpts: { group: 'debug', order: 2, @@ -572,7 +594,7 @@ class CloseExceptionWidgetAction extends EditorAction { registerAction2(OpenDisassemblyViewAction); registerAction2(ToggleDisassemblyViewSourceCodeAction); -registerEditorAction(ToggleBreakpointAction); +registerAction2(ToggleBreakpointAction); registerEditorAction(ConditionalBreakpointAction); registerEditorAction(LogPointAction); registerEditorAction(EditBreakpointAction); diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts index 06c4fa63988..6cf4c61e3cc 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts @@ -8,37 +8,40 @@ import { DomEmitter } from 'vs/base/browser/event'; import { IKeyboardEvent, StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { distinct, flatten } from 'vs/base/common/arrays'; import { RunOnceScheduler } from 'vs/base/common/async'; -import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { memoize } from 'vs/base/common/decorators'; -import { onUnexpectedExternalError } from 'vs/base/common/errors'; +import { illegalArgument, onUnexpectedExternalError } from 'vs/base/common/errors'; import { Event } from 'vs/base/common/event'; import { visit } from 'vs/base/common/json'; import { setProperty } from 'vs/base/common/jsonEdit'; import { KeyCode } from 'vs/base/common/keyCodes'; -import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { basename } from 'vs/base/common/path'; import * as env from 'vs/base/common/platform'; import * as strings from 'vs/base/common/strings'; +import { assertType, isDefined } from 'vs/base/common/types'; import { Constants } from 'vs/base/common/uint'; +import { URI } from 'vs/base/common/uri'; import { CoreEditingCommands } from 'vs/editor/browser/coreCommands'; import { ICodeEditor, IEditorMouseEvent, IPartialEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { EditorOption, IEditorHoverOptions } from 'vs/editor/common/config/editorOptions'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; -import { Range } from 'vs/editor/common/core/range'; +import { IRange, Range } from 'vs/editor/common/core/range'; import { DEFAULT_WORD_REGEXP } from 'vs/editor/common/core/wordHelper'; import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; -import { InlineValueContext } from 'vs/editor/common/languages'; -import { IModelDeltaDecoration, InjectedTextCursorStops, ITextModel } from 'vs/editor/common/model'; +import { InlineValue, InlineValueContext } from 'vs/editor/common/languages'; +import { IModelDeltaDecoration, ITextModel, InjectedTextCursorStops } from 'vs/editor/common/model'; import { IFeatureDebounceInformation, ILanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { IModelService } from 'vs/editor/common/services/model'; import { ModesHoverController } from 'vs/editor/contrib/hover/browser/hover'; import { HoverStartMode, HoverStartSource } from 'vs/editor/contrib/hover/browser/hoverOperation'; import * as nls from 'vs/nls'; -import { ICommandService } from 'vs/platform/commands/common/commands'; +import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { registerColor } from 'vs/platform/theme/common/colorRegistry'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { FloatingEditorClickWidget } from 'vs/workbench/browser/codeeditor'; @@ -787,3 +790,31 @@ export class DebugEditorContribution implements IDebugEditorContribution { this.oldDecorations.clear(); } } + + +CommandsRegistry.registerCommand( + '_executeInlineValueProvider', + async ( + accessor: ServicesAccessor, + uri: URI, + iRange: IRange, + context: InlineValueContext + ): Promise => { + assertType(URI.isUri(uri)); + assertType(Range.isIRange(iRange)); + + if (!context || typeof context.frameId !== 'number' || !Range.isIRange(context.stoppedLocation)) { + throw illegalArgument('context'); + } + + const model = accessor.get(IModelService).getModel(uri); + if (!model) { + throw illegalArgument('uri'); + } + + const range = Range.lift(iRange); + const { inlineValuesProvider } = accessor.get(ILanguageFeaturesService); + const providers = inlineValuesProvider.ordered(model); + const providerResults = await Promise.all(providers.map(provider => provider.provideInlineValues(model, range, context, CancellationToken.None))); + return providerResults.flat().filter(isDefined); + }); diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index 7e239aaa285..dbea3cefecb 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -1052,15 +1052,15 @@ export class DebugService implements IDebugService { await this.sendDataBreakpoints(); } - async addInstructionBreakpoint(address: string, offset: number, condition?: string, hitCondition?: string): Promise { - this.model.addInstructionBreakpoint(address, offset, condition, hitCondition); + async addInstructionBreakpoint(instructionReference: string, offset: number, address: bigint, condition?: string, hitCondition?: string): Promise { + this.model.addInstructionBreakpoint(instructionReference, offset, address, condition, hitCondition); this.debugStorage.storeBreakpoints(this.model); await this.sendInstructionBreakpoints(); this.debugStorage.storeBreakpoints(this.model); } - async removeInstructionBreakpoints(address?: string): Promise { - this.model.removeInstructionBreakpoints(address); + async removeInstructionBreakpoints(instructionReference?: string, offset?: number): Promise { + this.model.removeInstructionBreakpoints(instructionReference, offset); this.debugStorage.storeBreakpoints(this.model); await this.sendInstructionBreakpoints(); } diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index d95dc1fbf6e..f246e8609d7 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -536,7 +536,7 @@ export class DebugSession implements IDebugSession, IDisposable { } if (this.raw.readyForBreakpoints) { - const response = await this.raw.setInstructionBreakpoints({ breakpoints: instructionBreakpoints }); + const response = await this.raw.setInstructionBreakpoints({ breakpoints: instructionBreakpoints.map(ib => ib.toJSON()) }); if (response && response.body) { const data = new Map(); for (let i = 0; i < instructionBreakpoints.length; i++) { diff --git a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts index 7e4ee4af327..7c720d874d3 100644 --- a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts +++ b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts @@ -37,6 +37,7 @@ import { CONTINUE_ID, CONTINUE_LABEL, DISCONNECT_AND_SUSPEND_ID, DISCONNECT_AND_ import * as icons from 'vs/workbench/contrib/debug/browser/debugIcons'; import { CONTEXT_DEBUG_STATE, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_MULTI_SESSION_DEBUG, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_SUSPEND_DEBUGGEE_SUPPORTED, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED, IDebugConfiguration, IDebugService, State, VIEWLET_ID } from 'vs/workbench/contrib/debug/common/debug'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; +import { Codicon } from 'vs/base/common/codicons'; const DEBUG_TOOLBAR_POSITION_KEY = 'debug.actionswidgetposition'; const DEBUG_TOOLBAR_Y_KEY = 'debug.actionswidgety'; @@ -102,8 +103,7 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { const toolBarLocation = this.configurationService.getValue('debug').toolBarLocation; if ( state === State.Inactive || - toolBarLocation === 'docked' || - toolBarLocation === 'hidden' || + toolBarLocation !== 'floating' || this.debugService.getModel().getSessions().every(s => s.suppressDebugToolbar) || (state === State.Initializing && this.debugService.initializingOptions?.suppressDebugToolbar) ) { @@ -335,6 +335,15 @@ MenuRegistry.onDidChangeMenu(e => { } }); + +MenuRegistry.appendMenuItem(MenuId.CommandCenterCenter, { + submenu: MenuId.DebugToolBar, + title: 'Debug', + icon: Codicon.debug, + order: 1, + when: ContextKeyExpr.and(CONTEXT_DEBUG_STATE.notEqualsTo('inactive'), ContextKeyExpr.equals('config.debug.toolBarLocation', 'commandCenter')) +}); + registerDebugToolBarItem(CONTINUE_ID, CONTINUE_LABEL, 10, icons.debugContinue, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); registerDebugToolBarItem(PAUSE_ID, PAUSE_LABEL, 10, icons.debugPause, CONTEXT_DEBUG_STATE.notEqualsTo('stopped'), CONTEXT_DEBUG_STATE.isEqualTo('running')); registerDebugToolBarItem(STOP_ID, STOP_LABEL, 70, icons.debugStop, CONTEXT_FOCUSED_SESSION_IS_ATTACH.toNegated(), undefined, { id: DISCONNECT_ID, title: DISCONNECT_LABEL, icon: icons.debugDisconnect, precondition: ContextKeyExpr.and(CONTEXT_FOCUSED_SESSION_IS_ATTACH.toNegated(), CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED), }); diff --git a/src/vs/workbench/contrib/debug/browser/disassemblyView.ts b/src/vs/workbench/contrib/debug/browser/disassemblyView.ts index 1112a89f08a..53996d57321 100644 --- a/src/vs/workbench/contrib/debug/browser/disassemblyView.ts +++ b/src/vs/workbench/contrib/debug/browser/disassemblyView.ts @@ -4,63 +4,77 @@ *--------------------------------------------------------------------------------------------*/ import { PixelRatio } from 'vs/base/browser/browser'; -import { Dimension, append, $, addStandardDisposableListener } from 'vs/base/browser/dom'; +import { $, Dimension, addStandardDisposableListener, append } from 'vs/base/browser/dom'; +import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { ITableRenderer, ITableVirtualDelegate } from 'vs/base/browser/ui/table/table'; +import { binarySearch2 } from 'vs/base/common/arrays'; +import { Color } from 'vs/base/common/color'; +import { Emitter } from 'vs/base/common/event'; +import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { isAbsolute } from 'vs/base/common/path'; +import { Constants } from 'vs/base/common/uint'; +import { URI } from 'vs/base/common/uri'; +import { applyFontInfo } from 'vs/editor/browser/config/domFontInfo'; +import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; +import { IRange, Range } from 'vs/editor/common/core/range'; +import { StringBuilder } from 'vs/editor/common/core/stringBuilder'; +import { ITextModel } from 'vs/editor/common/model'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { WorkbenchTable } from 'vs/platform/list/browser/listService'; +import { ILogService } from 'vs/platform/log/common/log'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { editorBackground } from 'vs/platform/theme/common/colorRegistry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; -import { CONTEXT_LANGUAGE_SUPPORTS_DISASSEMBLE_REQUEST, DISASSEMBLY_VIEW_ID, IDebugService, IDebugSession, IInstructionBreakpoint, State, IDebugConfiguration } from 'vs/workbench/contrib/debug/common/debug'; -import * as icons from 'vs/workbench/contrib/debug/browser/debugIcons'; -import { StringBuilder } from 'vs/editor/common/core/stringBuilder'; -import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; -import { dispose, Disposable, IDisposable } from 'vs/base/common/lifecycle'; -import { Emitter } from 'vs/base/common/event'; -import { topStackFrameColor, focusedStackFrameColor } from 'vs/workbench/contrib/debug/browser/callStackEditorContribution'; -import { Color } from 'vs/base/common/color'; -import { InstructionBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; -import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; -import { getUriFromSource } from 'vs/workbench/contrib/debug/common/debugSource'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; -import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { ITextModel } from 'vs/editor/common/model'; -import { TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor'; -import { IRange, Range } from 'vs/editor/common/core/range'; -import { URI } from 'vs/base/common/uri'; -import { isUri } from 'vs/workbench/contrib/debug/common/debugUtils'; -import { isAbsolute } from 'vs/base/common/path'; -import { Constants } from 'vs/base/common/uint'; -import { applyFontInfo } from 'vs/editor/browser/config/domFontInfo'; -import { binarySearch2 } from 'vs/base/common/arrays'; -import { ILogService } from 'vs/platform/log/common/log'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { focusedStackFrameColor, topStackFrameColor } from 'vs/workbench/contrib/debug/browser/callStackEditorContribution'; +import * as icons from 'vs/workbench/contrib/debug/browser/debugIcons'; +import { CONTEXT_LANGUAGE_SUPPORTS_DISASSEMBLE_REQUEST, DISASSEMBLY_VIEW_ID, IDebugConfiguration, IDebugService, IDebugSession, IInstructionBreakpoint, State } from 'vs/workbench/contrib/debug/common/debug'; +import { InstructionBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; +import { getUriFromSource } from 'vs/workbench/contrib/debug/common/debugSource'; +import { isUri, sourcesEqual } from 'vs/workbench/contrib/debug/common/debugUtils'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; interface IDisassembledInstructionEntry { allowBreakpoint: boolean; isBreakpointSet: boolean; isBreakpointEnabled: boolean; + /** Instruction reference from the DA */ + instructionReference: string; + /** Offset from the instructionReference that's the basis for the `instructionOffset` */ + instructionReferenceOffset: number; + /** The number of instructions (+/-) away from the instructionReference and instructionReferenceOffset this instruction lies */ + instructionOffset: number; + /** Whether this is the first instruction on the target line. */ + showSourceLocation?: boolean; + /** Original instruction from the debugger */ instruction: DebugProtocol.DisassembledInstruction; - instructionAddress?: bigint; + /** Parsed instruction address */ + address: bigint; } + // Special entry as a placeholer when disassembly is not available const disassemblyNotAvailable: IDisassembledInstructionEntry = { allowBreakpoint: false, isBreakpointSet: false, isBreakpointEnabled: false, + instructionReference: '', + instructionOffset: 0, + instructionReferenceOffset: 0, + address: 0n, instruction: { address: '-1', instruction: localize('instructionNotAvailable', "Disassembly not available.") }, - instructionAddress: BigInt(-1) }; export class DisassemblyView extends EditorPane { @@ -75,6 +89,7 @@ export class DisassemblyView extends EditorPane { private _instructionBpList: readonly IInstructionBreakpoint[] = []; private _enableSourceCodeRender: boolean = true; private _loadingLock: boolean = false; + private readonly _referenceToMemoryAddress = new Map(); constructor( @ITelemetryService telemetryService: ITelemetryService, @@ -87,7 +102,7 @@ export class DisassemblyView extends EditorPane { super(DISASSEMBLY_VIEW_ID, telemetryService, themeService, storageService); this._disassembledInstructions = undefined; - this._onDidChangeStackFrame = new Emitter(); + this._onDidChangeStackFrame = this._register(new Emitter({ leakWarningThreshold: 1000 })); this._previousDebuggingState = _debugService.state; this._fontInfo = BareFontInfo.createFromRawSettings(_configurationService.getValue('editor'), PixelRatio.value); this._register(_configurationService.onDidChangeConfiguration(e => { @@ -100,7 +115,7 @@ export class DisassemblyView extends EditorPane { const newValue = this._configurationService.getValue('debug').disassemblyView.showSourceCode; if (this._enableSourceCodeRender !== newValue) { this._enableSourceCodeRender = newValue; - this.reloadDisassembly(undefined); + // todo: trigger rerender } else { this._disassembledInstructions?.rerender(); } @@ -115,18 +130,29 @@ export class DisassemblyView extends EditorPane { map(session => session.getAllThreads()). reduce((prev, curr) => prev.concat(curr), []). map(thread => thread.getTopStackFrame()). - map(frame => frame?.instructionPointerReference); + map(frame => frame?.instructionPointerReference). + map(ref => ref ? this.getReferenceAddress(ref) : undefined); } - // Instruction address of the top stack frame of the focused stack - get focusedCurrentInstructionAddress() { + // Instruction reference of the top stack frame of the focused stack + get focusedCurrentInstructionReference() { return this._debugService.getViewModel().focusedStackFrame?.thread.getTopStackFrame()?.instructionPointerReference; } - get focusedInstructionAddress() { + get focusedCurrentInstructionAddress() { + const ref = this.focusedCurrentInstructionReference; + return ref ? this.getReferenceAddress(ref) : undefined; + } + + get focusedInstructionReference() { return this._debugService.getViewModel().focusedStackFrame?.instructionPointerReference; } + get focusedInstructionAddress() { + const ref = this.focusedInstructionReference; + return ref ? this.getReferenceAddress(ref) : undefined; + } + get isSourceCodeRender() { return this._enableSourceCodeRender; } get debugSession(): IDebugSession | undefined { @@ -135,6 +161,17 @@ export class DisassemblyView extends EditorPane { get onDidChangeStackFrame() { return this._onDidChangeStackFrame.event; } + get focusedAddressAndOffset() { + const element = this._disassembledInstructions?.getFocusedElements()[0]; + if (!element) { + return undefined; + } + + const reference = element.instructionReference; + const offset = Number(element.address - this.getReferenceAddress(reference)!); + return { reference, offset, address: element.address }; + } + protected createEditor(parent: HTMLElement): void { this._enableSourceCodeRender = this._configurationService.getValue('debug').disassemblyView.showSourceCode; const lineHeight = this.fontInfo.lineHeight; @@ -142,7 +179,7 @@ export class DisassemblyView extends EditorPane { const delegate = new class implements ITableVirtualDelegate{ headerRowHeight: number = 0; // No header getHeight(row: IDisassembledInstructionEntry): number { - if (thisOM.isSourceCodeRender && row.instruction.location?.path && row.instruction.line) { + if (thisOM.isSourceCodeRender && row.showSourceLocation && row.instruction.location?.path && row.instruction.line) { // instruction line + source lines if (row.instruction.endLine) { return lineHeight * (row.instruction.endLine - row.instruction.line + 2); @@ -197,7 +234,9 @@ export class DisassemblyView extends EditorPane { } )) as WorkbenchTable; - this.reloadDisassembly(); + if (this.focusedInstructionReference) { + this.reloadDisassembly(this.focusedInstructionReference, 0); + } this._register(this._disassembledInstructions.onDidScroll(e => { if (this._loadingLock) { @@ -206,10 +245,10 @@ export class DisassemblyView extends EditorPane { if (e.oldScrollTop > e.scrollTop && e.scrollTop < e.height) { this._loadingLock = true; - const topElement = Math.floor(e.scrollTop / this.fontInfo.lineHeight) + DisassemblyView.NUM_INSTRUCTIONS_TO_LOAD; - this.scrollUp_LoadDisassembledInstructions(DisassemblyView.NUM_INSTRUCTIONS_TO_LOAD).then((success) => { - if (success) { - this._disassembledInstructions!.reveal(topElement, 0); + const prevTop = Math.floor(e.scrollTop / this.fontInfo.lineHeight); + this.scrollUp_LoadDisassembledInstructions(DisassemblyView.NUM_INSTRUCTIONS_TO_LOAD).then((loaded) => { + if (loaded > 0) { + this._disassembledInstructions!.reveal(prevTop + loaded, 0); } this._loadingLock = false; }); @@ -219,11 +258,11 @@ export class DisassemblyView extends EditorPane { } })); - this._register(this._debugService.getViewModel().onDidFocusStackFrame((stackFrame) => { - if (this._disassembledInstructions) { - this.goToAddress(); - this._onDidChangeStackFrame.fire(); + this._register(this._debugService.getViewModel().onDidFocusStackFrame(({ stackFrame }) => { + if (this._disassembledInstructions && stackFrame?.instructionPointerReference) { + this.goToInstructionAndOffset(stackFrame.instructionPointerReference, 0); } + this._onDidChangeStackFrame.fire(); })); // refresh breakpoints view @@ -233,7 +272,7 @@ export class DisassemblyView extends EditorPane { let changed = false; bpEvent.added?.forEach((bp) => { if (bp instanceof InstructionBreakpoint) { - const index = this.getIndexFromAddress(bp.instructionReference); + const index = this.getIndexFromReferenceAndOffset(bp.instructionReference, bp.offset); if (index >= 0) { this._disassembledInstructions!.row(index).isBreakpointSet = true; this._disassembledInstructions!.row(index).isBreakpointEnabled = bp.enabled; @@ -244,7 +283,7 @@ export class DisassemblyView extends EditorPane { bpEvent.removed?.forEach((bp) => { if (bp instanceof InstructionBreakpoint) { - const index = this.getIndexFromAddress(bp.instructionReference); + const index = this.getIndexFromReferenceAndOffset(bp.instructionReference, bp.offset); if (index >= 0) { this._disassembledInstructions!.row(index).isBreakpointSet = false; changed = true; @@ -254,7 +293,7 @@ export class DisassemblyView extends EditorPane { bpEvent.changed?.forEach((bp) => { if (bp instanceof InstructionBreakpoint) { - const index = this.getIndexFromAddress(bp.instructionReference); + const index = this.getIndexFromReferenceAndOffset(bp.instructionReference, bp.offset); if (index >= 0) { if (this._disassembledInstructions!.row(index).isBreakpointEnabled !== bp.enabled) { this._disassembledInstructions!.row(index).isBreakpointEnabled = bp.enabled; @@ -267,6 +306,13 @@ export class DisassemblyView extends EditorPane { // get an updated list so that items beyond the current range would render when reached. this._instructionBpList = this._debugService.getModel().getInstructionBreakpoints(); + // breakpoints restored from a previous session can be based on memory + // references that may no longer exist in the current session. Request + // those instructions to be loaded so the BP can be displayed. + for (const bp of this._instructionBpList) { + this.primeMemoryReference(bp.instructionReference); + } + if (changed) { this._onDidChangeStackFrame.fire(); } @@ -277,10 +323,12 @@ export class DisassemblyView extends EditorPane { if ((e === State.Running || e === State.Stopped) && (this._previousDebuggingState !== State.Running && this._previousDebuggingState !== State.Stopped)) { // Just started debugging, clear the view - this._disassembledInstructions?.splice(0, this._disassembledInstructions.length, [disassemblyNotAvailable]); + this.clear(); this._enableSourceCodeRender = this._configurationService.getValue('debug').disassemblyView.showSourceCode; } + this._previousDebuggingState = e; + this._onDidChangeStackFrame.fire(); })); } @@ -288,19 +336,33 @@ export class DisassemblyView extends EditorPane { this._disassembledInstructions?.layout(dimension.height); } + async goToInstructionAndOffset(instructionReference: string, offset: number, focus?: boolean) { + let addr = this._referenceToMemoryAddress.get(instructionReference); + if (addr === undefined) { + await this.loadDisassembledInstructions(instructionReference, 0, -DisassemblyView.NUM_INSTRUCTIONS_TO_LOAD, DisassemblyView.NUM_INSTRUCTIONS_TO_LOAD); + addr = this._referenceToMemoryAddress.get(instructionReference); + } + + if (addr) { + this.goToAddress(addr + BigInt(offset), focus); + } + } + + /** Gets the address associated with the instruction reference. */ + getReferenceAddress(instructionReference: string) { + return this._referenceToMemoryAddress.get(instructionReference); + } + /** - * Go to the address provided. If no address is provided, reveal the address of the currently focused stack frame. + * Go to the address provided. If no address is provided, reveal the address of the currently focused stack frame. Returns false if that address is not available. */ - goToAddress(address?: string, focus?: boolean): void { + private goToAddress(address: bigint, focus?: boolean): boolean { if (!this._disassembledInstructions) { - return; + return false; } if (!address) { - address = this.focusedInstructionAddress; - } - if (!address) { - return; + return false; } const index = this.getIndexFromAddress(address); @@ -311,51 +373,82 @@ export class DisassemblyView extends EditorPane { this._disassembledInstructions.domFocus(); this._disassembledInstructions.setFocus([index]); } - } else if (this._debugService.state === State.Stopped) { - // Address is not provided or not in the table currently, clear the table - // and reload if we are in the state where we can load disassembly. - this.reloadDisassembly(address); - } - } - - private async scrollUp_LoadDisassembledInstructions(instructionCount: number): Promise { - if (this._disassembledInstructions && this._disassembledInstructions.length > 0) { - const address: string | undefined = this._disassembledInstructions?.row(0).instruction.address; - return this.loadDisassembledInstructions(address, -instructionCount, instructionCount); + return true; } return false; } - private async scrollDown_LoadDisassembledInstructions(instructionCount: number): Promise { - if (this._disassembledInstructions && this._disassembledInstructions.length > 0) { - const address: string | undefined = this._disassembledInstructions?.row(this._disassembledInstructions?.length - 1).instruction.address; - return this.loadDisassembledInstructions(address, 1, instructionCount); + private async scrollUp_LoadDisassembledInstructions(instructionCount: number): Promise { + const first = this._disassembledInstructions?.row(0); + if (first) { + return this.loadDisassembledInstructions( + first.instructionReference, + first.instructionReferenceOffset, + first.instructionOffset - instructionCount, + instructionCount, + ); + } + + return 0; + } + + private async scrollDown_LoadDisassembledInstructions(instructionCount: number): Promise { + const last = this._disassembledInstructions?.row(this._disassembledInstructions?.length - 1); + if (last) { + return this.loadDisassembledInstructions( + last.instructionReference, + last.instructionReferenceOffset, + last.instructionOffset + 1, + instructionCount, + ); + } + + return 0; + } + + /** + * Sets the memory reference address. We don't just loadDisassembledInstructions + * for this, since we can't really deal with discontiguous ranges (we can't + * detect _if_ a range is discontiguous since we don't know how much memory + * comes between instructions.) + */ + private async primeMemoryReference(instructionReference: string) { + if (this._referenceToMemoryAddress.has(instructionReference)) { + return true; + } + + const s = await this.debugSession?.disassemble(instructionReference, 0, 0, 1); + if (s && s.length > 0) { + try { + this._referenceToMemoryAddress.set(instructionReference, BigInt(s[0].address)); + return true; + } catch { + return false; + } } return false; } - private async loadDisassembledInstructions(address: string | undefined, instructionOffset: number, instructionCount: number): Promise { - // if address is null, then use current stack frame. - if (!address || address === '-1') { - address = this.focusedInstructionAddress; - } - if (!address) { - return false; - } - - // console.log(`DisassemblyView: loadDisassembledInstructions ${address}, ${instructionOffset}, ${instructionCount}`); + /** Loads disasembled instructions. Returns the number of instructions that were loaded. */ + private async loadDisassembledInstructions(instructionReference: string, offset: number, instructionOffset: number, instructionCount: number): Promise { const session = this.debugSession; - const resultEntries = await session?.disassemble(address, 0, instructionOffset, instructionCount); + const resultEntries = await session?.disassemble(instructionReference, offset, instructionOffset, instructionCount); + + // Ensure we always load the baseline instructions so we know what address the instructionReference refers to. + if (!this._referenceToMemoryAddress.has(instructionReference) && instructionOffset !== 0) { + await this.loadDisassembledInstructions(instructionReference, 0, 0, DisassemblyView.NUM_INSTRUCTIONS_TO_LOAD); + } + if (session && resultEntries && this._disassembledInstructions) { const newEntries: IDisassembledInstructionEntry[] = []; let lastLocation: DebugProtocol.Source | undefined; let lastLine: IRange | undefined; for (let i = 0; i < resultEntries.length; i++) { - const found = this._instructionBpList.find(p => p.instructionReference === resultEntries[i].address); const instruction = resultEntries[i]; + const thisInstructionOffset = instructionOffset + i; // Forward fill the missing location as detailed in the DAP spec. if (instruction.location) { @@ -378,76 +471,152 @@ export class DisassemblyView extends EditorPane { } } - newEntries.push({ allowBreakpoint: true, isBreakpointSet: found !== undefined, isBreakpointEnabled: !!found?.enabled, instruction: instruction }); + let address: bigint; + try { + address = BigInt(instruction.address); + } catch { + console.error(`Could not parse disassembly address ${instruction.address} (in ${JSON.stringify(instruction)})`); + continue; + } + + const entry: IDisassembledInstructionEntry = { + allowBreakpoint: true, + isBreakpointSet: false, + isBreakpointEnabled: false, + instructionReference, + instructionReferenceOffset: offset, + instructionOffset: thisInstructionOffset, + instruction, + address, + }; + + newEntries.push(entry); + + // if we just loaded the first instruction for this reference, mark its address. + if (offset === 0 && thisInstructionOffset === 0) { + this._referenceToMemoryAddress.set(instructionReference, address); + } } - const specialEntriesToRemove = this._disassembledInstructions.length === 1 ? 1 : 0; - - // request is either at the start or end - if (instructionOffset >= 0) { - this._disassembledInstructions.splice(this._disassembledInstructions.length, specialEntriesToRemove, newEntries); - } else { - this._disassembledInstructions.splice(0, specialEntriesToRemove, newEntries); + if (newEntries.length === 0) { + return 0; } - return true; + const refBaseAddress = this._referenceToMemoryAddress.get(instructionReference); + const bps = this._instructionBpList.map(p => { + const base = this._referenceToMemoryAddress.get(p.instructionReference); + if (!base) { + return undefined; + } + return { + enabled: p.enabled, + address: base + BigInt(p.offset || 0), + }; + }); + + if (refBaseAddress !== undefined) { + for (const entry of newEntries) { + const bp = bps.find(p => p?.address === entry.address); + if (bp) { + entry.isBreakpointSet = true; + entry.isBreakpointEnabled = bp.enabled; + } + } + } + + const da = this._disassembledInstructions; + if (da.length === 1 && this._disassembledInstructions.row(0) === disassemblyNotAvailable) { + da.splice(0, 1); + } + + const firstAddr = newEntries[0].address; + const lastAddr = newEntries[newEntries.length - 1].address; + + const startN = binarySearch2(da.length, i => Number(da.row(i).address - firstAddr)); + const start = startN < 0 ? ~startN : startN; + const endN = binarySearch2(da.length, i => Number(da.row(i).address - lastAddr)); + const end = endN < 0 ? ~endN : endN; + const toDelete = end - start; + + // Go through everything we're about to add, and only show the source + // location if it's different from the previous one, "grouping" instructions by line + let lastLocated: undefined | DebugProtocol.DisassembledInstruction; + for (let i = start - 1; i >= 0; i--) { + const { instruction } = da.row(i); + if (instruction.location && instruction.line !== undefined) { + lastLocated = instruction; + break; + } + } + + const shouldShowLocation = (instruction: DebugProtocol.DisassembledInstruction) => + instruction.line !== undefined && instruction.location !== undefined && + (!lastLocated || !sourcesEqual(instruction.location, lastLocated.location) || instruction.line !== lastLocated.line); + + for (const entry of newEntries) { + if (shouldShowLocation(entry.instruction)) { + entry.showSourceLocation = true; + lastLocated = entry.instruction; + } + } + + da.splice(start, toDelete, newEntries); + + return newEntries.length - toDelete; } - return false; + return 0; } - private getIndexFromAddress(instructionAddress: string): number { + private getIndexFromReferenceAndOffset(instructionReference: string, offset: number): number { + const addr = this._referenceToMemoryAddress.get(instructionReference); + if (addr === undefined) { + return -1; + } + + return this.getIndexFromAddress(addr + BigInt(offset)); + } + + private getIndexFromAddress(address: bigint): number { const disassembledInstructions = this._disassembledInstructions; if (disassembledInstructions && disassembledInstructions.length > 0) { - const address = BigInt(instructionAddress); - if (address) { - return binarySearch2(disassembledInstructions.length, index => { - const row = disassembledInstructions.row(index); - - this.ensureAddressParsed(row); - if (row.instructionAddress! > address) { - return 1; - } else if (row.instructionAddress! < address) { - return -1; - } else { - return 0; - } - }); - } + return binarySearch2(disassembledInstructions.length, index => { + const row = disassembledInstructions.row(index); + return Number(row.address - address); + }); } return -1; } - private ensureAddressParsed(entry: IDisassembledInstructionEntry) { - if (entry.instructionAddress !== undefined) { - return; - } else { - entry.instructionAddress = BigInt(entry.instruction.address); - } - } - /** * Clears the table and reload instructions near the target address */ - private reloadDisassembly(targetAddress?: string) { - if (this._disassembledInstructions) { - this._loadingLock = true; // stop scrolling during the load. - this._disassembledInstructions.splice(0, this._disassembledInstructions.length, [disassemblyNotAvailable]); - this._instructionBpList = this._debugService.getModel().getInstructionBreakpoints(); - this.loadDisassembledInstructions(targetAddress, -DisassemblyView.NUM_INSTRUCTIONS_TO_LOAD * 4, DisassemblyView.NUM_INSTRUCTIONS_TO_LOAD * 8).then(() => { - // on load, set the target instruction in the middle of the page. - if (this._disassembledInstructions!.length > 0) { - const targetIndex = Math.floor(this._disassembledInstructions!.length / 2); - this._disassembledInstructions!.reveal(targetIndex, 0.5); - - // Always focus the target address on reload, or arrow key navigation would look terrible - this._disassembledInstructions!.domFocus(); - this._disassembledInstructions!.setFocus([targetIndex]); - } - this._loadingLock = false; - }); + private reloadDisassembly(instructionReference: string, offset: number) { + if (!this._disassembledInstructions) { + return; } + + this._loadingLock = true; // stop scrolling during the load. + this.clear(); + this._instructionBpList = this._debugService.getModel().getInstructionBreakpoints(); + this.loadDisassembledInstructions(instructionReference, offset, -DisassemblyView.NUM_INSTRUCTIONS_TO_LOAD * 4, DisassemblyView.NUM_INSTRUCTIONS_TO_LOAD * 8).then(() => { + // on load, set the target instruction in the middle of the page. + if (this._disassembledInstructions!.length > 0) { + const targetIndex = Math.floor(this._disassembledInstructions!.length / 2); + this._disassembledInstructions!.reveal(targetIndex, 0.5); + + // Always focus the target address on reload, or arrow key navigation would look terrible + this._disassembledInstructions!.domFocus(); + this._disassembledInstructions!.setFocus([targetIndex]); + } + this._loadingLock = false; + }); + } + + private clear() { + this._referenceToMemoryAddress.clear(); + this._disassembledInstructions?.splice(0, this._disassembledInstructions.length, [disassemblyNotAvailable]); } } @@ -504,11 +673,12 @@ class BreakpointRenderer implements ITableRenderer | undefined; + private _languageSupportsDisassembleRequest: IContextKey | undefined; constructor( @IEditorService editorService: IEditorService, @@ -786,7 +956,7 @@ export class DisassemblyViewContribution implements IWorkbenchContribution { @IContextKeyService contextKeyService: IContextKeyService ) { contextKeyService.bufferChangeEvents(() => { - this._languageSupportsDisassemleRequest = CONTEXT_LANGUAGE_SUPPORTS_DISASSEMBLE_REQUEST.bindTo(contextKeyService); + this._languageSupportsDisassembleRequest = CONTEXT_LANGUAGE_SUPPORTS_DISASSEMBLE_REQUEST.bindTo(contextKeyService); }); const onDidActiveEditorChangeListener = () => { @@ -800,13 +970,13 @@ export class DisassemblyViewContribution implements IWorkbenchContribution { const language = activeTextEditorControl.getModel()?.getLanguageId(); // TODO: instead of using idDebuggerInterestedInLanguage, have a specific ext point for languages // support disassembly - this._languageSupportsDisassemleRequest?.set(!!language && debugService.getAdapterManager().someDebuggerInterestedInLanguage(language)); + this._languageSupportsDisassembleRequest?.set(!!language && debugService.getAdapterManager().someDebuggerInterestedInLanguage(language)); this._onDidChangeModelLanguage = activeTextEditorControl.onDidChangeModelLanguage(e => { - this._languageSupportsDisassemleRequest?.set(debugService.getAdapterManager().someDebuggerInterestedInLanguage(e.newLanguage)); + this._languageSupportsDisassembleRequest?.set(debugService.getAdapterManager().someDebuggerInterestedInLanguage(e.newLanguage)); }); } else { - this._languageSupportsDisassemleRequest?.set(false); + this._languageSupportsDisassembleRequest?.set(false); } }; diff --git a/src/vs/workbench/contrib/debug/browser/statusbarColorProvider.ts b/src/vs/workbench/contrib/debug/browser/statusbarColorProvider.ts index eff654458ba..be84ffba9c6 100644 --- a/src/vs/workbench/contrib/debug/browser/statusbarColorProvider.ts +++ b/src/vs/workbench/contrib/debug/browser/statusbarColorProvider.ts @@ -4,14 +4,16 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { registerColor } from 'vs/platform/theme/common/colorRegistry'; +import { ColorTransformType, asCssVariable, asCssVariableName, registerColor } from 'vs/platform/theme/common/colorRegistry'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IDebugService, State, IDebugSession, IDebugConfiguration } from 'vs/workbench/contrib/debug/common/debug'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { STATUS_BAR_FOREGROUND, STATUS_BAR_BORDER } from 'vs/workbench/common/theme'; +import { STATUS_BAR_FOREGROUND, STATUS_BAR_BORDER, COMMAND_CENTER_BACKGROUND } from 'vs/workbench/common/theme'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { IStatusbarService } from 'vs/workbench/services/statusbar/browser/statusbar'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; + // colors for theming @@ -36,6 +38,18 @@ export const STATUS_BAR_DEBUGGING_BORDER = registerColor('statusBar.debuggingBor hcLight: STATUS_BAR_BORDER }, localize('statusBarDebuggingBorder', "Status bar border color separating to the sidebar and editor when a program is being debugged. The status bar is shown in the bottom of the window")); +export const COMMAND_CENTER_DEBUGGING_BACKGROUND = registerColor( + 'commandCenter.debuggingBackground', + { + dark: { value: STATUS_BAR_DEBUGGING_BACKGROUND, op: ColorTransformType.Transparent, factor: 0.258 }, + hcDark: { value: STATUS_BAR_DEBUGGING_BACKGROUND, op: ColorTransformType.Transparent, factor: 0.258 }, + light: { value: STATUS_BAR_DEBUGGING_BACKGROUND, op: ColorTransformType.Transparent, factor: 0.258 }, + hcLight: { value: STATUS_BAR_DEBUGGING_BACKGROUND, op: ColorTransformType.Transparent, factor: 0.258 } + }, + localize('commandCenter-activeBackground', "Command center background color when a program is being debugged"), + true +); + export class StatusBarColorProvider implements IWorkbenchContribution { private readonly disposables = new DisposableStore(); @@ -63,12 +77,13 @@ export class StatusBarColorProvider implements IWorkbenchContribution { @IDebugService private readonly debugService: IDebugService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IStatusbarService private readonly statusbarService: IStatusbarService, + @ILayoutService private readonly layoutService: ILayoutService, @IConfigurationService private readonly configurationService: IConfigurationService ) { this.debugService.onDidChangeState(this.update, this, this.disposables); this.contextService.onDidChangeWorkbenchState(this.update, this, this.disposables); this.configurationService.onDidChangeConfiguration((e) => { - if (e.affectsConfiguration('debug.enableStatusBarColor')) { + if (e.affectsConfiguration('debug.enableStatusBarColor') || e.affectsConfiguration('debug.toolBarLocation')) { this.update(); } }); @@ -76,12 +91,20 @@ export class StatusBarColorProvider implements IWorkbenchContribution { } protected update(): void { - const decorateStatusBar: boolean = this.configurationService.getValue('debug').enableStatusBarColor; - if (!decorateStatusBar) { + const debugConfig = this.configurationService.getValue('debug'); + const isInDebugMode = isStatusbarInDebugMode(this.debugService.state, this.debugService.getModel().getSessions()); + if (!debugConfig.enableStatusBarColor) { this.enabled = false; } else { - this.enabled = isStatusbarInDebugMode(this.debugService.state, this.debugService.getModel().getSessions()); + this.enabled = isInDebugMode; } + + const isInCommandCenter = debugConfig.toolBarLocation === 'commandCenter'; + this.layoutService.container.style.setProperty(asCssVariableName(COMMAND_CENTER_BACKGROUND), isInCommandCenter && isInDebugMode + ? asCssVariable(COMMAND_CENTER_DEBUGGING_BACKGROUND) + : '' + ); + } dispose(): void { diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 8975e581fc7..3620976fcbc 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -581,9 +581,11 @@ export interface IDataBreakpoint extends IBaseBreakpoint { } export interface IInstructionBreakpoint extends IBaseBreakpoint { - // instructionReference is the instruction 'address' from the debugger. readonly instructionReference: string; readonly offset?: number; + /** Original instruction memory address; display purposes only */ + readonly address: bigint; + toJSON(): DebugProtocol.InstructionBreakpoint; } export interface IExceptionInfo { @@ -677,7 +679,7 @@ export interface IDebugConfiguration { openDebug: 'neverOpen' | 'openOnSessionStart' | 'openOnFirstSessionStart' | 'openOnDebugBreak'; openExplorerOnEnd: boolean; inlineValues: boolean | 'auto' | 'on' | 'off'; // boolean for back-compat - toolBarLocation: 'floating' | 'docked' | 'hidden'; + toolBarLocation: 'floating' | 'docked' | 'commandCenter' | 'hidden'; showInStatusBar: 'never' | 'always' | 'onFirstSessionStart'; internalConsoleOptions: 'neverOpen' | 'openOnSessionStart' | 'openOnFirstSessionStart'; extensionHostDebugAdapter: boolean; @@ -1097,14 +1099,14 @@ export interface IDebugService { /** * Adds a new instruction breakpoint. */ - addInstructionBreakpoint(address: string, offset: number, condition?: string, hitCondition?: string): Promise; + addInstructionBreakpoint(instructionReference: string, offset: number, address: bigint, condition?: string, hitCondition?: string): Promise; /** * Removes all instruction breakpoints. If address is passed only removes the instruction breakpoint with the passed address. * The address should be the address string supplied by the debugger from the "Disassemble" request. * Notifies debug adapter of breakpoint changes. */ - removeInstructionBreakpoints(address?: string): Promise; + removeInstructionBreakpoints(instructionReference?: string, offset?: number): Promise; setExceptionBreakpointCondition(breakpoint: IExceptionBreakpoint, condition: string | undefined): Promise; diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index 15030ce7822..588ffece2f3 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -473,8 +473,9 @@ export class StackFrame implements IStackFrame { const threadStopReason = this.thread.stoppedDetails?.reason; if (this.instructionPointerReference && (threadStopReason === 'instruction breakpoint' || - (threadStopReason === 'step' && this.thread.lastSteppingGranularity === 'instruction'))) { - return editorService.openEditor(DisassemblyViewInput.instance, { pinned: true }); + (threadStopReason === 'step' && this.thread.lastSteppingGranularity === 'instruction') || + editorService.activeEditor instanceof DisassemblyViewInput)) { + return editorService.openEditor(DisassemblyViewInput.instance, { pinned: true, revealIfOpened: true }); } if (this.source.available) { @@ -1142,12 +1143,13 @@ export class InstructionBreakpoint extends BaseBreakpoint implements IInstructio hitCondition: string | undefined, condition: string | undefined, logMessage: string | undefined, + public readonly address: bigint, id = generateUuid() ) { super(enabled, hitCondition, condition, logMessage, id); } - override toJSON(): any { + override toJSON(): DebugProtocol.InstructionBreakpoint { const result = super.toJSON(); result.instructionReference = this.instructionReference; result.offset = this.offset; @@ -1677,17 +1679,22 @@ export class DebugModel extends Disposable implements IDebugModel { this._onDidChangeBreakpoints.fire({ removed, sessionOnly: false }); } - addInstructionBreakpoint(address: string, offset: number, condition?: string, hitCondition?: string): void { - const newInstructionBreakpoint = new InstructionBreakpoint(address, offset, false, true, hitCondition, condition, undefined); + addInstructionBreakpoint(instructionReference: string, offset: number, address: bigint, condition?: string, hitCondition?: string): void { + const newInstructionBreakpoint = new InstructionBreakpoint(instructionReference, offset, false, true, hitCondition, condition, undefined, address); this.instructionBreakpoints.push(newInstructionBreakpoint); this._onDidChangeBreakpoints.fire({ added: [newInstructionBreakpoint], sessionOnly: true }); } - removeInstructionBreakpoints(address?: string): void { - let removed: InstructionBreakpoint[]; - if (address) { - removed = this.instructionBreakpoints.filter(fbp => fbp.instructionReference === address); - this.instructionBreakpoints = this.instructionBreakpoints.filter(fbp => fbp.instructionReference !== address); + removeInstructionBreakpoints(instructionReference?: string, offset?: number): void { + let removed: InstructionBreakpoint[] = []; + if (instructionReference) { + for (let i = 0; i < this.instructionBreakpoints.length; i++) { + const ibp = this.instructionBreakpoints[i]; + if (ibp.instructionReference === instructionReference && (offset === undefined || ibp.offset === offset)) { + removed.push(ibp); + this.instructionBreakpoints.splice(i--, 1); + } + } } else { removed = this.instructionBreakpoints; this.instructionBreakpoints = []; diff --git a/src/vs/workbench/contrib/debug/common/debugUtils.ts b/src/vs/workbench/contrib/debug/common/debugUtils.ts index 3597d92b56b..acf1e467817 100644 --- a/src/vs/workbench/contrib/debug/common/debugUtils.ts +++ b/src/vs/workbench/contrib/debug/common/debugUtils.ts @@ -373,3 +373,6 @@ export async function saveAllBeforeDebugStart(configurationService: IConfigurati } await configurationService.reloadConfiguration(); } + +export const sourcesEqual = (a: DebugProtocol.Source | undefined, b: DebugProtocol.Source | undefined): boolean => + !a || !b ? a === b : a.name === b.name && a.path === b.path && a.sourceReference === b.sourceReference; diff --git a/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts index db19f575765..ffe12f1b31c 100644 --- a/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts @@ -296,7 +296,7 @@ suite('Debug - Breakpoints', () => { let eventCount = 0; disposables.add(model.onDidChangeBreakpoints(() => eventCount++)); //address: string, offset: number, condition?: string, hitCondition?: string - model.addInstructionBreakpoint('0xCCCCFFFF', 0); + model.addInstructionBreakpoint('0xCCCCFFFF', 0, 0n); assert.strictEqual(eventCount, 1); let instructionBreakpoints = model.getInstructionBreakpoints(); @@ -304,7 +304,7 @@ suite('Debug - Breakpoints', () => { assert.strictEqual(instructionBreakpoints[0].instructionReference, '0xCCCCFFFF'); assert.strictEqual(instructionBreakpoints[0].offset, 0); - model.addInstructionBreakpoint('0xCCCCEEEE', 1); + model.addInstructionBreakpoint('0xCCCCEEEE', 1, 0n); assert.strictEqual(eventCount, 2); instructionBreakpoints = model.getInstructionBreakpoints(); assert.strictEqual(instructionBreakpoints.length, 2); diff --git a/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts index baade0af6d2..c6901f77f85 100644 --- a/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts @@ -17,7 +17,7 @@ import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector'; import { DebugModel } from 'vs/workbench/contrib/debug/common/debugModel'; import { createTestSession } from 'vs/workbench/contrib/debug/test/browser/callStack.test'; import { createMockDebugModel } from 'vs/workbench/contrib/debug/test/browser/mockDebugModel'; -import { ansiColorMap } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; +import { ansiColorMap, registerColors } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; suite('Debug - ANSI Handling', () => { @@ -45,6 +45,7 @@ suite('Debug - ANSI Handling', () => { } const testTheme = new TestColorTheme(colors); themeService = new TestThemeService(testTheme); + registerColors(); }); teardown(() => { diff --git a/src/vs/workbench/contrib/debug/test/common/debugModel.test.ts b/src/vs/workbench/contrib/debug/test/common/debugModel.test.ts index a39f590b616..96e097d1205 100644 --- a/src/vs/workbench/contrib/debug/test/common/debugModel.test.ts +++ b/src/vs/workbench/contrib/debug/test/common/debugModel.test.ts @@ -41,6 +41,8 @@ suite('DebugModel', () => { const wholeStackDeferred = new DeferredPromise(); const fakeThread = mockObject()({ session: { capabilities: { supportsDelayedStackTraceLoading: true } } as any, + getCallStack: () => [], + getStaleCallStack: () => [], }); fakeThread.fetchCallStack.callsFake((levels: number) => { return levels === 1 ? topFrameDeferred.p : wholeStackDeferred.p; diff --git a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts index 30353a78a35..edd1cdfc1cd 100644 --- a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts +++ b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts @@ -81,7 +81,7 @@ export class MockDebugService implements IDebugService { throw new Error('not implemented'); } - addInstructionBreakpoint(address: string, offset: number, condition?: string, hitCondition?: string): Promise { + addInstructionBreakpoint(instructionReference: string, offset: number, address: bigint, condition?: string, hitCondition?: string): Promise { throw new Error('Method not implemented.'); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts index c816b6686ea..e840bd9b331 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts @@ -9,7 +9,7 @@ import { CancelablePromise, createCancelablePromise, Promises, raceCancellablePr import { CancellationToken } from 'vs/base/common/cancellation'; import { isCancellationError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; -import { DisposableStore, isDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, isDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; @@ -50,12 +50,12 @@ type RecommendationsNotificationActions = { onDidNeverShowRecommendedExtensionsAgain(extensions: IExtension[]): void; }; -class RecommendationsNotification { +class RecommendationsNotification extends Disposable { - private _onDidClose = new Emitter(); + private _onDidClose = this._register(new Emitter()); readonly onDidClose = this._onDidClose.event; - private _onDidChangeVisibility = new Emitter(); + private _onDidChangeVisibility = this._register(new Emitter()); readonly onDidChangeVisibility = this._onDidChangeVisibility.event; private notificationHandle: INotificationHandle | undefined; @@ -66,7 +66,9 @@ class RecommendationsNotification { private readonly message: string, private readonly choices: IPromptChoice[], private readonly notificationService: INotificationService - ) { } + ) { + super(); + } show(): void { if (!this.notificationHandle) { @@ -87,8 +89,8 @@ class RecommendationsNotification { return this.cancelled; } - private onDidCloseDisposable = new MutableDisposable(); - private onDidChangeVisibilityDisposable = new MutableDisposable(); + private onDidCloseDisposable = this._register(new MutableDisposable()); + private onDidChangeVisibilityDisposable = this._register(new MutableDisposable()); private updateNotificationHandle(notificationHandle: INotificationHandle) { this.onDidCloseDisposable.clear(); this.onDidChangeVisibilityDisposable.clear(); @@ -110,7 +112,7 @@ class RecommendationsNotification { type PendingRecommendationsNotification = { recommendationsNotification: RecommendationsNotification; source: RecommendationSource; token: CancellationToken }; type VisibleRecommendationsNotification = { recommendationsNotification: RecommendationsNotification; source: RecommendationSource; from: number }; -export class ExtensionRecommendationNotificationService implements IExtensionRecommendationNotificationService { +export class ExtensionRecommendationNotificationService extends Disposable implements IExtensionRecommendationNotificationService { declare readonly _serviceBrand: undefined; @@ -138,7 +140,9 @@ export class ExtensionRecommendationNotificationService implements IExtensionRec @IExtensionIgnoredRecommendationsService private readonly extensionIgnoredRecommendationsService: IExtensionIgnoredRecommendationsService, @IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService, @IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService, - ) { } + ) { + super(); + } hasToIgnoreRecommendationNotifications(): boolean { const config = this.configurationService.getValue<{ ignoreRecommendations: boolean; showRecommendationsOnlyOnDemand?: boolean }>('extensions'); @@ -255,8 +259,8 @@ export class ExtensionRecommendationNotificationService implements IExtensionRec } return raceCancellablePromises([ - this.showRecommendationsNotification(extensions, message, searchValue, source, recommendationsNotificationActions), - this.waitUntilRecommendationsAreInstalled(extensions) + this._registerP(this.showRecommendationsNotification(extensions, message, searchValue, source, recommendationsNotificationActions)), + this._registerP(this.waitUntilRecommendationsAreInstalled(extensions)) ]); } @@ -344,11 +348,11 @@ export class ExtensionRecommendationNotificationService implements IExtensionRec private async doShowRecommendationsNotification(severity: Severity, message: string, choices: IPromptChoice[], source: RecommendationSource, token: CancellationToken): Promise { const disposables = new DisposableStore(); try { - const recommendationsNotification = new RecommendationsNotification(severity, message, choices, this.notificationService); - Event.once(Event.filter(recommendationsNotification.onDidChangeVisibility, e => !e))(() => this.showNextNotification()); + const recommendationsNotification = disposables.add(new RecommendationsNotification(severity, message, choices, this.notificationService)); + disposables.add(Event.once(Event.filter(recommendationsNotification.onDidChangeVisibility, e => !e))(() => this.showNextNotification())); if (this.visibleNotification) { const index = this.pendingNotificaitons.length; - token.onCancellationRequested(() => this.pendingNotificaitons.splice(index, 1), disposables); + disposables.add(token.onCancellationRequested(() => this.pendingNotificaitons.splice(index, 1))); this.pendingNotificaitons.push({ recommendationsNotification, source, token }); if (source !== RecommendationSource.EXE && source <= this.visibleNotification!.source) { this.hideVisibleNotification(3000); @@ -357,7 +361,7 @@ export class ExtensionRecommendationNotificationService implements IExtensionRec this.visibleNotification = { recommendationsNotification, source, from: Date.now() }; recommendationsNotification.show(); } - await raceCancellation(Event.toPromise(recommendationsNotification.onDidClose), token); + await raceCancellation(new Promise(c => disposables.add(Event.once(recommendationsNotification.onDidClose)(c))), token); return !recommendationsNotification.isCancelled(); } finally { disposables.dispose(); @@ -442,4 +446,9 @@ export class ExtensionRecommendationNotificationService implements IExtensionRec private setIgnoreRecommendationsConfig(configVal: boolean) { this.configurationService.updateValue('extensions.ignoreRecommendations', configVal); } + + private _registerP(o: CancelablePromise): CancelablePromise { + this._register(toDisposable(() => o.cancel())); + return o; + } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts index 3dd31a217ff..a6806b20159 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { IExtensionManagementService, IExtensionGalleryService, InstallOperation, InstallExtensionResult } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IExtensionRecommendationsService, ExtensionRecommendationReason, IExtensionIgnoredRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -20,7 +20,7 @@ import { LanguageRecommendations } from 'vs/workbench/contrib/extensions/browser import { ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; import { ConfigBasedRecommendations } from 'vs/workbench/contrib/extensions/browser/configBasedRecommendations'; import { IExtensionRecommendationNotificationService } from 'vs/platform/extensionRecommendations/common/extensionRecommendations'; -import { timeout } from 'vs/base/common/async'; +import { CancelablePromise, timeout } from 'vs/base/common/async'; import { URI } from 'vs/base/common/uri'; import { WebRecommendations } from 'vs/workbench/contrib/extensions/browser/webRecommendations'; import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; @@ -71,14 +71,14 @@ export class ExtensionRecommendationsService extends Disposable implements IExte ) { super(); - this.workspaceRecommendations = instantiationService.createInstance(WorkspaceRecommendations); - this.fileBasedRecommendations = instantiationService.createInstance(FileBasedRecommendations); - this.configBasedRecommendations = instantiationService.createInstance(ConfigBasedRecommendations); - this.exeBasedRecommendations = instantiationService.createInstance(ExeBasedRecommendations); - this.keymapRecommendations = instantiationService.createInstance(KeymapRecommendations); - this.webRecommendations = instantiationService.createInstance(WebRecommendations); - this.languageRecommendations = instantiationService.createInstance(LanguageRecommendations); - this.remoteRecommendations = instantiationService.createInstance(RemoteRecommendations); + this.workspaceRecommendations = this._register(instantiationService.createInstance(WorkspaceRecommendations)); + this.fileBasedRecommendations = this._register(instantiationService.createInstance(FileBasedRecommendations)); + this.configBasedRecommendations = this._register(instantiationService.createInstance(ConfigBasedRecommendations)); + this.exeBasedRecommendations = this._register(instantiationService.createInstance(ExeBasedRecommendations)); + this.keymapRecommendations = this._register(instantiationService.createInstance(KeymapRecommendations)); + this.webRecommendations = this._register(instantiationService.createInstance(WebRecommendations)); + this.languageRecommendations = this._register(instantiationService.createInstance(LanguageRecommendations)); + this.remoteRecommendations = this._register(instantiationService.createInstance(RemoteRecommendations)); if (!this.isEnabled()) { this.sessionSeed = 0; @@ -276,8 +276,13 @@ export class ExtensionRecommendationsService extends Disposable implements IExte .filter(extensionId => this.isExtensionAllowedToBeRecommended(extensionId)); if (allowedRecommendations.length) { - await timeout(5000); + await this._registerP(timeout(5000)); await this.extensionRecommendationNotificationService.promptWorkspaceRecommendations(allowedRecommendations); } } + + private _registerP(o: CancelablePromise): CancelablePromise { + this._register(toDisposable(() => o.cancel())); + return o; + } } diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts index d19b0fedbdd..c644f0acb11 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts @@ -61,6 +61,9 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { platform } from 'vs/base/common/platform'; import { arch } from 'vs/base/common/process'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { timeout } from 'vs/base/common/async'; const mockExtensionGallery: IGalleryExtension[] = [ aGalleryExtension('MockExtension1', { @@ -180,6 +183,7 @@ function aGalleryExtension(name: string, properties: any = {}, galleryExtensionP } suite('ExtensionRecommendationsService Test', () => { + let disposableStore: DisposableStore; let workspaceService: IWorkspaceContextService; let instantiationService: TestInstantiationService; let testConfigurationService: TestConfigurationService; @@ -189,18 +193,27 @@ suite('ExtensionRecommendationsService Test', () => { uninstallEvent: Emitter, didUninstallEvent: Emitter; let prompted: boolean; - const promptedEmitter = new Emitter(); + let promptedEmitter: Emitter; let onModelAddedEvent: Emitter; - suiteSetup(() => { - instantiationService = new TestInstantiationService(); - installEvent = new Emitter(); - didInstallEvent = new Emitter(); - uninstallEvent = new Emitter(); - didUninstallEvent = new Emitter(); + teardown(async () => { + disposableStore.dispose(); + await timeout(0); // allow for async disposables to complete + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + setup(() => { + disposableStore = new DisposableStore(); + instantiationService = disposableStore.add(new TestInstantiationService()); + promptedEmitter = disposableStore.add(new Emitter()); + installEvent = disposableStore.add(new Emitter()); + didInstallEvent = disposableStore.add(new Emitter()); + uninstallEvent = disposableStore.add(new Emitter()); + didUninstallEvent = disposableStore.add(new Emitter()); instantiationService.stub(IExtensionGalleryService, ExtensionGalleryService); instantiationService.stub(ISharedProcessService, TestSharedProcessService); - instantiationService.stub(ILifecycleService, new TestLifecycleService()); + instantiationService.stub(ILifecycleService, disposableStore.add(new TestLifecycleService())); testConfigurationService = new TestConfigurationService(); instantiationService.stub(IConfigurationService, testConfigurationService); instantiationService.stub(INotificationService, new TestNotificationService()); @@ -222,11 +235,11 @@ suite('ExtensionRecommendationsService Test', () => { extensions: [], async whenInstalledExtensionsRegistered() { return true; } }); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposableStore.add(new TestExtensionEnablementService(instantiationService))); instantiationService.stub(ITelemetryService, NullTelemetryService); instantiationService.stub(IURLService, NativeURLService); instantiationService.stub(IWorkspaceTagsService, new NoOpWorkspaceTagsService()); - instantiationService.stub(IStorageService, new TestStorageService()); + instantiationService.stub(IStorageService, disposableStore.add(new TestStorageService())); instantiationService.stub(ILogService, new NullLogService()); instantiationService.stub(IProductService, >{ extensionTips: { @@ -275,13 +288,11 @@ suite('ExtensionRecommendationsService Test', () => { }, }); - instantiationService.set(IExtensionsWorkbenchService, instantiationService.createInstance(ExtensionsWorkbenchService)); - instantiationService.stub(IExtensionTipsService, instantiationService.createInstance(TestExtensionTipsService)); + instantiationService.set(IExtensionsWorkbenchService, disposableStore.add(instantiationService.createInstance(ExtensionsWorkbenchService))); + instantiationService.stub(IExtensionTipsService, disposableStore.add(instantiationService.createInstance(TestExtensionTipsService))); onModelAddedEvent = new Emitter(); - }); - setup(() => { instantiationService.stub(IEnvironmentService, >{}); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', []); instantiationService.stub(IExtensionGalleryService, 'isEnabled', true); @@ -307,12 +318,6 @@ suite('ExtensionRecommendationsService Test', () => { }); }); - teardown(() => (testObject).dispose()); - - suiteTeardown(() => { - instantiationService.dispose(); - }); - function setUpFolderWorkspace(folderName: string, recommendedExtensions: string[], ignoredRecommendations: string[] = []): Promise { return setUpFolder(folderName, recommendedExtensions, ignoredRecommendations); } @@ -320,9 +325,9 @@ suite('ExtensionRecommendationsService Test', () => { async function setUpFolder(folderName: string, recommendedExtensions: string[], ignoredRecommendations: string[] = []): Promise { const ROOT = URI.file('tests').with({ scheme: 'vscode-tests' }); const logService = new NullLogService(); - const fileService = new FileService(logService); - const fileSystemProvider = new InMemoryFileSystemProvider(); - fileService.registerProvider(ROOT.scheme, fileSystemProvider); + const fileService = disposableStore.add(new FileService(logService)); + const fileSystemProvider = disposableStore.add(new InMemoryFileSystemProvider()); + disposableStore.add(fileService.registerProvider(ROOT.scheme, fileSystemProvider)); const folderDir = joinPath(ROOT, folderName); const workspaceSettingsDir = joinPath(folderDir, '.vscode'); @@ -338,14 +343,14 @@ suite('ExtensionRecommendationsService Test', () => { instantiationService.stub(IFileService, fileService); workspaceService = new TestContextService(myWorkspace); instantiationService.stub(IWorkspaceContextService, workspaceService); - instantiationService.stub(IWorkspaceExtensionsConfigService, instantiationService.createInstance(WorkspaceExtensionsConfigService)); - instantiationService.stub(IExtensionIgnoredRecommendationsService, instantiationService.createInstance(ExtensionIgnoredRecommendationsService)); - instantiationService.stub(IExtensionRecommendationNotificationService, instantiationService.createInstance(ExtensionRecommendationNotificationService)); + instantiationService.stub(IWorkspaceExtensionsConfigService, disposableStore.add(instantiationService.createInstance(WorkspaceExtensionsConfigService))); + instantiationService.stub(IExtensionIgnoredRecommendationsService, disposableStore.add(instantiationService.createInstance(ExtensionIgnoredRecommendationsService))); + instantiationService.stub(IExtensionRecommendationNotificationService, disposableStore.add(instantiationService.createInstance(ExtensionRecommendationNotificationService))); } function testNoPromptForValidRecommendations(recommendations: string[]) { return setUpFolderWorkspace('myFolder', recommendations).then(() => { - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = disposableStore.add(instantiationService.createInstance(ExtensionRecommendationsService)); return testObject.activationPromise.then(() => { assert.strictEqual(Object.keys(testObject.getAllRecommendationsWithReason()).length, recommendations.length); assert.ok(!prompted); @@ -355,7 +360,7 @@ suite('ExtensionRecommendationsService Test', () => { function testNoPromptOrRecommendationsForValidRecommendations(recommendations: string[]) { return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => { - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = disposableStore.add(instantiationService.createInstance(ExtensionRecommendationsService)); assert.ok(!prompted); return testObject.getWorkspaceRecommendations().then(() => { @@ -384,7 +389,7 @@ suite('ExtensionRecommendationsService Test', () => { test('ExtensionRecommendationsService: Prompt for valid workspace recommendations', () => runWithFakedTimers({ useFakeTimers: true }, async () => { await setUpFolderWorkspace('myFolder', mockTestData.recommendedExtensions); - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = disposableStore.add(instantiationService.createInstance(ExtensionRecommendationsService)); await Event.toPromise(promptedEmitter.event); const recommendations = Object.keys(testObject.getAllRecommendationsWithReason()); @@ -413,7 +418,7 @@ suite('ExtensionRecommendationsService Test', () => { test('ExtensionRecommendationsService: No Prompt for valid workspace recommendations if showRecommendationsOnlyOnDemand is set', () => runWithFakedTimers({ useFakeTimers: true }, async () => { testConfigurationService.setUserConfiguration(ConfigurationKey, { showRecommendationsOnlyOnDemand: true }); return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => { - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = disposableStore.add(instantiationService.createInstance(ExtensionRecommendationsService)); return testObject.activationPromise.then(() => { assert.ok(!prompted); }); @@ -431,7 +436,7 @@ suite('ExtensionRecommendationsService Test', () => { instantiationService.get(IStorageService).store('extensionsAssistant/ignored_recommendations', '["ms-dotnettools.csharp", "mockpublisher2.mockextension2"]', StorageScope.PROFILE, StorageTarget.MACHINE); return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => { - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = disposableStore.add(instantiationService.createInstance(ExtensionRecommendationsService)); return testObject.activationPromise.then(() => { const recommendations = testObject.getAllRecommendationsWithReason(); assert.ok(!recommendations['ms-dotnettools.csharp']); // stored recommendation that has been globally ignored @@ -449,7 +454,7 @@ suite('ExtensionRecommendationsService Test', () => { instantiationService.get(IStorageService).store('extensionsAssistant/recommendations', storedRecommendations, StorageScope.PROFILE, StorageTarget.MACHINE); return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions, ignoredRecommendations).then(() => { - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = disposableStore.add(instantiationService.createInstance(ExtensionRecommendationsService)); return testObject.activationPromise.then(() => { const recommendations = testObject.getAllRecommendationsWithReason(); assert.ok(!recommendations['ms-dotnettools.csharp']); // stored recommendation that has been workspace ignored @@ -471,7 +476,7 @@ suite('ExtensionRecommendationsService Test', () => { storageService.store('extensionsAssistant/ignored_recommendations', globallyIgnoredRecommendations, StorageScope.PROFILE, StorageTarget.MACHINE); await setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions, workspaceIgnoredRecommendations); - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = disposableStore.add(instantiationService.createInstance(ExtensionRecommendationsService)); await testObject.activationPromise; const recommendations = testObject.getAllRecommendationsWithReason(); @@ -489,7 +494,7 @@ suite('ExtensionRecommendationsService Test', () => { await setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions); const extensionIgnoredRecommendationsService = instantiationService.get(IExtensionIgnoredRecommendationsService); - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = disposableStore.add(instantiationService.createInstance(ExtensionRecommendationsService)); await testObject.activationPromise; let recommendations = testObject.getAllRecommendationsWithReason(); @@ -521,9 +526,9 @@ suite('ExtensionRecommendationsService Test', () => { storageService.store('extensionsAssistant/ignored_recommendations', '["ms-vscode.vscode"]', StorageScope.PROFILE, StorageTarget.MACHINE); await setUpFolderWorkspace('myFolder', []); - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = disposableStore.add(instantiationService.createInstance(ExtensionRecommendationsService)); const extensionIgnoredRecommendationsService = instantiationService.get(IExtensionIgnoredRecommendationsService); - extensionIgnoredRecommendationsService.onDidChangeGlobalIgnoredRecommendation(changeHandlerTarget); + disposableStore.add(extensionIgnoredRecommendationsService.onDidChangeGlobalIgnoredRecommendation(changeHandlerTarget)); extensionIgnoredRecommendationsService.toggleGlobalIgnoredRecommendation(ignoredExtensionId, true); await testObject.activationPromise; @@ -536,7 +541,7 @@ suite('ExtensionRecommendationsService Test', () => { instantiationService.get(IStorageService).store('extensionsAssistant/recommendations', storedRecommendations, StorageScope.PROFILE, StorageTarget.MACHINE); return setUpFolderWorkspace('myFolder', []).then(() => { - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = disposableStore.add(instantiationService.createInstance(ExtensionRecommendationsService)); return testObject.activationPromise.then(() => { const recommendations = testObject.getFileBasedRecommendations(); assert.strictEqual(recommendations.length, 2); @@ -555,7 +560,7 @@ suite('ExtensionRecommendationsService Test', () => { instantiationService.get(IStorageService).store('extensionsAssistant/recommendations', storedRecommendations, StorageScope.PROFILE, StorageTarget.MACHINE); await setUpFolderWorkspace('myFolder', []); - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = disposableStore.add(instantiationService.createInstance(ExtensionRecommendationsService)); await testObject.activationPromise; const recommendations = testObject.getFileBasedRecommendations(); diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts index 6a1743a35f9..c811b48b7bf 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts @@ -25,7 +25,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { IExtensionService, toExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; +import { TestContextService, TestWorkspaceTrustManagementService } from 'vs/workbench/test/common/workbenchTestServices'; import { TestExtensionTipsService, TestSharedProcessService } from 'vs/workbench/test/electron-sandbox/workbenchTestServices'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService, NullLogService } from 'vs/platform/log/common/log'; @@ -52,10 +52,10 @@ import { UserDataSyncEnablementService } from 'vs/platform/userDataSync/common/u import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; -import { TestWorkspaceTrustManagementService } from 'vs/workbench/services/workspaces/test/common/testWorkspaceTrustService'; import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment'; import { platform } from 'vs/base/common/platform'; import { arch } from 'vs/base/common/process'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; let instantiationService: TestInstantiationService; let installEvent: Emitter, @@ -63,14 +63,11 @@ let installEvent: Emitter, uninstallEvent: Emitter, didUninstallEvent: Emitter; -let disposables: DisposableStore; - -function setupTest() { - disposables = new DisposableStore(); - installEvent = new Emitter(); - didInstallEvent = new Emitter(); - uninstallEvent = new Emitter(); - didUninstallEvent = new Emitter(); +function setupTest(disposables: Pick) { + installEvent = disposables.add(new Emitter()); + didInstallEvent = disposables.add(new Emitter()); + uninstallEvent = disposables.add(new Emitter()); + didUninstallEvent = disposables.add(new Emitter()); instantiationService = disposables.add(new TestInstantiationService()); @@ -123,11 +120,11 @@ function setupTest() { } }); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - instantiationService.stub(ILabelService, { onDidChangeFormatters: new Emitter().event }); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); + instantiationService.stub(ILabelService, { onDidChangeFormatters: disposables.add(new Emitter()).event }); - instantiationService.stub(ILifecycleService, new TestLifecycleService()); - instantiationService.stub(IExtensionTipsService, instantiationService.createInstance(TestExtensionTipsService)); + instantiationService.stub(ILifecycleService, disposables.add(new TestLifecycleService())); + instantiationService.stub(IExtensionTipsService, disposables.add(instantiationService.createInstance(TestExtensionTipsService))); instantiationService.stub(IExtensionRecommendationsService, {}); instantiationService.stub(IURLService, NativeURLService); @@ -137,28 +134,28 @@ function setupTest() { instantiationService.stub(IExtensionService, >{ extensions: [], onDidChangeExtensions: Event.None, canAddExtension: (extension: IExtensionDescription) => false, canRemoveExtension: (extension: IExtensionDescription) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); (instantiationService.get(IWorkbenchExtensionEnablementService)).reset(); - instantiationService.stub(IUserDataSyncEnablementService, instantiationService.createInstance(UserDataSyncEnablementService)); + instantiationService.stub(IUserDataSyncEnablementService, disposables.add(instantiationService.createInstance(UserDataSyncEnablementService))); instantiationService.set(IExtensionsWorkbenchService, disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService))); - instantiationService.stub(IWorkspaceTrustManagementService, new TestWorkspaceTrustManagementService()); + instantiationService.stub(IWorkspaceTrustManagementService, disposables.add(new TestWorkspaceTrustManagementService())); } suite('ExtensionsActions', () => { - setup(setupTest); - teardown(() => disposables.dispose()); + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + setup(() => setupTest(disposables)); test('Install action is disabled when there is no extension', () => { - const testObject: ExtensionsActions.InstallAction = instantiationService.createInstance(ExtensionsActions.InstallAction, { installPreReleaseVersion: false }); + const testObject: ExtensionsActions.InstallAction = disposables.add(instantiationService.createInstance(ExtensionsActions.InstallAction, { installPreReleaseVersion: false })); assert.ok(!testObject.enabled); }); test('Test Install action when state is installed', () => { const workbenchService = instantiationService.get(IExtensionsWorkbenchService); - const testObject: ExtensionsActions.InstallAction = instantiationService.createInstance(ExtensionsActions.InstallAction, { installPreReleaseVersion: false }); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.InstallAction = disposables.add(instantiationService.createInstance(ExtensionsActions.InstallAction, { installPreReleaseVersion: false })); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const local = aLocalExtension('a'); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); return workbenchService.queryLocal() @@ -176,8 +173,8 @@ suite('ExtensionsActions', () => { test('Test InstallingLabelAction when state is installing', () => { const workbenchService = instantiationService.get(IExtensionsWorkbenchService); - const testObject: ExtensionsActions.InstallAction = instantiationService.createInstance(ExtensionsActions.InstallingLabelAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.InstallAction = disposables.add(instantiationService.createInstance(ExtensionsActions.InstallingLabelAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const gallery = aGalleryExtension('a'); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); return workbenchService.queryGallery(CancellationToken.None) @@ -193,8 +190,8 @@ suite('ExtensionsActions', () => { test('Test Install action when state is uninstalled', async () => { const workbenchService = instantiationService.get(IExtensionsWorkbenchService); - const testObject: ExtensionsActions.InstallAction = instantiationService.createInstance(ExtensionsActions.InstallAction, { installPreReleaseVersion: false }); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.InstallAction = disposables.add(instantiationService.createInstance(ExtensionsActions.InstallAction, { installPreReleaseVersion: false })); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const gallery = aGalleryExtension('a'); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); const paged = await workbenchService.queryGallery(CancellationToken.None); @@ -206,8 +203,8 @@ suite('ExtensionsActions', () => { }); test('Test Install action when extension is system action', () => { - const testObject: ExtensionsActions.InstallAction = instantiationService.createInstance(ExtensionsActions.InstallAction, { installPreReleaseVersion: false }); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.InstallAction = disposables.add(instantiationService.createInstance(ExtensionsActions.InstallAction, { installPreReleaseVersion: false })); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const local = aLocalExtension('a', {}, { type: ExtensionType.System }); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -221,8 +218,8 @@ suite('ExtensionsActions', () => { }); test('Test Install action when extension doesnot has gallery', () => { - const testObject: ExtensionsActions.InstallAction = instantiationService.createInstance(ExtensionsActions.InstallAction, { installPreReleaseVersion: false }); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.InstallAction = disposables.add(instantiationService.createInstance(ExtensionsActions.InstallAction, { installPreReleaseVersion: false })); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const local = aLocalExtension('a'); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -236,15 +233,15 @@ suite('ExtensionsActions', () => { }); test('Uninstall action is disabled when there is no extension', () => { - const testObject: ExtensionsActions.UninstallAction = instantiationService.createInstance(ExtensionsActions.UninstallAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.UninstallAction = disposables.add(instantiationService.createInstance(ExtensionsActions.UninstallAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); assert.ok(!testObject.enabled); }); test('Test Uninstall action when state is uninstalling', () => { - const testObject: ExtensionsActions.UninstallAction = instantiationService.createInstance(ExtensionsActions.UninstallAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.UninstallAction = disposables.add(instantiationService.createInstance(ExtensionsActions.UninstallAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const local = aLocalExtension('a'); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -259,8 +256,8 @@ suite('ExtensionsActions', () => { }); test('Test Uninstall action when state is installed and is user extension', () => { - const testObject: ExtensionsActions.UninstallAction = instantiationService.createInstance(ExtensionsActions.UninstallAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.UninstallAction = disposables.add(instantiationService.createInstance(ExtensionsActions.UninstallAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const local = aLocalExtension('a'); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -274,8 +271,8 @@ suite('ExtensionsActions', () => { }); test('Test Uninstall action when state is installed and is system extension', () => { - const testObject: ExtensionsActions.UninstallAction = instantiationService.createInstance(ExtensionsActions.UninstallAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.UninstallAction = disposables.add(instantiationService.createInstance(ExtensionsActions.UninstallAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const local = aLocalExtension('a', {}, { type: ExtensionType.System }); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -289,8 +286,8 @@ suite('ExtensionsActions', () => { }); test('Test Uninstall action when state is installing and is user extension', () => { - const testObject: ExtensionsActions.UninstallAction = instantiationService.createInstance(ExtensionsActions.UninstallAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.UninstallAction = disposables.add(instantiationService.createInstance(ExtensionsActions.UninstallAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const local = aLocalExtension('a'); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -306,8 +303,8 @@ suite('ExtensionsActions', () => { }); test('Test Uninstall action after extension is installed', async () => { - const testObject: ExtensionsActions.UninstallAction = instantiationService.createInstance(ExtensionsActions.UninstallAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.UninstallAction = disposables.add(instantiationService.createInstance(ExtensionsActions.UninstallAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const gallery = aGalleryExtension('a'); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -325,15 +322,15 @@ suite('ExtensionsActions', () => { }); test('Test UpdateAction when there is no extension', () => { - const testObject: ExtensionsActions.UpdateAction = instantiationService.createInstance(ExtensionsActions.UpdateAction, false); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.UpdateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.UpdateAction, false)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); assert.ok(!testObject.enabled); }); test('Test UpdateAction when extension is uninstalled', () => { - const testObject: ExtensionsActions.UpdateAction = instantiationService.createInstance(ExtensionsActions.UpdateAction, false); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.UpdateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.UpdateAction, false)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const gallery = aGalleryExtension('a', { version: '1.0.0' }); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); return instantiationService.get(IExtensionsWorkbenchService).queryGallery(CancellationToken.None) @@ -344,8 +341,8 @@ suite('ExtensionsActions', () => { }); test('Test UpdateAction when extension is installed and not outdated', () => { - const testObject: ExtensionsActions.UpdateAction = instantiationService.createInstance(ExtensionsActions.UpdateAction, false); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.UpdateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.UpdateAction, false)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const local = aLocalExtension('a', { version: '1.0.0' }); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -359,8 +356,8 @@ suite('ExtensionsActions', () => { }); test('Test UpdateAction when extension is installed outdated and system extension', () => { - const testObject: ExtensionsActions.UpdateAction = instantiationService.createInstance(ExtensionsActions.UpdateAction, false); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.UpdateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.UpdateAction, false)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const local = aLocalExtension('a', { version: '1.0.0' }, { type: ExtensionType.System }); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -374,8 +371,8 @@ suite('ExtensionsActions', () => { }); test('Test UpdateAction when extension is installed outdated and user extension', () => { - const testObject: ExtensionsActions.UpdateAction = instantiationService.createInstance(ExtensionsActions.UpdateAction, false); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.UpdateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.UpdateAction, false)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const local = aLocalExtension('a', { version: '1.0.0' }); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -389,19 +386,19 @@ suite('ExtensionsActions', () => { instantiationService.stubPromise(IExtensionGalleryService, 'getExtensions', [gallery]); assert.ok(!testObject.enabled); return new Promise(c => { - testObject.onDidChange(() => { + disposables.add(testObject.onDidChange(() => { if (testObject.enabled) { c(); } - }); + })); instantiationService.get(IExtensionsWorkbenchService).queryGallery(CancellationToken.None); }); }); }); test('Test UpdateAction when extension is installing and outdated and user extension', async () => { - const testObject: ExtensionsActions.UpdateAction = instantiationService.createInstance(ExtensionsActions.UpdateAction, false); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.UpdateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.UpdateAction, false)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const local = aLocalExtension('a', { version: '1.0.0' }); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -412,33 +409,33 @@ suite('ExtensionsActions', () => { instantiationService.stubPromise(IExtensionGalleryService, 'getCompatibleExtension', gallery); instantiationService.stubPromise(IExtensionGalleryService, 'getExtensions', [gallery]); await new Promise(c => { - testObject.onDidChange(() => { + disposables.add(testObject.onDidChange(() => { if (testObject.enabled) { c(); } - }); + })); instantiationService.get(IExtensionsWorkbenchService).queryGallery(CancellationToken.None); }); await new Promise(c => { - testObject.onDidChange(() => { + disposables.add(testObject.onDidChange(() => { if (!testObject.enabled) { c(); } - }); + })); installEvent.fire({ identifier: local.identifier, source: gallery }); }); }); test('Test ManageExtensionAction when there is no extension', () => { - const testObject: ExtensionsActions.ManageExtensionAction = instantiationService.createInstance(ExtensionsActions.ManageExtensionAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.ManageExtensionAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ManageExtensionAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); assert.ok(!testObject.enabled); }); test('Test ManageExtensionAction when extension is installed', () => { - const testObject: ExtensionsActions.ManageExtensionAction = instantiationService.createInstance(ExtensionsActions.ManageExtensionAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.ManageExtensionAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ManageExtensionAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const local = aLocalExtension('a'); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -452,8 +449,8 @@ suite('ExtensionsActions', () => { }); test('Test ManageExtensionAction when extension is uninstalled', () => { - const testObject: ExtensionsActions.ManageExtensionAction = instantiationService.createInstance(ExtensionsActions.ManageExtensionAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.ManageExtensionAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ManageExtensionAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const gallery = aGalleryExtension('a'); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -467,8 +464,8 @@ suite('ExtensionsActions', () => { }); test('Test ManageExtensionAction when extension is installing', () => { - const testObject: ExtensionsActions.ManageExtensionAction = instantiationService.createInstance(ExtensionsActions.ManageExtensionAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.ManageExtensionAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ManageExtensionAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const gallery = aGalleryExtension('a'); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -484,8 +481,8 @@ suite('ExtensionsActions', () => { }); test('Test ManageExtensionAction when extension is queried from gallery and installed', async () => { - const testObject: ExtensionsActions.ManageExtensionAction = instantiationService.createInstance(ExtensionsActions.ManageExtensionAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.ManageExtensionAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ManageExtensionAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const gallery = aGalleryExtension('a'); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -502,8 +499,8 @@ suite('ExtensionsActions', () => { }); test('Test ManageExtensionAction when extension is system extension', () => { - const testObject: ExtensionsActions.ManageExtensionAction = instantiationService.createInstance(ExtensionsActions.ManageExtensionAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.ManageExtensionAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ManageExtensionAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const local = aLocalExtension('a', {}, { type: ExtensionType.System }); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -517,8 +514,8 @@ suite('ExtensionsActions', () => { }); test('Test ManageExtensionAction when extension is uninstalling', () => { - const testObject: ExtensionsActions.ManageExtensionAction = instantiationService.createInstance(ExtensionsActions.ManageExtensionAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.ManageExtensionAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ManageExtensionAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const local = aLocalExtension('a'); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -534,7 +531,7 @@ suite('ExtensionsActions', () => { }); test('Test EnableForWorkspaceAction when there is no extension', () => { - const testObject: ExtensionsActions.EnableForWorkspaceAction = instantiationService.createInstance(ExtensionsActions.EnableForWorkspaceAction); + const testObject: ExtensionsActions.EnableForWorkspaceAction = disposables.add(instantiationService.createInstance(ExtensionsActions.EnableForWorkspaceAction)); assert.ok(!testObject.enabled); }); @@ -545,7 +542,7 @@ suite('ExtensionsActions', () => { return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { - const testObject: ExtensionsActions.EnableForWorkspaceAction = instantiationService.createInstance(ExtensionsActions.EnableForWorkspaceAction); + const testObject: ExtensionsActions.EnableForWorkspaceAction = disposables.add(instantiationService.createInstance(ExtensionsActions.EnableForWorkspaceAction)); testObject.extension = extensions[0]; assert.ok(!testObject.enabled); }); @@ -559,7 +556,7 @@ suite('ExtensionsActions', () => { return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { - const testObject: ExtensionsActions.EnableForWorkspaceAction = instantiationService.createInstance(ExtensionsActions.EnableForWorkspaceAction); + const testObject: ExtensionsActions.EnableForWorkspaceAction = disposables.add(instantiationService.createInstance(ExtensionsActions.EnableForWorkspaceAction)); testObject.extension = extensions[0]; assert.ok(testObject.enabled); }); @@ -574,7 +571,7 @@ suite('ExtensionsActions', () => { return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { - const testObject: ExtensionsActions.EnableForWorkspaceAction = instantiationService.createInstance(ExtensionsActions.EnableForWorkspaceAction); + const testObject: ExtensionsActions.EnableForWorkspaceAction = disposables.add(instantiationService.createInstance(ExtensionsActions.EnableForWorkspaceAction)); testObject.extension = extensions[0]; assert.ok(testObject.enabled); }); @@ -590,7 +587,7 @@ suite('ExtensionsActions', () => { return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { - const testObject: ExtensionsActions.EnableForWorkspaceAction = instantiationService.createInstance(ExtensionsActions.EnableForWorkspaceAction); + const testObject: ExtensionsActions.EnableForWorkspaceAction = disposables.add(instantiationService.createInstance(ExtensionsActions.EnableForWorkspaceAction)); testObject.extension = extensions[0]; assert.ok(testObject.enabled); }); @@ -598,7 +595,7 @@ suite('ExtensionsActions', () => { }); test('Test EnableGloballyAction when there is no extension', () => { - const testObject: ExtensionsActions.EnableGloballyAction = instantiationService.createInstance(ExtensionsActions.EnableGloballyAction); + const testObject: ExtensionsActions.EnableGloballyAction = disposables.add(instantiationService.createInstance(ExtensionsActions.EnableGloballyAction)); assert.ok(!testObject.enabled); }); @@ -609,7 +606,7 @@ suite('ExtensionsActions', () => { return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { - const testObject: ExtensionsActions.EnableGloballyAction = instantiationService.createInstance(ExtensionsActions.EnableGloballyAction); + const testObject: ExtensionsActions.EnableGloballyAction = disposables.add(instantiationService.createInstance(ExtensionsActions.EnableGloballyAction)); testObject.extension = extensions[0]; assert.ok(!testObject.enabled); }); @@ -623,7 +620,7 @@ suite('ExtensionsActions', () => { return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { - const testObject: ExtensionsActions.EnableGloballyAction = instantiationService.createInstance(ExtensionsActions.EnableGloballyAction); + const testObject: ExtensionsActions.EnableGloballyAction = disposables.add(instantiationService.createInstance(ExtensionsActions.EnableGloballyAction)); testObject.extension = extensions[0]; assert.ok(!testObject.enabled); }); @@ -638,7 +635,7 @@ suite('ExtensionsActions', () => { return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { - const testObject: ExtensionsActions.EnableGloballyAction = instantiationService.createInstance(ExtensionsActions.EnableGloballyAction); + const testObject: ExtensionsActions.EnableGloballyAction = disposables.add(instantiationService.createInstance(ExtensionsActions.EnableGloballyAction)); testObject.extension = extensions[0]; assert.ok(testObject.enabled); }); @@ -654,7 +651,7 @@ suite('ExtensionsActions', () => { return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { - const testObject: ExtensionsActions.EnableGloballyAction = instantiationService.createInstance(ExtensionsActions.EnableGloballyAction); + const testObject: ExtensionsActions.EnableGloballyAction = disposables.add(instantiationService.createInstance(ExtensionsActions.EnableGloballyAction)); testObject.extension = extensions[0]; assert.ok(testObject.enabled); }); @@ -662,7 +659,7 @@ suite('ExtensionsActions', () => { }); test('Test EnableAction when there is no extension', () => { - const testObject: ExtensionsActions.EnableDropDownAction = instantiationService.createInstance(ExtensionsActions.EnableDropDownAction); + const testObject: ExtensionsActions.EnableDropDownAction = disposables.add(instantiationService.createInstance(ExtensionsActions.EnableDropDownAction)); assert.ok(!testObject.enabled); }); @@ -673,7 +670,7 @@ suite('ExtensionsActions', () => { return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { - const testObject: ExtensionsActions.EnableDropDownAction = instantiationService.createInstance(ExtensionsActions.EnableDropDownAction); + const testObject: ExtensionsActions.EnableDropDownAction = disposables.add(instantiationService.createInstance(ExtensionsActions.EnableDropDownAction)); testObject.extension = extensions[0]; assert.ok(!testObject.enabled); }); @@ -687,7 +684,7 @@ suite('ExtensionsActions', () => { return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { - const testObject: ExtensionsActions.EnableDropDownAction = instantiationService.createInstance(ExtensionsActions.EnableDropDownAction); + const testObject: ExtensionsActions.EnableDropDownAction = disposables.add(instantiationService.createInstance(ExtensionsActions.EnableDropDownAction)); testObject.extension = extensions[0]; assert.ok(testObject.enabled); }); @@ -702,7 +699,7 @@ suite('ExtensionsActions', () => { return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { - const testObject: ExtensionsActions.EnableDropDownAction = instantiationService.createInstance(ExtensionsActions.EnableDropDownAction); + const testObject: ExtensionsActions.EnableDropDownAction = disposables.add(instantiationService.createInstance(ExtensionsActions.EnableDropDownAction)); testObject.extension = extensions[0]; assert.ok(testObject.enabled); }); @@ -715,7 +712,7 @@ suite('ExtensionsActions', () => { return instantiationService.get(IExtensionsWorkbenchService).queryGallery(CancellationToken.None) .then(page => { - const testObject: ExtensionsActions.EnableDropDownAction = instantiationService.createInstance(ExtensionsActions.EnableDropDownAction); + const testObject: ExtensionsActions.EnableDropDownAction = disposables.add(instantiationService.createInstance(ExtensionsActions.EnableDropDownAction)); testObject.extension = page.firstPage[0]; assert.ok(!testObject.enabled); }); @@ -727,9 +724,9 @@ suite('ExtensionsActions', () => { return instantiationService.get(IExtensionsWorkbenchService).queryGallery(CancellationToken.None) .then(page => { - const testObject: ExtensionsActions.EnableDropDownAction = instantiationService.createInstance(ExtensionsActions.EnableDropDownAction); + const testObject: ExtensionsActions.EnableDropDownAction = disposables.add(instantiationService.createInstance(ExtensionsActions.EnableDropDownAction)); testObject.extension = page.firstPage[0]; - instantiationService.createInstance(ExtensionContainers, [testObject]); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); installEvent.fire({ identifier: gallery.identifier, source: gallery }); assert.ok(!testObject.enabled); @@ -742,7 +739,7 @@ suite('ExtensionsActions', () => { return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { - const testObject: ExtensionsActions.EnableDropDownAction = instantiationService.createInstance(ExtensionsActions.EnableDropDownAction); + const testObject: ExtensionsActions.EnableDropDownAction = disposables.add(instantiationService.createInstance(ExtensionsActions.EnableDropDownAction)); testObject.extension = extensions[0]; uninstallEvent.fire({ identifier: local.identifier }); assert.ok(!testObject.enabled); @@ -750,7 +747,7 @@ suite('ExtensionsActions', () => { }); test('Test DisableForWorkspaceAction when there is no extension', () => { - const testObject: ExtensionsActions.DisableForWorkspaceAction = instantiationService.createInstance(ExtensionsActions.DisableForWorkspaceAction); + const testObject: ExtensionsActions.DisableForWorkspaceAction = disposables.add(instantiationService.createInstance(ExtensionsActions.DisableForWorkspaceAction)); assert.ok(!testObject.enabled); }); @@ -763,7 +760,7 @@ suite('ExtensionsActions', () => { return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { - const testObject: ExtensionsActions.DisableForWorkspaceAction = instantiationService.createInstance(ExtensionsActions.DisableForWorkspaceAction); + const testObject: ExtensionsActions.DisableForWorkspaceAction = disposables.add(instantiationService.createInstance(ExtensionsActions.DisableForWorkspaceAction)); testObject.extension = extensions[0]; assert.ok(!testObject.enabled); }); @@ -778,7 +775,7 @@ suite('ExtensionsActions', () => { return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { - const testObject: ExtensionsActions.DisableForWorkspaceAction = instantiationService.createInstance(ExtensionsActions.DisableForWorkspaceAction); + const testObject: ExtensionsActions.DisableForWorkspaceAction = disposables.add(instantiationService.createInstance(ExtensionsActions.DisableForWorkspaceAction)); testObject.extension = extensions[0]; assert.ok(!testObject.enabled); }); @@ -797,14 +794,14 @@ suite('ExtensionsActions', () => { return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { - const testObject: ExtensionsActions.DisableForWorkspaceAction = instantiationService.createInstance(ExtensionsActions.DisableForWorkspaceAction); + const testObject: ExtensionsActions.DisableForWorkspaceAction = disposables.add(instantiationService.createInstance(ExtensionsActions.DisableForWorkspaceAction)); testObject.extension = extensions[0]; assert.ok(testObject.enabled); }); }); test('Test DisableGloballyAction when there is no extension', () => { - const testObject: ExtensionsActions.DisableGloballyAction = instantiationService.createInstance(ExtensionsActions.DisableGloballyAction); + const testObject: ExtensionsActions.DisableGloballyAction = disposables.add(instantiationService.createInstance(ExtensionsActions.DisableGloballyAction)); assert.ok(!testObject.enabled); }); @@ -817,7 +814,7 @@ suite('ExtensionsActions', () => { return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { - const testObject: ExtensionsActions.DisableGloballyAction = instantiationService.createInstance(ExtensionsActions.DisableGloballyAction); + const testObject: ExtensionsActions.DisableGloballyAction = disposables.add(instantiationService.createInstance(ExtensionsActions.DisableGloballyAction)); testObject.extension = extensions[0]; assert.ok(!testObject.enabled); }); @@ -832,7 +829,7 @@ suite('ExtensionsActions', () => { return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { - const testObject: ExtensionsActions.DisableGloballyAction = instantiationService.createInstance(ExtensionsActions.DisableGloballyAction); + const testObject: ExtensionsActions.DisableGloballyAction = disposables.add(instantiationService.createInstance(ExtensionsActions.DisableGloballyAction)); testObject.extension = extensions[0]; assert.ok(!testObject.enabled); }); @@ -850,7 +847,7 @@ suite('ExtensionsActions', () => { return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { - const testObject: ExtensionsActions.DisableGloballyAction = instantiationService.createInstance(ExtensionsActions.DisableGloballyAction); + const testObject: ExtensionsActions.DisableGloballyAction = disposables.add(instantiationService.createInstance(ExtensionsActions.DisableGloballyAction)); testObject.extension = extensions[0]; assert.ok(testObject.enabled); }); @@ -867,7 +864,7 @@ suite('ExtensionsActions', () => { return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { - const testObject: ExtensionsActions.DisableGloballyAction = instantiationService.createInstance(ExtensionsActions.DisableGloballyAction); + const testObject: ExtensionsActions.DisableGloballyAction = disposables.add(instantiationService.createInstance(ExtensionsActions.DisableGloballyAction)); testObject.extension = extensions[0]; assert.ok(testObject.enabled); }); @@ -887,7 +884,7 @@ suite('ExtensionsActions', () => { return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { - const testObject: ExtensionsActions.DisableGloballyAction = instantiationService.createInstance(ExtensionsActions.DisableGloballyAction); + const testObject: ExtensionsActions.DisableGloballyAction = disposables.add(instantiationService.createInstance(ExtensionsActions.DisableGloballyAction)); testObject.extension = extensions[0]; assert.ok(!testObject.enabled); }); @@ -905,7 +902,7 @@ suite('ExtensionsActions', () => { return instantiationService.get(IExtensionsWorkbenchService).queryGallery(CancellationToken.None) .then(page => { - const testObject: ExtensionsActions.DisableGloballyAction = instantiationService.createInstance(ExtensionsActions.DisableGloballyAction); + const testObject: ExtensionsActions.DisableGloballyAction = disposables.add(instantiationService.createInstance(ExtensionsActions.DisableGloballyAction)); testObject.extension = page.firstPage[0]; assert.ok(!testObject.enabled); }); @@ -922,9 +919,9 @@ suite('ExtensionsActions', () => { return instantiationService.get(IExtensionsWorkbenchService).queryGallery(CancellationToken.None) .then(page => { - const testObject: ExtensionsActions.DisableGloballyAction = instantiationService.createInstance(ExtensionsActions.DisableGloballyAction); + const testObject: ExtensionsActions.DisableGloballyAction = disposables.add(instantiationService.createInstance(ExtensionsActions.DisableGloballyAction)); testObject.extension = page.firstPage[0]; - instantiationService.createInstance(ExtensionContainers, [testObject]); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); installEvent.fire({ identifier: gallery.identifier, source: gallery }); assert.ok(!testObject.enabled); }); @@ -941,9 +938,9 @@ suite('ExtensionsActions', () => { return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { - const testObject: ExtensionsActions.DisableGloballyAction = instantiationService.createInstance(ExtensionsActions.DisableGloballyAction); + const testObject: ExtensionsActions.DisableGloballyAction = disposables.add(instantiationService.createInstance(ExtensionsActions.DisableGloballyAction)); testObject.extension = extensions[0]; - instantiationService.createInstance(ExtensionContainers, [testObject]); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); uninstallEvent.fire({ identifier: local.identifier }); assert.ok(!testObject.enabled); }); @@ -953,19 +950,20 @@ suite('ExtensionsActions', () => { suite('ReloadAction', () => { - setup(setupTest); - teardown(() => disposables.dispose()); + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + setup(() => setupTest(disposables)); test('Test ReloadAction when there is no extension', () => { - const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); assert.ok(!testObject.enabled); }); test('Test ReloadAction when extension state is installing', async () => { - const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const workbenchService = instantiationService.get(IExtensionsWorkbenchService); const gallery = aGalleryExtension('a'); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -977,8 +975,8 @@ suite('ReloadAction', () => { }); test('Test ReloadAction when extension state is uninstalling', async () => { - const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const local = aLocalExtension('a'); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -996,8 +994,8 @@ suite('ReloadAction', () => { canAddExtension: (extension) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const gallery = aGalleryExtension('a'); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -1021,8 +1019,8 @@ suite('ReloadAction', () => { canAddExtension: (extension) => true, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const gallery = aGalleryExtension('a'); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -1043,8 +1041,8 @@ suite('ReloadAction', () => { canAddExtension: (extension) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const gallery = aGalleryExtension('a'); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); const paged = await instantiationService.get(IExtensionsWorkbenchService).queryGallery(CancellationToken.None); @@ -1067,9 +1065,9 @@ suite('ReloadAction', () => { canAddExtension: (extension) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - instantiationService.set(IExtensionsWorkbenchService, instantiationService.createInstance(ExtensionsWorkbenchService)); - const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + instantiationService.set(IExtensionsWorkbenchService, disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService))); + const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const local = aLocalExtension('a'); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); const extensions = await instantiationService.get(IExtensionsWorkbenchService).queryLocal(); @@ -1090,8 +1088,8 @@ suite('ReloadAction', () => { canAddExtension: (extension) => true, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); const extensions = await instantiationService.get(IExtensionsWorkbenchService).queryLocal(); testObject.extension = extensions[0]; @@ -1109,8 +1107,8 @@ suite('ReloadAction', () => { canAddExtension: (extension) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const local = aLocalExtension('a'); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); const extensions = await instantiationService.get(IExtensionsWorkbenchService).queryLocal(); @@ -1135,9 +1133,9 @@ suite('ReloadAction', () => { canAddExtension: (extension) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - instantiationService.set(IExtensionsWorkbenchService, instantiationService.createInstance(ExtensionsWorkbenchService)); - const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + instantiationService.set(IExtensionsWorkbenchService, disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService))); + const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const local = aLocalExtension('a', { version: '1.0.1' }); const workbenchService = instantiationService.get(IExtensionsWorkbenchService); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -1145,11 +1143,11 @@ suite('ReloadAction', () => { testObject.extension = extensions[0]; return new Promise(c => { - testObject.onDidChange(() => { + disposables.add(testObject.onDidChange(() => { if (testObject.enabled && testObject.tooltip === 'Please reload Visual Studio Code to enable the updated extension.') { c(); } - }); + })); const gallery = aGalleryExtension('a', { uuid: local.identifier.id, version: '1.0.2' }); installEvent.fire({ identifier: gallery.identifier, source: gallery }); didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', gallery, gallery) }]); @@ -1166,8 +1164,8 @@ suite('ReloadAction', () => { }); const local = aLocalExtension('a', { version: '1.0.1' }); await instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([local], EnablementState.DisabledGlobally); - const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const workbenchService = instantiationService.get(IExtensionsWorkbenchService); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); const extensions = await workbenchService.queryLocal(); @@ -1188,9 +1186,9 @@ suite('ReloadAction', () => { canAddExtension: (extension) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - instantiationService.set(IExtensionsWorkbenchService, instantiationService.createInstance(ExtensionsWorkbenchService)); - const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + instantiationService.set(IExtensionsWorkbenchService, disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService))); + const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const local = aLocalExtension('a'); const workbenchService = instantiationService.get(IExtensionsWorkbenchService); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -1211,9 +1209,9 @@ suite('ReloadAction', () => { canAddExtension: (extension) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - instantiationService.set(IExtensionsWorkbenchService, instantiationService.createInstance(ExtensionsWorkbenchService)); - const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + instantiationService.set(IExtensionsWorkbenchService, disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService))); + const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const local = aLocalExtension('a'); const workbenchService = instantiationService.get(IExtensionsWorkbenchService); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -1234,8 +1232,8 @@ suite('ReloadAction', () => { }); const local = aLocalExtension('a'); await instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([local], EnablementState.DisabledGlobally); - const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const workbenchService = instantiationService.get(IExtensionsWorkbenchService); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); const extensions = await workbenchService.queryLocal(); @@ -1256,8 +1254,8 @@ suite('ReloadAction', () => { }); const local = aLocalExtension('a'); await instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([local], EnablementState.DisabledGlobally); - const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const workbenchService = instantiationService.get(IExtensionsWorkbenchService); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); const extensions = await workbenchService.queryLocal(); @@ -1277,8 +1275,8 @@ suite('ReloadAction', () => { }); const local = aLocalExtension('a', { version: '1.0.1' }); await instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([local], EnablementState.DisabledGlobally); - const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const workbenchService = instantiationService.get(IExtensionsWorkbenchService); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); const extensions = await workbenchService.queryLocal(); @@ -1301,8 +1299,8 @@ suite('ReloadAction', () => { canAddExtension: (extension) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const gallery = aGalleryExtension('a'); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -1323,8 +1321,8 @@ suite('ReloadAction', () => { canAddExtension: (extension) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const local = aLocalExtension('a', { version: '1.0.1', contributes: { localizations: [{ languageId: 'de', translations: [] }] } }); const workbenchService = instantiationService.get(IExtensionsWorkbenchService); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -1344,7 +1342,7 @@ suite('ReloadAction', () => { const remoteExtension = aLocalExtension('a', { extensionKind: ['workspace'] }, { location: URI.file('pub.a').with({ scheme: Schemas.vscodeRemote }) }); const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([remoteExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); const onDidChangeExtensionsEmitter = new Emitter<{ added: IExtensionDescription[]; removed: IExtensionDescription[] }>(); instantiationService.stub(IExtensionService, >{ extensions: [toExtensionDescription(remoteExtension)], @@ -1352,11 +1350,11 @@ suite('ReloadAction', () => { canAddExtension: (extension) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); - const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); await workbenchService.queryGallery(CancellationToken.None); @@ -1378,7 +1376,7 @@ suite('ReloadAction', () => { localExtensionManagementService.onDidUninstallExtension = onDidUninstallEvent.event; const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, localExtensionManagementService, createExtensionManagementService([remoteExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); const onDidChangeExtensionsEmitter = new Emitter<{ added: IExtensionDescription[]; removed: IExtensionDescription[] }>(); instantiationService.stub(IExtensionService, >{ extensions: [toExtensionDescription(remoteExtension)], @@ -1386,11 +1384,11 @@ suite('ReloadAction', () => { canAddExtension: (extension) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); - const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); await workbenchService.queryGallery(CancellationToken.None); @@ -1414,8 +1412,8 @@ suite('ReloadAction', () => { const localExtension = aLocalExtension('a', { extensionKind: ['workspace'] }, { location: URI.file('pub.a') }); const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), remoteExtensionManagementService); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); const onDidChangeExtensionsEmitter = new Emitter<{ added: IExtensionDescription[]; removed: IExtensionDescription[] }>(); @@ -1425,8 +1423,8 @@ suite('ReloadAction', () => { canAddExtension: (extension) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); await workbenchService.queryGallery(CancellationToken.None); @@ -1453,8 +1451,8 @@ suite('ReloadAction', () => { const remoteExtension = aLocalExtension('a', { extensionKind: ['ui'] }, { location: URI.file('pub.a').with({ scheme: Schemas.vscodeRemote }) }); const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, localExtensionManagementService, createExtensionManagementService([remoteExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); const onDidChangeExtensionsEmitter = new Emitter<{ added: IExtensionDescription[]; removed: IExtensionDescription[] }>(); @@ -1464,8 +1462,8 @@ suite('ReloadAction', () => { canAddExtension: (extension) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); await workbenchService.queryGallery(CancellationToken.None); @@ -1493,7 +1491,7 @@ suite('ReloadAction', () => { const remoteExtension = aLocalExtension('a', { extensionKind: ['ui'] }, { location: URI.file('pub.a').with({ scheme: Schemas.vscodeRemote }) }); const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, localExtensionManagementService, createExtensionManagementService([remoteExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); const onDidChangeExtensionsEmitter = new Emitter<{ added: IExtensionDescription[]; removed: IExtensionDescription[] }>(); instantiationService.stub(IExtensionService, >{ extensions: [toExtensionDescription(localExtension)], @@ -1501,11 +1499,11 @@ suite('ReloadAction', () => { canAddExtension: (extension) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); - const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); await workbenchService.queryGallery(CancellationToken.None); @@ -1525,8 +1523,8 @@ suite('ReloadAction', () => { const remoteExtension = aLocalExtension('a', { extensionKind: ['workspace', 'ui'] }, { location: URI.file('pub.a').with({ scheme: Schemas.vscodeRemote }) }); const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, localExtensionManagementService, createExtensionManagementService([remoteExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); const onDidChangeExtensionsEmitter = new Emitter<{ added: IExtensionDescription[]; removed: IExtensionDescription[] }>(); @@ -1536,8 +1534,8 @@ suite('ReloadAction', () => { canAddExtension: (extension) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); await workbenchService.queryGallery(CancellationToken.None); @@ -1557,8 +1555,8 @@ suite('ReloadAction', () => { remoteExtensionManagementService.onDidInstallExtensions = onDidInstallEvent.event; const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), remoteExtensionManagementService); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); const onDidChangeExtensionsEmitter = new Emitter<{ added: IExtensionDescription[]; removed: IExtensionDescription[] }>(); @@ -1568,8 +1566,8 @@ suite('ReloadAction', () => { canAddExtension: (extension) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); await workbenchService.queryGallery(CancellationToken.None); @@ -1589,8 +1587,8 @@ suite('ReloadAction', () => { const remoteExtension = aLocalExtension('a', { extensionKind: ['workspace', 'ui'] }, { location: URI.file('pub.a').with({ scheme: Schemas.vscodeRemote }) }); const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, localExtensionManagementService, createExtensionManagementService([remoteExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); const onDidChangeExtensionsEmitter = new Emitter<{ added: IExtensionDescription[]; removed: IExtensionDescription[] }>(); @@ -1600,8 +1598,8 @@ suite('ReloadAction', () => { canAddExtension: (extension) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); await workbenchService.queryGallery(CancellationToken.None); @@ -1621,8 +1619,8 @@ suite('ReloadAction', () => { remoteExtensionManagementService.onDidInstallExtensions = onDidInstallEvent.event; const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), remoteExtensionManagementService); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); const onDidChangeExtensionsEmitter = new Emitter<{ added: IExtensionDescription[]; removed: IExtensionDescription[] }>(); @@ -1632,8 +1630,8 @@ suite('ReloadAction', () => { canAddExtension: (extension) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); await workbenchService.queryGallery(CancellationToken.None); @@ -1650,18 +1648,18 @@ suite('ReloadAction', () => { const remoteExtension = aLocalExtension('a', { extensionKind: ['ui', 'workspace'], 'browser': 'browser.js' }, { location: URI.file('pub.a').with({ scheme: Schemas.vscodeRemote }) }); const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, null, createExtensionManagementService([remoteExtension]), createExtensionManagementService([webExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); instantiationService.stub(IExtensionService, >{ extensions: [toExtensionDescription(remoteExtension)], onDidChangeExtensions: Event.None, canAddExtension: (extension) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); - const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); await workbenchService.queryGallery(CancellationToken.None); @@ -1678,18 +1676,18 @@ suite('ReloadAction', () => { const localExtension = aLocalExtension('a', { extensionKind: ['workspace', 'ui'], 'browser': 'browser.js' }, { location: URI.file('pub.a') }); const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), null, createExtensionManagementService([webExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); instantiationService.stub(IExtensionService, >{ extensions: [toExtensionDescription(localExtension)], onDidChangeExtensions: Event.None, canAddExtension: (extension) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); - const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); await workbenchService.queryGallery(CancellationToken.None); @@ -1702,21 +1700,22 @@ suite('ReloadAction', () => { suite('RemoteInstallAction', () => { - setup(setupTest); - teardown(() => disposables.dispose()); + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + setup(() => setupTest(disposables)); test('Test remote install action is enabled for local workspace extension', async () => { // multi server setup const localWorkspaceExtension = aLocalExtension('a', { extensionKind: ['workspace'] }, { location: URI.file(`pub.a`) }); const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localWorkspaceExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a', { identifier: localWorkspaceExtension.identifier }))); - const testObject = instantiationService.createInstance(ExtensionsActions.RemoteInstallAction, false); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsActions.RemoteInstallAction, false)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const extensions = await workbenchService.queryLocal(extensionManagementServerService.localExtensionManagementServer!); await workbenchService.queryGallery(CancellationToken.None); @@ -1734,15 +1733,15 @@ suite('RemoteInstallAction', () => { const localWorkspaceExtension = aLocalExtension('a', { extensionKind: ['workspace'] }, { location: URI.file(`pub.a`) }); const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localWorkspaceExtension]), remoteExtensionManagementService); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.stub(IExtensionsWorkbenchService, workbenchService, 'open', undefined); instantiationService.set(IExtensionsWorkbenchService, workbenchService); const gallery = aGalleryExtension('a', { identifier: localWorkspaceExtension.identifier }); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); - const testObject = instantiationService.createInstance(ExtensionsActions.RemoteInstallAction, false); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsActions.RemoteInstallAction, false)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const extensions = await workbenchService.queryLocal(extensionManagementServerService.localExtensionManagementServer!); await workbenchService.queryGallery(CancellationToken.None); @@ -1767,15 +1766,15 @@ suite('RemoteInstallAction', () => { const localWorkspaceExtension = aLocalExtension('a', { extensionKind: ['workspace'] }, { location: URI.file(`pub.a`) }); const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localWorkspaceExtension]), remoteExtensionManagementService); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.stub(IExtensionsWorkbenchService, workbenchService, 'open', undefined); instantiationService.set(IExtensionsWorkbenchService, workbenchService); const gallery = aGalleryExtension('a', { identifier: localWorkspaceExtension.identifier }); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); - const testObject = instantiationService.createInstance(ExtensionsActions.RemoteInstallAction, false); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsActions.RemoteInstallAction, false)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const extensions = await workbenchService.queryLocal(extensionManagementServerService.localExtensionManagementServer!); await workbenchService.queryGallery(CancellationToken.None); @@ -1801,15 +1800,15 @@ suite('RemoteInstallAction', () => { const localWorkspaceExtension = aLocalExtension('a', { extensionKind: ['workspace'] }, { location: URI.file(`pub.a`) }); const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localWorkspaceExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); const remoteWorkspaceExtension = aLocalExtension('a', { extensionKind: ['workspace'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); await instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([remoteWorkspaceExtension], EnablementState.DisabledGlobally); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a', { identifier: localWorkspaceExtension.identifier }))); - const testObject = instantiationService.createInstance(ExtensionsActions.RemoteInstallAction, false); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsActions.RemoteInstallAction, false)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const extensions = await workbenchService.queryLocal(extensionManagementServerService.localExtensionManagementServer!); await workbenchService.queryGallery(CancellationToken.None); @@ -1824,14 +1823,14 @@ suite('RemoteInstallAction', () => { const localWorkspaceExtension = aLocalExtension('a', { extensionKind: ['workspace', 'ui'] }, { location: URI.file(`pub.a`) }); const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localWorkspaceExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); await instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([localWorkspaceExtension], EnablementState.DisabledGlobally); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a', { identifier: localWorkspaceExtension.identifier }))); - const testObject = instantiationService.createInstance(ExtensionsActions.RemoteInstallAction, false); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsActions.RemoteInstallAction, false)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const extensions = await workbenchService.queryLocal(extensionManagementServerService.localExtensionManagementServer!); await workbenchService.queryGallery(CancellationToken.None); @@ -1846,14 +1845,14 @@ suite('RemoteInstallAction', () => { const localWorkspaceExtension = aLocalExtension('a', { extensionKind: ['ui', 'workspace'] }, { location: URI.file(`pub.a`) }); const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localWorkspaceExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); await instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([localWorkspaceExtension], EnablementState.DisabledGlobally); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a', { identifier: localWorkspaceExtension.identifier }))); - const testObject = instantiationService.createInstance(ExtensionsActions.RemoteInstallAction, true); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsActions.RemoteInstallAction, true)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const extensions = await workbenchService.queryLocal(extensionManagementServerService.localExtensionManagementServer!); await workbenchService.queryGallery(CancellationToken.None); @@ -1868,14 +1867,14 @@ suite('RemoteInstallAction', () => { const localWorkspaceExtension = aLocalExtension('a', { extensionKind: ['ui', 'workspace'] }, { location: URI.file(`pub.a`) }); const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localWorkspaceExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); await instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([localWorkspaceExtension], EnablementState.DisabledGlobally); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a', { identifier: localWorkspaceExtension.identifier }))); - const testObject = instantiationService.createInstance(ExtensionsActions.RemoteInstallAction, false); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsActions.RemoteInstallAction, false)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const extensions = await workbenchService.queryLocal(extensionManagementServerService.localExtensionManagementServer!); await workbenchService.queryGallery(CancellationToken.None); @@ -1887,13 +1886,13 @@ suite('RemoteInstallAction', () => { // multi server setup const localWorkspaceExtension = aLocalExtension('a', { extensionKind: ['workspace'] }, { location: URI.file(`pub.a`) }); const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localWorkspaceExtension])); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a', { identifier: localWorkspaceExtension.identifier }))); - const testObject = instantiationService.createInstance(ExtensionsActions.RemoteInstallAction, false); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsActions.RemoteInstallAction, false)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); await workbenchService.queryLocal(extensionManagementServerService.localExtensionManagementServer!); await workbenchService.queryGallery(CancellationToken.None); @@ -1904,13 +1903,13 @@ suite('RemoteInstallAction', () => { // multi server setup const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a'))); - const testObject = instantiationService.createInstance(ExtensionsActions.RemoteInstallAction, false); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsActions.RemoteInstallAction, false)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const pager = await workbenchService.queryGallery(CancellationToken.None); testObject.extension = pager.firstPage[0]; @@ -1928,13 +1927,13 @@ suite('RemoteInstallAction', () => { instantiationService.stub(IWorkbenchEnvironmentService, environmentService); instantiationService.stub(INativeWorkbenchEnvironmentService, environmentService); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a', { identifier: localWorkspaceExtension.identifier }))); - const testObject = instantiationService.createInstance(ExtensionsActions.RemoteInstallAction, false); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsActions.RemoteInstallAction, false)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const extensions = await workbenchService.queryLocal(extensionManagementServerService.localExtensionManagementServer!); await workbenchService.queryGallery(CancellationToken.None); @@ -1950,8 +1949,8 @@ suite('RemoteInstallAction', () => { const localWorkspaceExtension = aLocalExtension('a', { extensionKind: ['workspace'] }, { location: URI.file(`pub.a`) }); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [localWorkspaceExtension]); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a', { identifier: localWorkspaceExtension.identifier }))); - const testObject = instantiationService.createInstance(ExtensionsActions.RemoteInstallAction, false); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsActions.RemoteInstallAction, false)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const extensions = await workbenchService.queryLocal(extensionManagementServerService.localExtensionManagementServer!); await workbenchService.queryGallery(CancellationToken.None); @@ -1965,15 +1964,15 @@ suite('RemoteInstallAction', () => { const extensionManagementService = instantiationService.get(IExtensionManagementService) as IProfileAwareExtensionManagementService; const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, extensionManagementService); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); const localWorkspaceExtension = aLocalExtension('a', { extensionKind: ['workspace'] }, { location: URI.file(`pub.a`) }); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [localWorkspaceExtension]); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a', { identifier: localWorkspaceExtension.identifier }))); - const testObject = instantiationService.createInstance(ExtensionsActions.RemoteInstallAction, false); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsActions.RemoteInstallAction, false)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const extensions = await workbenchService.queryLocal(extensionManagementServerService.localExtensionManagementServer!); await workbenchService.queryGallery(CancellationToken.None); @@ -1991,13 +1990,13 @@ suite('RemoteInstallAction', () => { const remoteWorkspaceExtension = aLocalExtension('a', { extensionKind: ['workspace'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localWorkspaceExtension]), createExtensionManagementService([remoteWorkspaceExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a', { identifier: localWorkspaceExtension.identifier }))); - const testObject = instantiationService.createInstance(ExtensionsActions.RemoteInstallAction, false); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsActions.RemoteInstallAction, false)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const extensions = await workbenchService.queryLocal(extensionManagementServerService.localExtensionManagementServer!); await workbenchService.queryGallery(CancellationToken.None); @@ -2011,13 +2010,13 @@ suite('RemoteInstallAction', () => { const localWorkspaceExtension = aLocalExtension('a', { extensionKind: ['workspace'] }, { location: URI.file(`pub.a`) }); const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localWorkspaceExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a', { identifier: localWorkspaceExtension.identifier }))); - const testObject = instantiationService.createInstance(ExtensionsActions.RemoteInstallAction, false); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsActions.RemoteInstallAction, false)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const extensions = await workbenchService.queryLocal(extensionManagementServerService.localExtensionManagementServer!); testObject.extension = extensions[0]; @@ -2030,13 +2029,13 @@ suite('RemoteInstallAction', () => { const localWorkspaceSystemExtension = aLocalExtension('a', { extensionKind: ['workspace'] }, { location: URI.file(`pub.a`), type: ExtensionType.System }); const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localWorkspaceSystemExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a', { identifier: localWorkspaceSystemExtension.identifier }))); - const testObject = instantiationService.createInstance(ExtensionsActions.RemoteInstallAction, false); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsActions.RemoteInstallAction, false)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const extensions = await workbenchService.queryLocal(extensionManagementServerService.localExtensionManagementServer!); testObject.extension = extensions[0]; @@ -2049,13 +2048,13 @@ suite('RemoteInstallAction', () => { const localUIExtension = aLocalExtension('a', { extensionKind: ['ui'] }, { location: URI.file(`pub.a`) }); const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localUIExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a', { identifier: localUIExtension.identifier }))); - const testObject = instantiationService.createInstance(ExtensionsActions.RemoteInstallAction, false); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsActions.RemoteInstallAction, false)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const extensions = await workbenchService.queryLocal(extensionManagementServerService.localExtensionManagementServer!); testObject.extension = extensions[0]; @@ -2069,13 +2068,13 @@ suite('RemoteInstallAction', () => { const remoteUIExtension = aLocalExtension('a', { extensionKind: ['ui'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localUIExtension]), createExtensionManagementService([remoteUIExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a', { identifier: localUIExtension.identifier }))); - const testObject = instantiationService.createInstance(ExtensionsActions.RemoteInstallAction, false); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsActions.RemoteInstallAction, false)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const extensions = await workbenchService.queryLocal(extensionManagementServerService.localExtensionManagementServer!); testObject.extension = extensions[0]; @@ -2088,13 +2087,13 @@ suite('RemoteInstallAction', () => { const languagePackExtension = aLocalExtension('a', { contributes: { localizations: [{ languageId: 'de', translations: [] }] } }, { location: URI.file(`pub.a`) }); const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([languagePackExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a', { identifier: languagePackExtension.identifier }))); - const testObject = instantiationService.createInstance(ExtensionsActions.RemoteInstallAction, false); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsActions.RemoteInstallAction, false)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const extensions = await workbenchService.queryLocal(extensionManagementServerService.localExtensionManagementServer!); await workbenchService.queryGallery(CancellationToken.None); @@ -2109,15 +2108,15 @@ suite('RemoteInstallAction', () => { const extensionManagementService = instantiationService.get(IExtensionManagementService) as IProfileAwareExtensionManagementService; const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, extensionManagementService); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); const languagePackExtension = aLocalExtension('a', { contributes: { localizations: [{ languageId: 'de', translations: [] }] } }, { location: URI.file(`pub.a`) }); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [languagePackExtension]); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a', { identifier: languagePackExtension.identifier }))); - const testObject = instantiationService.createInstance(ExtensionsActions.RemoteInstallAction, false); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsActions.RemoteInstallAction, false)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const extensions = await workbenchService.queryLocal(extensionManagementServerService.localExtensionManagementServer!); await workbenchService.queryGallery(CancellationToken.None); @@ -2132,21 +2131,22 @@ suite('RemoteInstallAction', () => { suite('LocalInstallAction', () => { - setup(setupTest); - teardown(() => disposables.dispose()); + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + setup(() => setupTest(disposables)); test('Test local install action is enabled for remote ui extension', async () => { // multi server setup const remoteUIExtension = aLocalExtension('a', { extensionKind: ['ui'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService(), createExtensionManagementService([remoteUIExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a', { identifier: remoteUIExtension.identifier }))); - const testObject = instantiationService.createInstance(ExtensionsActions.LocalInstallAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsActions.LocalInstallAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const extensions = await workbenchService.queryLocal(extensionManagementServerService.remoteExtensionManagementServer!); await workbenchService.queryGallery(CancellationToken.None); @@ -2161,13 +2161,13 @@ suite('LocalInstallAction', () => { const remoteUIExtension = aLocalExtension('a', { extensionKind: ['ui', 'workspace'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService(), createExtensionManagementService([remoteUIExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a', { identifier: remoteUIExtension.identifier }))); - const testObject = instantiationService.createInstance(ExtensionsActions.LocalInstallAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsActions.LocalInstallAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const extensions = await workbenchService.queryLocal(extensionManagementServerService.remoteExtensionManagementServer!); await workbenchService.queryGallery(CancellationToken.None); @@ -2185,15 +2185,15 @@ suite('LocalInstallAction', () => { const remoteUIExtension = aLocalExtension('a', { extensionKind: ['ui'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, localExtensionManagementService, createExtensionManagementService([remoteUIExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.stub(IExtensionsWorkbenchService, workbenchService, 'open', undefined); instantiationService.set(IExtensionsWorkbenchService, workbenchService); const gallery = aGalleryExtension('a', { identifier: remoteUIExtension.identifier }); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); - const testObject = instantiationService.createInstance(ExtensionsActions.LocalInstallAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsActions.LocalInstallAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const extensions = await workbenchService.queryLocal(extensionManagementServerService.remoteExtensionManagementServer!); await workbenchService.queryGallery(CancellationToken.None); @@ -2218,15 +2218,15 @@ suite('LocalInstallAction', () => { const remoteUIExtension = aLocalExtension('a', { extensionKind: ['ui'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, localExtensionManagementService, createExtensionManagementService([remoteUIExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.stub(IExtensionsWorkbenchService, workbenchService, 'open', undefined); instantiationService.set(IExtensionsWorkbenchService, workbenchService); const gallery = aGalleryExtension('a', { identifier: remoteUIExtension.identifier }); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); - const testObject = instantiationService.createInstance(ExtensionsActions.LocalInstallAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsActions.LocalInstallAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const extensions = await workbenchService.queryLocal(extensionManagementServerService.remoteExtensionManagementServer!); await workbenchService.queryGallery(CancellationToken.None); @@ -2252,15 +2252,15 @@ suite('LocalInstallAction', () => { const remoteUIExtension = aLocalExtension('a', { extensionKind: ['ui'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService(), createExtensionManagementService([remoteUIExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); const localUIExtension = aLocalExtension('a', { extensionKind: ['ui'] }, { location: URI.file(`pub.a`) }); await instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([localUIExtension], EnablementState.DisabledGlobally); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a', { identifier: remoteUIExtension.identifier }))); - const testObject = instantiationService.createInstance(ExtensionsActions.LocalInstallAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsActions.LocalInstallAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const extensions = await workbenchService.queryLocal(extensionManagementServerService.remoteExtensionManagementServer!); await workbenchService.queryGallery(CancellationToken.None); @@ -2274,13 +2274,13 @@ suite('LocalInstallAction', () => { // multi server setup const remoteUIExtension = aLocalExtension('a', { extensionKind: ['ui'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService(), createExtensionManagementService([remoteUIExtension])); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a', { identifier: remoteUIExtension.identifier }))); - const testObject = instantiationService.createInstance(ExtensionsActions.LocalInstallAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsActions.LocalInstallAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); await workbenchService.queryLocal(extensionManagementServerService.remoteExtensionManagementServer!); await workbenchService.queryGallery(CancellationToken.None); @@ -2291,13 +2291,13 @@ suite('LocalInstallAction', () => { // multi server setup const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a'))); - const testObject = instantiationService.createInstance(ExtensionsActions.LocalInstallAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsActions.LocalInstallAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const pager = await workbenchService.queryGallery(CancellationToken.None); testObject.extension = pager.firstPage[0]; @@ -2315,13 +2315,13 @@ suite('LocalInstallAction', () => { instantiationService.stub(INativeWorkbenchEnvironmentService, environmentService); const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService(), createExtensionManagementService([remoteUIExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a', { identifier: remoteUIExtension.identifier }))); - const testObject = instantiationService.createInstance(ExtensionsActions.LocalInstallAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsActions.LocalInstallAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const extensions = await workbenchService.queryLocal(extensionManagementServerService.remoteExtensionManagementServer!); await workbenchService.queryGallery(CancellationToken.None); @@ -2335,13 +2335,13 @@ suite('LocalInstallAction', () => { const remoteUIExtension = aLocalExtension('a', { extensionKind: ['ui'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); const extensionManagementServerService = aSingleRemoteExtensionManagementServerService(instantiationService, createExtensionManagementService([remoteUIExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a', { identifier: remoteUIExtension.identifier }))); - const testObject = instantiationService.createInstance(ExtensionsActions.LocalInstallAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsActions.LocalInstallAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const extensions = await workbenchService.queryLocal(extensionManagementServerService.remoteExtensionManagementServer!); await workbenchService.queryGallery(CancellationToken.None); @@ -2356,13 +2356,13 @@ suite('LocalInstallAction', () => { const remoteUIExtension = aLocalExtension('a', { extensionKind: ['ui'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localUIExtension]), createExtensionManagementService([remoteUIExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a', { identifier: localUIExtension.identifier }))); - const testObject = instantiationService.createInstance(ExtensionsActions.LocalInstallAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsActions.LocalInstallAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const extensions = await workbenchService.queryLocal(extensionManagementServerService.remoteExtensionManagementServer!); await workbenchService.queryGallery(CancellationToken.None); @@ -2376,15 +2376,15 @@ suite('LocalInstallAction', () => { const extensionManagementService = instantiationService.get(IExtensionManagementService) as IProfileAwareExtensionManagementService; const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService(), extensionManagementService); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); const remoteUIExtension = aLocalExtension('a', { extensionKind: ['ui'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [remoteUIExtension]); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a', { identifier: remoteUIExtension.identifier }))); - const testObject = instantiationService.createInstance(ExtensionsActions.LocalInstallAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsActions.LocalInstallAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const extensions = await workbenchService.queryLocal(extensionManagementServerService.remoteExtensionManagementServer!); await workbenchService.queryGallery(CancellationToken.None); @@ -2401,13 +2401,13 @@ suite('LocalInstallAction', () => { const remoteUIExtension = aLocalExtension('a', { extensionKind: ['ui'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService(), createExtensionManagementService([remoteUIExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a', { identifier: remoteUIExtension.identifier }))); - const testObject = instantiationService.createInstance(ExtensionsActions.LocalInstallAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsActions.LocalInstallAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const extensions = await workbenchService.queryLocal(extensionManagementServerService.remoteExtensionManagementServer!); testObject.extension = extensions[0]; @@ -2420,13 +2420,13 @@ suite('LocalInstallAction', () => { const remoteUISystemExtension = aLocalExtension('a', { extensionKind: ['ui'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }), type: ExtensionType.System }); const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService(), createExtensionManagementService([remoteUISystemExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a', { identifier: remoteUISystemExtension.identifier }))); - const testObject = instantiationService.createInstance(ExtensionsActions.LocalInstallAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsActions.LocalInstallAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const extensions = await workbenchService.queryLocal(extensionManagementServerService.remoteExtensionManagementServer!); testObject.extension = extensions[0]; @@ -2439,13 +2439,13 @@ suite('LocalInstallAction', () => { const remoteWorkspaceExtension = aLocalExtension('a', { extensionKind: ['workspace'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService(), createExtensionManagementService([remoteWorkspaceExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a', { identifier: remoteWorkspaceExtension.identifier }))); - const testObject = instantiationService.createInstance(ExtensionsActions.LocalInstallAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsActions.LocalInstallAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const extensions = await workbenchService.queryLocal(extensionManagementServerService.remoteExtensionManagementServer!); testObject.extension = extensions[0]; @@ -2459,13 +2459,13 @@ suite('LocalInstallAction', () => { const remoteWorkspaceExtension = aLocalExtension('a', { extensionKind: ['workspace'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localWorkspaceExtension]), createExtensionManagementService([remoteWorkspaceExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a', { identifier: localWorkspaceExtension.identifier }))); - const testObject = instantiationService.createInstance(ExtensionsActions.LocalInstallAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsActions.LocalInstallAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const extensions = await workbenchService.queryLocal(extensionManagementServerService.remoteExtensionManagementServer!); testObject.extension = extensions[0]; @@ -2478,13 +2478,13 @@ suite('LocalInstallAction', () => { const languagePackExtension = aLocalExtension('a', { contributes: { localizations: [{ languageId: 'de', translations: [] }] } }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService(), createExtensionManagementService([languagePackExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a', { identifier: languagePackExtension.identifier }))); - const testObject = instantiationService.createInstance(ExtensionsActions.LocalInstallAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsActions.LocalInstallAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const extensions = await workbenchService.queryLocal(extensionManagementServerService.remoteExtensionManagementServer!); await workbenchService.queryGallery(CancellationToken.None); @@ -2499,15 +2499,15 @@ suite('LocalInstallAction', () => { const extensionManagementService = instantiationService.get(IExtensionManagementService) as IProfileAwareExtensionManagementService; const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService(), extensionManagementService); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); const languagePackExtension = aLocalExtension('a', { contributes: { localizations: [{ languageId: 'de', translations: [] }] } }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [languagePackExtension]); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a', { identifier: languagePackExtension.identifier }))); - const testObject = instantiationService.createInstance(ExtensionsActions.LocalInstallAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsActions.LocalInstallAction)); + disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const extensions = await workbenchService.queryLocal(extensionManagementServerService.remoteExtensionManagementServer!); await workbenchService.queryGallery(CancellationToken.None); diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts index e5a665e70dd..440111628df 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts @@ -47,9 +47,12 @@ import { platform } from 'vs/base/common/platform'; import { arch } from 'vs/base/common/process'; import { IProductService } from 'vs/platform/product/common/productService'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('ExtensionsViews Tests', () => { + const disposableStore = ensureNoDisposablesAreLeakedInTestSuite(); + let instantiationService: TestInstantiationService; let testableView: ExtensionsListView; let installEvent: Emitter, @@ -75,13 +78,13 @@ suite('ExtensionsViews Tests', () => { const fileBasedRecommendationB = aGalleryExtension('filebased-recommendation-B'); const otherRecommendationA = aGalleryExtension('other-recommendation-A'); - suiteSetup(() => { - installEvent = new Emitter(); - didInstallEvent = new Emitter(); - uninstallEvent = new Emitter(); - didUninstallEvent = new Emitter(); + setup(async () => { + installEvent = disposableStore.add(new Emitter()); + didInstallEvent = disposableStore.add(new Emitter()); + uninstallEvent = disposableStore.add(new Emitter()); + didUninstallEvent = disposableStore.add(new Emitter()); - instantiationService = new TestInstantiationService(); + instantiationService = disposableStore.add(new TestInstantiationService()); instantiationService.stub(ITelemetryService, NullTelemetryService); instantiationService.stub(ILogService, NullLogService); instantiationService.stub(IProductService, {}); @@ -122,7 +125,7 @@ suite('ExtensionsViews Tests', () => { } }); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposableStore.add(new TestExtensionEnablementService(instantiationService))); const reasons: { [key: string]: any } = {}; reasons[workspaceRecommendationA.identifier.id] = { reasonId: ExtensionRecommendationReason.Workspace }; @@ -163,9 +166,7 @@ suite('ExtensionsViews Tests', () => { } }); instantiationService.stub(IURLService, NativeURLService); - }); - setup(async () => { instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [localEnabledTheme, localEnabledLanguage, localRandom, localDisabledTheme, localDisabledLanguage, builtInTheme, builtInBasic]); instantiationService.stubPromise(IExtensionManagementService, 'getExtensgetExtensionsControlManifestionsReport', {}); instantiationService.stub(IExtensionGalleryService, 'isEnabled', true); @@ -195,17 +196,8 @@ suite('ExtensionsViews Tests', () => { await (instantiationService.get(IWorkbenchExtensionEnablementService)).setEnablement([localDisabledTheme], EnablementState.DisabledGlobally); await (instantiationService.get(IWorkbenchExtensionEnablementService)).setEnablement([localDisabledLanguage], EnablementState.DisabledGlobally); - instantiationService.set(IExtensionsWorkbenchService, instantiationService.createInstance(ExtensionsWorkbenchService)); - testableView = instantiationService.createInstance(ExtensionsListView, {}, { id: '', title: '' }); - }); - - teardown(() => { - (instantiationService.get(IExtensionsWorkbenchService)).dispose(); - testableView.dispose(); - }); - - suiteTeardown(() => { - instantiationService.dispose(); + instantiationService.set(IExtensionsWorkbenchService, disposableStore.add(instantiationService.createInstance(ExtensionsWorkbenchService))); + testableView = disposableStore.add(instantiationService.createInstance(ExtensionsListView, {}, { id: '', title: '' })); }); test('Test query types', () => { @@ -513,7 +505,7 @@ suite('ExtensionsViews Tests', () => { }); testableView.dispose(); - testableView = instantiationService.createInstance(ExtensionsListView, {}, { id: '', title: '' }); + testableView = disposableStore.add(instantiationService.createInstance(ExtensionsListView, {}, { id: '', title: '' })); return testableView.show('search-me').then(result => { const options: IQueryOptions = queryTarget.args[0][0]; @@ -540,7 +532,7 @@ suite('ExtensionsViews Tests', () => { const queryTarget = instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(...realResults)); testableView.dispose(); - testableView = instantiationService.createInstance(ExtensionsListView, {}, { id: '', title: '' }); + disposableStore.add(testableView = instantiationService.createInstance(ExtensionsListView, {}, { id: '', title: '' })); return testableView.show('search-me @sort:installs').then(result => { const options: IQueryOptions = queryTarget.args[0][0]; diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts index 36450a6c665..91d873c7af0 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts @@ -48,28 +48,28 @@ import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKe import { platform } from 'vs/base/common/platform'; import { arch } from 'vs/base/common/process'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { toDisposable } from 'vs/base/common/lifecycle'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('ExtensionsWorkbenchServiceTest', () => { let instantiationService: TestInstantiationService; let testObject: IExtensionsWorkbenchService; - const suiteDisposables = new DisposableStore(); - let testDisposables: DisposableStore = new DisposableStore(); + const disposableStore = ensureNoDisposablesAreLeakedInTestSuite(); let installEvent: Emitter, didInstallEvent: Emitter, uninstallEvent: Emitter, didUninstallEvent: Emitter; - suiteSetup(() => { - suiteDisposables.add(toDisposable(() => sinon.restore())); - installEvent = suiteDisposables.add(new Emitter()); - didInstallEvent = suiteDisposables.add(new Emitter()); - uninstallEvent = suiteDisposables.add(new Emitter()); - didUninstallEvent = suiteDisposables.add(new Emitter()); + setup(async () => { + disposableStore.add(toDisposable(() => sinon.restore())); + installEvent = disposableStore.add(new Emitter()); + didInstallEvent = disposableStore.add(new Emitter()); + uninstallEvent = disposableStore.add(new Emitter()); + didUninstallEvent = disposableStore.add(new Emitter()); - instantiationService = suiteDisposables.add(new TestInstantiationService()); + instantiationService = disposableStore.add(new TestInstantiationService()); instantiationService.stub(ITelemetryService, NullTelemetryService); instantiationService.stub(ILogService, NullLogService); instantiationService.stub(IProgressService, ProgressService); @@ -115,10 +115,10 @@ suite('ExtensionsWorkbenchServiceTest', () => { extensionManagementService: instantiationService.get(IExtensionManagementService) as IProfileAwareExtensionManagementService, }, null, null)); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposableStore.add(new TestExtensionEnablementService(instantiationService))); - instantiationService.stub(ILifecycleService, new TestLifecycleService()); - instantiationService.stub(IExtensionTipsService, instantiationService.createInstance(TestExtensionTipsService)); + instantiationService.stub(ILifecycleService, disposableStore.add(new TestLifecycleService())); + instantiationService.stub(IExtensionTipsService, disposableStore.add(instantiationService.createInstance(TestExtensionTipsService))); instantiationService.stub(IExtensionRecommendationsService, {}); instantiationService.stub(INotificationService, { prompt: () => null! }); @@ -128,12 +128,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { extensions: [], async whenInstalledExtensionsRegistered() { return true; } }); - }); - suiteTeardown(() => suiteDisposables.dispose()); - - setup(async () => { - testDisposables = new DisposableStore(); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', []); instantiationService.stub(IExtensionGalleryService, 'isEnabled', true); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage()); @@ -142,8 +137,6 @@ suite('ExtensionsWorkbenchServiceTest', () => { (instantiationService.get(IWorkbenchExtensionEnablementService)).reset(); }); - teardown(() => testDisposables.dispose()); - test('test gallery extension', async () => { const expected = aGalleryExtension('expectedName', { displayName: 'expectedDisplayName', @@ -325,7 +318,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { testObject = await aWorkbenchService(); await testObject.queryLocal(); - return eventToPromise(testObject.onChange).then(() => { + return Event.toPromise(testObject.onChange).then(() => { const actuals = testObject.local; assert.strictEqual(2, actuals.length); @@ -448,7 +441,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { testObject = await aWorkbenchService(); const target = testObject.local[0]; - await eventToPromise(Event.filter(testObject.onChange, e => !!e?.gallery)); + await Event.toPromise(Event.filter(testObject.onChange, e => !!e?.gallery)); assert.ok(await testObject.canInstall(target)); }); @@ -482,7 +475,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { assert.strictEqual(ExtensionState.Uninstalled, extension.state); testObject.install(extension); - testObject.onChange(target); + disposableStore.add(testObject.onChange(target)); // Installing installEvent.fire({ identifier: gallery.identifier, source: gallery }); @@ -498,7 +491,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { const target = sinon.spy(); testObject.uninstall(testObject.local[0]); - testObject.onChange(target); + disposableStore.add(testObject.onChange(target)); uninstallEvent.fire({ identifier: local.identifier }); assert.ok(target.calledOnce); @@ -512,7 +505,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { testObject.uninstall(testObject.local[0]); uninstallEvent.fire({ identifier: local.identifier }); - testObject.onChange(target); + disposableStore.add(testObject.onChange(target)); didUninstallEvent.fire({ identifier: local.identifier }); assert.ok(target.calledOnce); @@ -996,7 +989,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [aLocalExtension('a')]); testObject = await aWorkbenchService(); const target = sinon.spy(); - testObject.onChange(target); + disposableStore.add(testObject.onChange(target)); return testObject.setEnablement(testObject.local[0], EnablementState.DisabledGlobally) .then(() => assert.ok(target.calledOnce)); @@ -1011,7 +1004,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [localExtension]); testObject = await aWorkbenchService(); const target = sinon.spy(); - testObject.onChange(target); + disposableStore.add(testObject.onChange(target)); return instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([localExtension], EnablementState.DisabledGlobally) .then(() => assert.ok(target.calledOnce)); @@ -1071,7 +1064,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposableStore.add(new TestExtensionEnablementService(instantiationService))); testObject = await aWorkbenchService(); const actual = await testObject.queryLocal(); @@ -1087,7 +1080,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposableStore.add(new TestExtensionEnablementService(instantiationService))); testObject = await aWorkbenchService(); const actual = await testObject.queryLocal(); @@ -1103,7 +1096,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposableStore.add(new TestExtensionEnablementService(instantiationService))); testObject = await aWorkbenchService(); const actual = await testObject.queryLocal(); @@ -1119,7 +1112,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposableStore.add(new TestExtensionEnablementService(instantiationService))); testObject = await aWorkbenchService(); const actual = await testObject.queryLocal(); @@ -1135,7 +1128,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposableStore.add(new TestExtensionEnablementService(instantiationService))); testObject = await aWorkbenchService(); const actual = await testObject.queryLocal(); @@ -1151,7 +1144,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposableStore.add(new TestExtensionEnablementService(instantiationService))); testObject = await aWorkbenchService(); const actual = await testObject.queryLocal(); @@ -1167,7 +1160,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposableStore.add(new TestExtensionEnablementService(instantiationService))); testObject = await aWorkbenchService(); const actual = await testObject.queryLocal(); @@ -1183,7 +1176,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposableStore.add(new TestExtensionEnablementService(instantiationService))); testObject = await aWorkbenchService(); const actual = await testObject.queryLocal(); @@ -1199,7 +1192,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposableStore.add(new TestExtensionEnablementService(instantiationService))); testObject = await aWorkbenchService(); const actual = await testObject.queryLocal(); @@ -1215,7 +1208,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposableStore.add(new TestExtensionEnablementService(instantiationService))); testObject = await aWorkbenchService(); const actual = await testObject.queryLocal(); @@ -1231,7 +1224,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposableStore.add(new TestExtensionEnablementService(instantiationService))); testObject = await aWorkbenchService(); const actual = await testObject.queryLocal(); @@ -1248,7 +1241,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([remoteExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposableStore.add(new TestExtensionEnablementService(instantiationService))); testObject = await aWorkbenchService(); const actual = await testObject.queryLocal(); @@ -1265,7 +1258,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([remoteExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposableStore.add(new TestExtensionEnablementService(instantiationService))); testObject = await aWorkbenchService(); const actual = await testObject.queryLocal(); @@ -1281,7 +1274,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService(), createExtensionManagementService([remoteExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposableStore.add(new TestExtensionEnablementService(instantiationService))); testObject = await aWorkbenchService(); const actual = await testObject.queryLocal(); @@ -1298,7 +1291,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([remoteExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposableStore.add(new TestExtensionEnablementService(instantiationService))); testObject = await aWorkbenchService(); const actual = await testObject.queryLocal(); @@ -1315,7 +1308,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([remoteExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposableStore.add(new TestExtensionEnablementService(instantiationService))); await instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([remoteExtension], EnablementState.DisabledGlobally); testObject = await aWorkbenchService(); @@ -1334,7 +1327,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([remoteExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposableStore.add(new TestExtensionEnablementService(instantiationService))); await instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([remoteExtension], EnablementState.DisabledWorkspace); testObject = await aWorkbenchService(); @@ -1353,7 +1346,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([remoteExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposableStore.add(new TestExtensionEnablementService(instantiationService))); await instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([localExtension], EnablementState.DisabledGlobally); testObject = await aWorkbenchService(); @@ -1372,7 +1365,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([remoteExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposableStore.add(new TestExtensionEnablementService(instantiationService))); await instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([localExtension], EnablementState.DisabledWorkspace); testObject = await aWorkbenchService(); @@ -1391,7 +1384,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([remoteExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposableStore.add(new TestExtensionEnablementService(instantiationService))); testObject = await aWorkbenchService(); const actual = await testObject.queryLocal(); @@ -1407,7 +1400,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([]), createExtensionManagementService([remoteExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); - instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + instantiationService.stub(IWorkbenchExtensionEnablementService, disposableStore.add(new TestExtensionEnablementService(instantiationService))); testObject = await aWorkbenchService(); const actual = await testObject.queryLocal(); @@ -1417,7 +1410,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { }); async function aWorkbenchService(): Promise { - const workbenchService: ExtensionsWorkbenchService = testDisposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); + const workbenchService: ExtensionsWorkbenchService = disposableStore.add(instantiationService.createInstance(ExtensionsWorkbenchService)); await workbenchService.queryLocal(); return workbenchService; } @@ -1458,17 +1451,6 @@ suite('ExtensionsWorkbenchServiceTest', () => { return { firstPage: objects, total: objects.length, pageSize: objects.length, getPage: () => null! }; } - function eventToPromise(event: Event, count: number = 1): Promise { - return new Promise(c => { - let counter = 0; - event(() => { - if (++counter === count) { - c(undefined); - } - }); - }); - } - function aMultiExtensionManagementServerService(instantiationService: TestInstantiationService, localExtensionManagementService?: IProfileAwareExtensionManagementService, remoteExtensionManagementService?: IProfileAwareExtensionManagementService): IExtensionManagementServerService { const localExtensionManagementServer: IExtensionManagementServer = { id: 'vscode-local', diff --git a/src/vs/workbench/contrib/files/common/files.ts b/src/vs/workbench/contrib/files/common/files.ts index 145f0670ea8..c525fb6bb73 100644 --- a/src/vs/workbench/contrib/files/common/files.ts +++ b/src/vs/workbench/contrib/files/common/files.ts @@ -10,7 +10,7 @@ import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { IFilesConfiguration as PlatformIFilesConfiguration, FileChangeType, IFileService } from 'vs/platform/files/common/files'; import { ContextKeyExpr, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { ITextModelContentProvider } from 'vs/editor/common/services/resolverService'; -import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { ITextModel } from 'vs/editor/common/model'; import { IModelService } from 'vs/editor/common/services/model'; import { ILanguageService, ILanguageSelection } from 'vs/editor/common/languages/language'; @@ -171,6 +171,7 @@ export class TextFileContentProvider extends Disposable implements ITextModelCon private static textFileToResource(resource: URI): URI { const { scheme, query } = JSON.parse(resource.query); + return resource.with({ scheme, query }); } @@ -188,14 +189,16 @@ export class TextFileContentProvider extends Disposable implements ITextModelCon // Make sure to keep contents up to date when it changes if (!this.fileWatcherDisposable.value) { - this.fileWatcherDisposable.value = this.fileService.onDidFilesChange(changes => { + const disposables = new DisposableStore(); + this.fileWatcherDisposable.value = disposables; + disposables.add(this.fileService.onDidFilesChange(changes => { if (changes.contains(savedFileResource, FileChangeType.UPDATED)) { this.resolveEditorModel(resource, false /* do not create if missing */); // update model when resource changes } - }); + })); if (codeEditorModel) { - once(codeEditorModel.onWillDispose)(() => this.fileWatcherDisposable.clear()); + disposables.add(once(codeEditorModel.onWillDispose)(() => this.fileWatcherDisposable.clear())); } } diff --git a/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts b/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts index 87691ff1fa3..1f89d3dc33c 100644 --- a/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts @@ -5,10 +5,10 @@ import * as assert from 'assert'; import { Event } from 'vs/base/common/event'; -import { toResource } from 'vs/base/test/common/utils'; +import { ensureNoDisposablesAreLeakedInTestSuite, toResource } from 'vs/base/test/common/utils'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { TestFilesConfigurationService, workbenchInstantiationService, TestServiceAccessor, registerTestFileEditor, createEditorPart, TestEnvironmentService, TestFileService } from 'vs/workbench/test/browser/workbenchTestServices'; -import { IResolvedTextFileEditorModel, ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; +import { ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; @@ -43,19 +43,19 @@ suite('EditorAutoSave', () => { configurationService.setUserConfiguration('files', autoSaveConfig); instantiationService.stub(IConfigurationService, configurationService); - instantiationService.stub(IFilesConfigurationService, new TestFilesConfigurationService( + instantiationService.stub(IFilesConfigurationService, disposables.add(new TestFilesConfigurationService( instantiationService.createInstance(MockContextKeyService), configurationService, new TestContextService(TestWorkspace), TestEnvironmentService, - new UriIdentityService(new TestFileService()), - new TestFileService() - )); + disposables.add(new UriIdentityService(disposables.add(new TestFileService()))), + disposables.add(new TestFileService()) + ))); const part = await createEditorPart(instantiationService, disposables); instantiationService.stub(IEditorGroupsService, part); - const editorService: EditorService = instantiationService.createInstance(EditorService); + const editorService: EditorService = disposables.add(instantiationService.createInstance(EditorService)); instantiationService.stub(IEditorService, editorService); const accessor = instantiationService.createInstance(TestServiceAccessor); @@ -71,14 +71,14 @@ suite('EditorAutoSave', () => { const resource = toResource.call(this, '/path/index.txt'); - const model = await accessor.textFileService.files.resolve(resource) as IResolvedTextFileEditorModel; - model.textEditorModel.setValue('Super Good'); + const model: ITextFileEditorModel = disposables.add(await accessor.textFileService.files.resolve(resource)); + model.textEditorModel?.setValue('Super Good'); assert.ok(model.isDirty()); await awaitModelSaved(model); - assert.ok(!model.isDirty()); + assert.strictEqual(model.isDirty(), false); }); test('editor auto saves on focus change if configured', async function () { @@ -87,19 +87,23 @@ suite('EditorAutoSave', () => { const resource = toResource.call(this, '/path/index.txt'); await accessor.editorService.openEditor({ resource, options: { override: DEFAULT_EDITOR_ASSOCIATION.id } }); - const model = await accessor.textFileService.files.resolve(resource) as IResolvedTextFileEditorModel; - model.textEditorModel.setValue('Super Good'); + const model: ITextFileEditorModel = disposables.add(await accessor.textFileService.files.resolve(resource)); + model.textEditorModel?.setValue('Super Good'); assert.ok(model.isDirty()); - await accessor.editorService.openEditor({ resource: toResource.call(this, '/path/index_other.txt') }); + const editorPane = await accessor.editorService.openEditor({ resource: toResource.call(this, '/path/index_other.txt') }); await awaitModelSaved(model); - assert.ok(!model.isDirty()); + assert.strictEqual(model.isDirty(), false); + + await editorPane?.group?.closeAllEditors(); }); function awaitModelSaved(model: ITextFileEditorModel): Promise { return Event.toPromise(Event.once(model.onDidChangeDirty)); } + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts b/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts index d66d44e14bb..ef6adefcdd2 100644 --- a/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { URI } from 'vs/base/common/uri'; -import { toResource } from 'vs/base/test/common/utils'; +import { ensureNoDisposablesAreLeakedInTestSuite, toResource } from 'vs/base/test/common/utils'; import { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/fileEditorInput'; import { workbenchInstantiationService, TestServiceAccessor, getLastResolvedFileStat } from 'vs/workbench/test/browser/workbenchTestServices'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -25,12 +25,12 @@ import { TextEditorService } from 'vs/workbench/services/textfile/common/textEdi suite('Files - FileEditorInput', () => { - let disposables: DisposableStore; + const disposables = new DisposableStore(); let instantiationService: IInstantiationService; let accessor: TestServiceAccessor; function createFileInput(resource: URI, preferredResource?: URI, preferredLanguageId?: string, preferredName?: string, preferredDescription?: string, preferredContents?: string): FileEditorInput { - return instantiationService.createInstance(FileEditorInput, resource, preferredResource, preferredName, preferredDescription, undefined, preferredLanguageId, preferredContents); + return disposables.add(instantiationService.createInstance(FileEditorInput, resource, preferredResource, preferredName, preferredDescription, undefined, preferredLanguageId, preferredContents)); } class TestTextEditorService extends TextEditorService { @@ -44,7 +44,6 @@ suite('Files - FileEditorInput', () => { } setup(() => { - disposables = new DisposableStore(); instantiationService = workbenchInstantiationService({ textEditorService: instantiationService => instantiationService.createInstance(TestTextEditorService) }, disposables); @@ -53,7 +52,7 @@ suite('Files - FileEditorInput', () => { }); teardown(() => { - disposables.dispose(); + disposables.clear(); }); test('Basics', async function () { @@ -128,20 +127,15 @@ suite('Files - FileEditorInput', () => { }); test('reports as readonly with readonly file scheme', async function () { - - const inMemoryFilesystemProvider = new InMemoryFileSystemProvider(); + const inMemoryFilesystemProvider = disposables.add(new InMemoryFileSystemProvider()); inMemoryFilesystemProvider.setReadOnly(true); - const disposable = accessor.fileService.registerProvider('someTestingReadonlyScheme', inMemoryFilesystemProvider); - try { - const input = createFileInput(toResource.call(this, '/foo/bar/file.js').with({ scheme: 'someTestingReadonlyScheme' })); + disposables.add(accessor.fileService.registerProvider('someTestingReadonlyScheme', inMemoryFilesystemProvider)); + const input = createFileInput(toResource.call(this, '/foo/bar/file.js').with({ scheme: 'someTestingReadonlyScheme' })); - assert.ok(!input.hasCapability(EditorInputCapabilities.Untitled)); - assert.ok(input.hasCapability(EditorInputCapabilities.Readonly)); - assert.ok(input.isReadonly()); - } finally { - disposable.dispose(); - } + assert.ok(!input.hasCapability(EditorInputCapabilities.Untitled)); + assert.ok(input.hasCapability(EditorInputCapabilities.Readonly)); + assert.ok(input.isReadonly()); }); test('preferred resource', function () { @@ -158,9 +152,9 @@ suite('Files - FileEditorInput', () => { assert.strictEqual(inputWithPreferredResource.preferredResource.toString(), preferredResource.toString()); let didChangeLabel = false; - const listener = inputWithPreferredResource.onDidChangeLabel(e => { + disposables.add(inputWithPreferredResource.onDidChangeLabel(e => { didChangeLabel = true; - }); + })); assert.strictEqual(inputWithPreferredResource.getName(), 'UPDATEFILE.js'); @@ -171,20 +165,18 @@ suite('Files - FileEditorInput', () => { assert.strictEqual(inputWithPreferredResource.preferredResource.toString(), otherPreferredResource.toString()); assert.strictEqual(inputWithPreferredResource.getName(), 'updateFILE.js'); assert.strictEqual(didChangeLabel, true); - - listener.dispose(); }); test('preferred language', async function () { const languageId = 'file-input-test'; - const registration = accessor.languageService.registerLanguage({ + disposables.add(accessor.languageService.registerLanguage({ id: languageId, - }); + })); const input = createFileInput(toResource.call(this, '/foo/bar/file.js'), undefined, languageId); assert.strictEqual(input.getPreferredLanguageId(), languageId); - const model = await input.resolve() as TextFileEditorModel; + const model = disposables.add(await input.resolve() as TextFileEditorModel); assert.strictEqual(model.textEditorModel!.getLanguageId(), languageId); input.setLanguageId('text'); @@ -194,16 +186,14 @@ suite('Files - FileEditorInput', () => { const input2 = createFileInput(toResource.call(this, '/foo/bar/file.js')); input2.setPreferredLanguageId(languageId); - const model2 = await input2.resolve() as TextFileEditorModel; + const model2 = disposables.add(await input2.resolve() as TextFileEditorModel); assert.strictEqual(model2.textEditorModel!.getLanguageId(), languageId); - - registration.dispose(); }); test('preferred contents', async function () { const input = createFileInput(toResource.call(this, '/foo/bar/file.js'), undefined, undefined, undefined, undefined, 'My contents'); - const model = await input.resolve() as TextFileEditorModel; + const model = disposables.add(await input.resolve() as TextFileEditorModel); assert.strictEqual(model.textEditorModel!.getValue(), 'My contents'); assert.strictEqual(input.isDirty(), true); @@ -248,15 +238,14 @@ suite('Files - FileEditorInput', () => { await input.setEncoding('utf16', EncodingMode.Encode); assert.strictEqual(input.getEncoding(), 'utf16'); - const resolved = await input.resolve() as TextFileEditorModel; + const resolved = disposables.add(await input.resolve() as TextFileEditorModel); assert.strictEqual(input.getEncoding(), resolved.getEncoding()); - resolved.dispose(); }); test('save', async function () { const input = createFileInput(toResource.call(this, '/foo/bar/updatefile.js')); - const resolved = await input.resolve() as TextFileEditorModel; + const resolved = disposables.add(await input.resolve() as TextFileEditorModel); resolved.textEditorModel!.setValue('changed'); assert.ok(input.isDirty()); assert.ok(input.isModified()); @@ -264,13 +253,12 @@ suite('Files - FileEditorInput', () => { await input.save(0); assert.ok(!input.isDirty()); assert.ok(!input.isModified()); - resolved.dispose(); }); test('revert', async function () { const input = createFileInput(toResource.call(this, '/foo/bar/updatefile.js')); - const resolved = await input.resolve() as TextFileEditorModel; + const resolved = disposables.add(await input.resolve() as TextFileEditorModel); resolved.textEditorModel!.setValue('changed'); assert.ok(input.isDirty()); assert.ok(input.isModified()); @@ -281,8 +269,6 @@ suite('Files - FileEditorInput', () => { input.dispose(); assert.ok(input.isDisposed()); - - resolved.dispose(); }); test('resolve handles binary files', async function () { @@ -290,9 +276,8 @@ suite('Files - FileEditorInput', () => { accessor.textFileService.setReadStreamErrorOnce(new TextFileOperationError('error', TextFileOperationResult.FILE_IS_BINARY)); - const resolved = await input.resolve(); + const resolved = disposables.add(await input.resolve()); assert.ok(resolved); - resolved.dispose(); }); test('resolve throws for too large files', async function () { @@ -306,42 +291,36 @@ suite('Files - FileEditorInput', () => { e = error; } assert.ok(e); - input.dispose(); }); test('attaches to model when created and reports dirty', async function () { const input = createFileInput(toResource.call(this, '/foo/bar/updatefile.js')); let listenerCount = 0; - const listener = input.onDidChangeDirty(() => { + disposables.add(input.onDidChangeDirty(() => { listenerCount++; - }); + })); // instead of going through file input resolve method // we resolve the model directly through the service - const model = await accessor.textFileService.files.resolve(input.resource); + const model = disposables.add(await accessor.textFileService.files.resolve(input.resource)); model.textEditorModel?.setValue('hello world'); assert.strictEqual(listenerCount, 1); assert.ok(input.isDirty()); - - input.dispose(); - listener.dispose(); }); test('force open text/binary', async function () { const input = createFileInput(toResource.call(this, '/foo/bar/updatefile.js')); input.setForceOpenAsBinary(); - let resolved = await input.resolve(); + let resolved = disposables.add(await input.resolve()); assert.ok(resolved instanceof BinaryEditorModel); input.setForceOpenAsText(); - resolved = await input.resolve(); + resolved = disposables.add(await input.resolve()); assert.ok(resolved instanceof TextFileEditorModel); - - resolved.dispose(); }); test('file editor serializer', async function () { @@ -349,7 +328,7 @@ suite('Files - FileEditorInput', () => { const input = createFileInput(toResource.call(this, '/foo/bar/updatefile.js')); - const disposable = Registry.as(EditorExtensions.EditorFactory).registerEditorSerializer('workbench.editors.files.fileEditorInput', FileEditorInputSerializer); + disposables.add(Registry.as(EditorExtensions.EditorFactory).registerEditorSerializer('workbench.editors.files.fileEditorInput', FileEditorInputSerializer)); const editorSerializer = Registry.as(EditorExtensions.EditorFactory).getEditorSerializer(input.typeId); if (!editorSerializer) { @@ -377,8 +356,6 @@ suite('Files - FileEditorInput', () => { const inputWithPreferredResourceDeserialized = editorSerializer.deserialize(instantiationService, inputWithPreferredResourceSerialized) as FileEditorInput; assert.strictEqual(inputWithPreferredResource.resource.toString(), inputWithPreferredResourceDeserialized.resource.toString()); assert.strictEqual(inputWithPreferredResource.preferredResource.toString(), inputWithPreferredResourceDeserialized.preferredResource.toString()); - - disposable.dispose(); }); test('preferred name/description', async function () { @@ -387,9 +364,9 @@ suite('Files - FileEditorInput', () => { const customFileInput = createFileInput(toResource.call(this, '/foo/bar/updatefile.js').with({ scheme: 'test-custom' }), undefined, undefined, 'My Name', 'My Description'); let didChangeLabelCounter = 0; - customFileInput.onDidChangeLabel(() => { + disposables.add(customFileInput.onDidChangeLabel(() => { didChangeLabelCounter++; - }); + })); assert.strictEqual(customFileInput.getName(), 'My Name'); assert.strictEqual(customFileInput.getDescription(), 'My Description'); @@ -408,9 +385,9 @@ suite('Files - FileEditorInput', () => { const fileInput = createFileInput(toResource.call(this, '/foo/bar/updatefile.js'), undefined, undefined, 'My Name', 'My Description'); didChangeLabelCounter = 0; - fileInput.onDidChangeLabel(() => { + disposables.add(fileInput.onDidChangeLabel(() => { didChangeLabelCounter++; - }); + })); assert.notStrictEqual(fileInput.getName(), 'My Name'); assert.notStrictEqual(fileInput.getDescription(), 'My Description'); @@ -422,19 +399,17 @@ suite('Files - FileEditorInput', () => { assert.notStrictEqual(fileInput.getDescription(), 'My Description 2'); assert.strictEqual(didChangeLabelCounter, 0); - - fileInput.dispose(); }); test('reports readonly changes', async function () { const input = createFileInput(toResource.call(this, '/foo/bar/updatefile.js')); let listenerCount = 0; - const listener = input.onDidChangeCapabilities(() => { + disposables.add(input.onDidChangeCapabilities(() => { listenerCount++; - }); + })); - const model = await accessor.textFileService.files.resolve(input.resource); + const model = disposables.add(await accessor.textFileService.files.resolve(input.resource)); assert.strictEqual(model.isReadonly(), false); assert.strictEqual(input.hasCapability(EditorInputCapabilities.Readonly), false); @@ -465,8 +440,7 @@ suite('Files - FileEditorInput', () => { assert.strictEqual(input.hasCapability(EditorInputCapabilities.Readonly), false); assert.strictEqual(input.isReadonly(), false); assert.strictEqual(listenerCount, 2); - - input.dispose(); - listener.dispose(); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/contrib/files/test/browser/fileOnDiskProvider.test.ts b/src/vs/workbench/contrib/files/test/browser/fileOnDiskProvider.test.ts index 9b84f11c8df..fb5b9a9286b 100644 --- a/src/vs/workbench/contrib/files/test/browser/fileOnDiskProvider.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/fileOnDiskProvider.test.ts @@ -10,25 +10,25 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { TextFileContentProvider } from 'vs/workbench/contrib/files/common/files'; import { snapshotToString } from 'vs/workbench/services/textfile/common/textfiles'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('Files - FileOnDiskContentProvider', () => { - let disposables: DisposableStore; + const disposables = new DisposableStore(); let instantiationService: IInstantiationService; let accessor: TestServiceAccessor; setup(() => { - disposables = new DisposableStore(); instantiationService = workbenchInstantiationService(undefined, disposables); accessor = instantiationService.createInstance(TestServiceAccessor); }); teardown(() => { - disposables.dispose(); + disposables.clear(); }); test('provideTextContent', async () => { - const provider = instantiationService.createInstance(TextFileContentProvider); + const provider = disposables.add(instantiationService.createInstance(TextFileContentProvider)); const uri = URI.parse('testFileOnDiskContentProvider://foo'); const content = await provider.provideTextContent(uri.with({ scheme: 'conflictResolution', query: JSON.stringify({ scheme: uri.scheme }) })); @@ -37,5 +37,9 @@ suite('Files - FileOnDiskContentProvider', () => { assert.strictEqual(snapshotToString(content!.createSnapshot()), 'Hello Html'); assert.strictEqual(accessor.fileService.getLastReadFileUri().scheme, uri.scheme); assert.strictEqual(accessor.fileService.getLastReadFileUri().path, uri.path); + + content.dispose(); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts b/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts index b0afe7cc554..ff9eb5b4383 100644 --- a/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts @@ -6,9 +6,9 @@ import * as assert from 'assert'; import { Event } from 'vs/base/common/event'; import { TextFileEditorTracker } from 'vs/workbench/contrib/files/browser/editors/textFileEditorTracker'; -import { toResource } from 'vs/base/test/common/utils'; +import { ensureNoDisposablesAreLeakedInTestSuite, toResource } from 'vs/base/test/common/utils'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { workbenchInstantiationService, TestServiceAccessor, TestFilesConfigurationService, registerTestFileEditor, registerTestResourceEditor, createEditorPart, TestEnvironmentService, TestFileService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { workbenchInstantiationService, TestServiceAccessor, TestFilesConfigurationService, registerTestFileEditor, registerTestResourceEditor, createEditorPart, TestEnvironmentService, TestFileService, workbenchTeardown } from 'vs/workbench/test/browser/workbenchTestServices'; import { IResolvedTextFileEditorModel, snapshotToString, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { FileChangesEvent, FileChangeType, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -25,8 +25,6 @@ import { IFilesConfigurationService } from 'vs/workbench/services/filesConfigura import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { FILE_EDITOR_INPUT_ID } from 'vs/workbench/contrib/files/common/files'; -import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; -import { TestWorkspaceTrustRequestService } from 'vs/workbench/services/workspaces/test/common/testWorkspaceTrustService'; import { DEFAULT_EDITOR_ASSOCIATION } from 'vs/workbench/common/editor'; import { TestWorkspace } from 'vs/platform/workspace/test/common/testWorkspace'; import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; @@ -52,7 +50,7 @@ suite('Files - TextFileEditorTracker', () => { disposables.clear(); }); - async function createTracker(autoSaveEnabled = false): Promise { + async function createTracker(autoSaveEnabled = false): Promise<{ accessor: TestServiceAccessor; cleanup: () => Promise }> { const instantiationService = workbenchInstantiationService(undefined, disposables); if (autoSaveEnabled) { @@ -61,22 +59,22 @@ suite('Files - TextFileEditorTracker', () => { instantiationService.stub(IConfigurationService, configurationService); - instantiationService.stub(IFilesConfigurationService, new TestFilesConfigurationService( + const fileService = disposables.add(new TestFileService()); + + instantiationService.stub(IFilesConfigurationService, disposables.add(new TestFilesConfigurationService( instantiationService.createInstance(MockContextKeyService), configurationService, new TestContextService(TestWorkspace), TestEnvironmentService, - new UriIdentityService(new TestFileService()), - new TestFileService() - )); + disposables.add(new UriIdentityService(fileService)), + fileService + ))); } const part = await createEditorPart(instantiationService, disposables); instantiationService.stub(IEditorGroupsService, part); - instantiationService.stub(IWorkspaceTrustRequestService, new TestWorkspaceTrustRequestService(false)); - - const editorService: EditorService = instantiationService.createInstance(EditorService); + const editorService: EditorService = disposables.add(instantiationService.createInstance(EditorService)); disposables.add(editorService); instantiationService.stub(IEditorService, editorService); @@ -85,11 +83,16 @@ suite('Files - TextFileEditorTracker', () => { disposables.add(instantiationService.createInstance(TestTextFileEditorTracker)); - return accessor; + const cleanup = async () => { + await workbenchTeardown(instantiationService); + part.dispose(); + }; + + return { accessor, cleanup }; } test('file change event updates model', async function () { - const accessor = await createTracker(); + const { accessor, cleanup } = await createTracker(); const resource = toResource.call(this, '/path/index.txt'); @@ -107,6 +110,8 @@ suite('Files - TextFileEditorTracker', () => { await timeout(0); // due to event updating model async assert.strictEqual(snapshotToString(model.createSnapshot()!), 'Hello Html'); + + await cleanup(); }); test('dirty text file model opens as editor', async function () { @@ -134,7 +139,7 @@ suite('Files - TextFileEditorTracker', () => { }); async function testDirtyTextFileModelOpensEditorDependingOnAutoSaveSetting(resource: URI, autoSave: boolean, error: boolean): Promise { - const accessor = await createTracker(autoSave); + const { accessor, cleanup } = await createTracker(autoSave); assert.ok(!accessor.editorService.isOpened({ resource, typeId: FILE_EDITOR_INPUT_ID, editorId: DEFAULT_EDITOR_ASSOCIATION.id })); @@ -159,6 +164,8 @@ suite('Files - TextFileEditorTracker', () => { await awaitEditorOpening(accessor.editorService); assert.ok(accessor.editorService.isOpened({ resource, typeId: FILE_EDITOR_INPUT_ID, editorId: DEFAULT_EDITOR_ASSOCIATION.id })); } + + await cleanup(); } test('dirty untitled text file model opens as editor', function () { @@ -170,7 +177,7 @@ suite('Files - TextFileEditorTracker', () => { }); async function testUntitledEditor(autoSaveEnabled: boolean): Promise { - const accessor = await createTracker(autoSaveEnabled); + const { accessor, cleanup } = await createTracker(autoSaveEnabled); const untitledTextEditor = await accessor.textEditorService.resolveTextEditor({ resource: undefined, forceUntitled: true }) as UntitledTextEditorInput; const model = disposables.add(await untitledTextEditor.resolve()); @@ -181,6 +188,8 @@ suite('Files - TextFileEditorTracker', () => { await awaitEditorOpening(accessor.editorService); assert.ok(accessor.editorService.isOpened(untitledTextEditor)); + + await cleanup(); } function awaitEditorOpening(editorService: IEditorService): Promise { @@ -188,7 +197,7 @@ suite('Files - TextFileEditorTracker', () => { } test('non-dirty files reload on window focus', async function () { - const accessor = await createTracker(); + const { accessor, cleanup } = await createTracker(); const resource = toResource.call(this, '/path/index.txt'); @@ -198,6 +207,8 @@ suite('Files - TextFileEditorTracker', () => { accessor.hostService.setFocus(true); await awaitModelResolveEvent(accessor.textFileService, resource); + + await cleanup(); }); function awaitModelResolveEvent(textFileService: ITextFileService, resource: URI): Promise { @@ -210,4 +221,6 @@ suite('Files - TextFileEditorTracker', () => { }); }); } + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index c90d70f10e9..b792b83f8ea 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -10,7 +10,7 @@ import { EditorAction2 } from 'vs/editor/browser/editorExtensions'; import { EmbeddedCodeEditorWidget, EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { InlineChatController, InlineChatRunOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; -import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST, CTX_INLINE_CHAT_HAS_PROVIDER, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_VISIBLE, MENU_INLINE_CHAT_WIDGET, MENU_INLINE_CHAT_WIDGET_DISCARD, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_LAST_FEEDBACK, CTX_INLINE_CHAT_EDIT_MODE, EditMode, CTX_INLINE_CHAT_LAST_RESPONSE_TYPE, MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE, CTX_INLINE_CHAT_MESSAGE_CROP_STATE, CTX_INLINE_CHAT_DOCUMENT_CHANGED, CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, MENU_INLINE_CHAT_WIDGET_FEEDBACK, ACTION_ACCEPT_CHANGES, ACTION_REGENERATE_RESPONSE, InlineChatResponseType, CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChateResponseTypes, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_USER_DID_EDIT, MENU_INLINE_CHAT_WIDGET_TOGGLE, CTX_INLINE_CHAT_INNER_CURSOR_START, CTX_INLINE_CHAT_INNER_CURSOR_END } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST, CTX_INLINE_CHAT_HAS_PROVIDER, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_VISIBLE, MENU_INLINE_CHAT_WIDGET, MENU_INLINE_CHAT_WIDGET_DISCARD, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_LAST_FEEDBACK, CTX_INLINE_CHAT_EDIT_MODE, EditMode, CTX_INLINE_CHAT_LAST_RESPONSE_TYPE, MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE, CTX_INLINE_CHAT_MESSAGE_CROP_STATE, CTX_INLINE_CHAT_DOCUMENT_CHANGED, CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, MENU_INLINE_CHAT_WIDGET_FEEDBACK, ACTION_ACCEPT_CHANGES, ACTION_REGENERATE_RESPONSE, InlineChatResponseType, CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChateResponseTypes, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_USER_DID_EDIT, MENU_INLINE_CHAT_WIDGET_TOGGLE, CTX_INLINE_CHAT_INNER_CURSOR_START, CTX_INLINE_CHAT_INNER_CURSOR_END, CTX_INLINE_CHAT_RESPONSE_FOCUSED } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { localize } from 'vs/nls'; import { IAction2Options, MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; @@ -643,6 +643,6 @@ export class InlineAccessibilityHelpContribution extends Disposable { return; } runAccessibilityHelpAction(accessor, codeEditor, 'inlineChat'); - }, CTX_INLINE_CHAT_FOCUSED)); + }, ContextKeyExpr.or(CTX_INLINE_CHAT_RESPONSE_FOCUSED, CTX_INLINE_CHAT_FOCUSED))); } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 1d8cacb2985..398a1482be0 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -151,7 +151,9 @@ export class InlineChatController implements IEditorContribution { dispose(): void { this._strategy?.dispose(); this._stashedSession.clear(); - this.finishExistingSession(); + if (this._activeSession) { + this._inlineChatSessionService.releaseSession(this._activeSession); + } this._store.dispose(); this._log('controller disposed'); } @@ -543,6 +545,7 @@ export class InlineChatController implements IEditorContribution { this._ctxHasActiveRequest.set(false); this._zone.value.widget.updateProgress(false); this._zone.value.widget.updateInfo(''); + this._zone.value.widget.updateToolbar(true); this._log('request took', sw.elapsed(), this._activeSession.provider.debugName); } @@ -662,7 +665,6 @@ export class InlineChatController implements IEditorContribution { const renderedMarkdown = renderMarkdown(response.raw.message, { inline: true }); this._zone.value.widget.updateStatus(''); this._zone.value.widget.updateMarkdownMessage(renderedMarkdown.element); - this._zone.value.widget.updateToolbar(true); const content = renderedMarkdown.element.textContent; if (content) { status = localize('markdownResponseMessage', "{0}", content); @@ -672,7 +674,6 @@ export class InlineChatController implements IEditorContribution { } else if (response instanceof EditResponse) { // edit response -> complex... this._zone.value.widget.updateMarkdownMessage(undefined); - this._zone.value.widget.updateToolbar(true); const canContinue = this._strategy.checkChanges(response); if (!canContinue) { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatLivePreviewWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatLivePreviewWidget.ts index 0f153ca64da..2d2a2a7a5bb 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatLivePreviewWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatLivePreviewWidget.ts @@ -170,9 +170,15 @@ export class InlineChatLivePreviewWidget extends ZoneWidget { return; } - // complex changes - this._logService.debug('[IE] livePreview-mode: full diff'); - this._renderChangesWithFullDiff(changes, range); + if (changes.length === 0 || this._session.textModel0.getValueLength() === 0) { + // no change or changes to an empty file + this._logService.debug('[IE] livePreview-mode: no diff'); + this._cleanupFullDiff(); + } else { + // complex changes + this._logService.debug('[IE] livePreview-mode: full diff'); + this._renderChangesWithFullDiff(changes, range); + } } // --- full diff diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts index 76944f5b38e..5ffa49b8217 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts @@ -409,7 +409,7 @@ export class LivePreviewStrategy extends LiveStrategy { } if (response.singleCreateFileEdit) { - this._previewZone.value.showCreation(this._session.wholeRange.value, response.singleCreateFileEdit.uri, await Promise.all(response.singleCreateFileEdit.edits)); + this._previewZone.value.showCreation(this._session.wholeRange.value.collapseToEnd(), response.singleCreateFileEdit.uri, await Promise.all(response.singleCreateFileEdit.edits)); } else { this._previewZone.value.hide(); } diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts index e31040fecc5..806f24b91e4 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts @@ -8,7 +8,10 @@ import { equals } from 'vs/base/common/arrays'; import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { mock } from 'vs/base/test/common/mock'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { TestDiffProviderFactoryService } from 'vs/editor/browser/diff/testDiffProviderFactoryService'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { IDiffProviderFactoryService } from 'vs/editor/browser/widget/diffEditor/diffProviderFactoryService'; import { Range } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; import { IModelService } from 'vs/editor/common/services/model'; @@ -30,7 +33,6 @@ import { InlineChatServiceImpl } from 'vs/workbench/contrib/inlineChat/common/in import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; suite('InteractiveChatController', function () { - class TestController extends InlineChatController { static INIT_SEQUENCE: readonly State[] = [State.CREATE_SESSION, State.INIT_UI, State.WAIT_FOR_INPUT]; @@ -92,6 +94,7 @@ suite('InteractiveChatController', function () { const serviceCollection = new ServiceCollection( [IContextKeyService, contextKeyService], [IInlineChatService, inlineChatService], + [IDiffProviderFactoryService, new SyncDescriptor(TestDiffProviderFactoryService)], [IInlineChatSessionService, new SyncDescriptor(InlineChatSessionService)], [IEditorProgressService, new class extends mock() { override show(total: unknown, delay?: unknown): IProgressRunner { @@ -145,8 +148,7 @@ suite('InteractiveChatController', function () { ctrl?.dispose(); }); - // todo: re-enable this when earlier tests are fixed - // ensureNoDisposablesAreLeakedInTestSuite(); + ensureNoDisposablesAreLeakedInTestSuite(); test('creation, not showing anything', function () { ctrl = instaService.createInstance(TestController, editor); @@ -229,7 +231,7 @@ suite('InteractiveChatController', function () { test('typing outside of wholeRange finishes session', async function () { ctrl = instaService.createInstance(TestController, editor); const p = ctrl.waitFor(TestController.INIT_SEQUENCE_AUTO_SEND); - ctrl.run({ message: 'Hello', autoSend: true }); + const r = ctrl.run({ message: 'Hello', autoSend: true }); await p; @@ -241,6 +243,7 @@ suite('InteractiveChatController', function () { editor.trigger('test', 'type', { text: 'a' }); await ctrl.waitFor([State.ACCEPT]); + await r; }); test('\'whole range\' isn\'t updated for edits outside whole range #4346', async function () { @@ -270,7 +273,7 @@ suite('InteractiveChatController', function () { store.add(d); ctrl = instaService.createInstance(TestController, editor); const p = ctrl.waitFor(TestController.INIT_SEQUENCE); - ctrl.run({ message: 'Hello', autoSend: false }); + const r = ctrl.run({ message: 'Hello', autoSend: false }); await p; @@ -283,6 +286,9 @@ suite('InteractiveChatController', function () { await ctrl.waitFor([State.MAKE_REQUEST, State.APPLY_RESPONSE, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); assert.deepStrictEqual(session.wholeRange.value, new Range(1, 1, 4, 12)); + + ctrl.cancelSession(); + await r; }); test('Stuck inline chat widget #211', async function () { diff --git a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts index afa4414c3c0..90dbcd2f771 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts @@ -53,6 +53,7 @@ import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/note import * as icons from 'vs/workbench/contrib/notebook/browser/notebookIcons'; import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService'; import { CellEditType, CellKind, CellUri, INTERACTIVE_WINDOW_EDITOR_ID, NotebookWorkingCopyTypeIdentifier } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { InteractiveWindowOpen } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { columnToEditorGroup } from 'vs/workbench/services/editor/common/editorGroupColumn'; @@ -699,7 +700,11 @@ registerAction2(class extends Action2 { id: 'interactive.input.focus', title: { value: localize('interactive.input.focus', "Focus Input Editor"), original: 'Focus Input Editor' }, category: interactiveWindowCategory, - f1: true + menu: { + id: MenuId.CommandPalette, + when: InteractiveWindowOpen, + }, + precondition: InteractiveWindowOpen, }); } diff --git a/src/vs/workbench/contrib/mergeEditor/browser/model/textModelProjection.ts b/src/vs/workbench/contrib/mergeEditor/browser/model/textModelProjection.ts deleted file mode 100644 index 01ee19a1ff9..00000000000 --- a/src/vs/workbench/contrib/mergeEditor/browser/model/textModelProjection.ts +++ /dev/null @@ -1,157 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { ArrayQueue } from 'vs/base/common/arrays'; -import { BugIndicatingError } from 'vs/base/common/errors'; -import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; -import { URI } from 'vs/base/common/uri'; -import { Position } from 'vs/editor/common/core/position'; -import { ITextModel } from 'vs/editor/common/model'; -import { IModelService } from 'vs/editor/common/services/model'; -import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model/lineRange'; - -export class TextModelProjection extends Disposable { - private static counter: number = 0; - - public static create( - sourceDocument: ITextModel, - projectionConfiguration: ProjectionConfiguration, - modelService: IModelService - ): TextModelProjection { - const textModel = TextModelProjection.createModelReference( - modelService - ); - return new TextModelProjection(textModel, sourceDocument, { dispose: () => { } }, projectionConfiguration); - } - - public static createForTargetDocument( - sourceDocument: ITextModel, - projectionConfiguration: ProjectionConfiguration, - targetDocument: ITextModel, - ): TextModelProjection { - return new TextModelProjection(targetDocument, sourceDocument, new DisposableStore(), projectionConfiguration); - } - - private static createModelReference( - modelService: IModelService - ): ITextModel { - const uri = URI.from({ - scheme: 'projected-text-model', - path: `/projection${TextModelProjection.counter++}`, - }); - - return modelService.createModel('', null, uri, false); - } - - private currentBlocks: Block[]; - - constructor( - public readonly targetDocument: ITextModel, - private readonly sourceDocument: ITextModel, - disposable: IDisposable, - projectionConfiguration: ProjectionConfiguration - ) { - super(); - - this._register(disposable); - - const result = getBlocks(sourceDocument, projectionConfiguration); - this.currentBlocks = result.blocks; - targetDocument.setValue(result.transformedContent); - - this._register( - sourceDocument.onDidChangeContent((c) => { - // TODO improve this - const result = getBlocks(sourceDocument, projectionConfiguration); - this.currentBlocks = result.blocks; - targetDocument.setValue(result.transformedContent); - }) - ); - } - - /** - * The created transformer can only be called with monotonically increasing positions. - */ - createMonotonousReverseTransformer(): Transformer { - let lineDelta = 0; - const blockQueue = new ArrayQueue(this.currentBlocks); - let lastLineNumber = 0; - const sourceDocument = this.sourceDocument; - return { - transform(position) { - if (position.lineNumber < lastLineNumber) { - throw new BugIndicatingError(); - } - lastLineNumber = position.lineNumber; - - while (true) { - const next = blockQueue.peek(); - if (!next) { - break; - } - if (position.lineNumber + lineDelta > next.lineRange.startLineNumber) { - blockQueue.dequeue(); - lineDelta += next.lineRange.lineCount - 1; - } else if (position.lineNumber + lineDelta === next.lineRange.startLineNumber && position.column === 2) { - const targetLineNumber = position.lineNumber + lineDelta + next.lineRange.lineCount - 1; - return new Position(targetLineNumber, sourceDocument.getLineMaxColumn(targetLineNumber)); - } else { - break; - } - } - - // Column number never changes - return new Position(position.lineNumber + lineDelta, position.column); - }, - }; - } -} - -function getBlocks(document: ITextModel, configuration: ProjectionConfiguration): { blocks: Block[]; transformedContent: string } { - const blocks: Block[] = []; - const transformedContent: string[] = []; - - let inBlock = false; - let startLineNumber = -1; - let curLine = 0; - - for (const line of document.getLinesContent()) { - curLine++; - if (!inBlock) { - if (line.startsWith(configuration.blockToRemoveStartLinePrefix)) { - inBlock = true; - startLineNumber = curLine; - } else { - transformedContent.push(line); - } - } else { - if (line.startsWith(configuration.blockToRemoveEndLinePrefix)) { - inBlock = false; - blocks.push(new Block(new LineRange(startLineNumber, curLine - startLineNumber + 1))); - // We add a (hopefully) unique symbol so that diffing recognizes the deleted block (HEXAGRAM FOR CONFLICT) - // allow-any-unicode-next-line - transformedContent.push('䷅'); - } - } - } - - return { - blocks, - transformedContent: transformedContent.join('\n') - }; -} - -class Block { - constructor(public readonly lineRange: LineRange) { } -} - -interface ProjectionConfiguration { - blockToRemoveStartLinePrefix: string; - blockToRemoveEndLinePrefix: string; -} - -interface Transformer { - transform(position: Position): Position; -} diff --git a/src/vs/workbench/contrib/mergeEditor/test/browser/projection.test.ts b/src/vs/workbench/contrib/mergeEditor/test/browser/projection.test.ts deleted file mode 100644 index 1020861d913..00000000000 --- a/src/vs/workbench/contrib/mergeEditor/test/browser/projection.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as assert from 'assert'; -import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import { Position } from 'vs/editor/common/core/position'; -import { createTextModel } from 'vs/editor/test/common/testTextModel'; -import { TextModelProjection } from 'vs/workbench/contrib/mergeEditor/browser/model/textModelProjection'; - -suite('TextModelProjection', () => { - ensureNoDisposablesAreLeakedInTestSuite(); - - test('Basic', () => { - const source = createTextModel(` -1 this.container.appendChild(this.labelContainer); -2 -3 // Beak Container -4 this.beakContainer = document.createElement('div'); -<<<<<<< .\input1.ts -this.beakContainer.className = 'status-bar-item-beak-container'; -======= -this.beakContainer.className = 'status-bar-beak-container'; - -// Add to parent ->>>>>>> .\input2.ts -5 this.container.appendChild(this.beakContainer); -6 -7 this.update(entry); -`); - const target = createTextModel(''); - - const projection = TextModelProjection.createForTargetDocument(source, { blockToRemoveStartLinePrefix: '<<<<<<<', blockToRemoveEndLinePrefix: '>>>>>>>' }, target); - - assert.deepStrictEqual(target.getValue(), ` -1 this.container.appendChild(this.labelContainer); -2 -3 // Beak Container -4 this.beakContainer = document.createElement('div'); -䷅ -5 this.container.appendChild(this.beakContainer); -6 -7 this.update(entry); -`); - - const transformer = projection.createMonotonousReverseTransformer(); - const lineNumbers = target.getLinesContent().map((l, idx) => idx + 1); - const transformedLineNumbers = lineNumbers.map(n => transformer.transform(new Position(n, 1))); - - assert.deepStrictEqual(transformedLineNumbers.map(l => l.lineNumber), [ - 1, - 2, - 3, - 4, - 5, - 6, - 13, - 14, - 15, - 16, - ]); - - projection.dispose(); - source.dispose(); - target.dispose(); - }); -}); diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel.ts index 3e85f092688..0f31bdfc6ca 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel.ts @@ -84,7 +84,8 @@ export class DiffNestedCellViewModel extends Disposable implements IDiffNestedCe this._outputViewModels = this.textModel.outputs.map(output => new CellOutputViewModel(this, output, this._notebookService)); this._register(this.textModel.onDidChangeOutputs((splice) => { this._outputCollection.splice(splice.start, splice.deleteCount, ...splice.newOutputs.map(() => 0)); - this._outputViewModels.splice(splice.start, splice.deleteCount, ...splice.newOutputs.map(output => new CellOutputViewModel(this, output, this._notebookService))); + const removed = this._outputViewModels.splice(splice.start, splice.deleteCount, ...splice.newOutputs.map(output => new CellOutputViewModel(this, output, this._notebookService))); + removed.forEach(vm => vm.dispose()); this._outputsTop = null; this._onDidChangeOutputLayout.fire(); @@ -130,4 +131,12 @@ export class DiffNestedCellViewModel extends Disposable implements IDiffNestedCe return this._outputsTop?.getTotalSum() ?? 0; } + + public override dispose(): void { + super.dispose(); + + this._outputViewModels.forEach(output => { + output.dispose(); + }); + } } diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookEditorServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookEditorServiceImpl.ts index e61cfbe8181..e5baf7c6af3 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookEditorServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookEditorServiceImpl.ts @@ -12,9 +12,12 @@ import { isCompositeNotebookEditorInput, NotebookEditorInput } from 'vs/workbenc import { IBorrowValue, INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService'; import { INotebookEditor, INotebookEditorCreationOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { Emitter } from 'vs/base/common/event'; -import { GroupIdentifier } from 'vs/workbench/common/editor'; +import { GroupIdentifier, GroupModelChangeKind } from 'vs/workbench/common/editor'; import { Dimension } from 'vs/base/browser/dom'; import { URI } from 'vs/base/common/uri'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { InteractiveWindowOpen } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; export class NotebookEditorWidgetService implements INotebookEditorService { @@ -34,6 +37,8 @@ export class NotebookEditorWidgetService implements INotebookEditorService { constructor( @IEditorGroupsService editorGroupService: IEditorGroupsService, + @IEditorService editorService: IEditorService, + @IContextKeyService contextKeyService: IContextKeyService ) { const groupListener = new Map(); @@ -90,6 +95,19 @@ export class NotebookEditorWidgetService implements INotebookEditorService { } } })); + + const interactiveWindowOpen = InteractiveWindowOpen.bindTo(contextKeyService); + this._disposables.add(editorService.onDidEditorsChange(e => { + if (e.event.kind === GroupModelChangeKind.EDITOR_OPEN && !interactiveWindowOpen.get()) { + if (editorService.editors.find(editor => editor.editorId === 'interactive')) { + interactiveWindowOpen.set(true); + } + } else if (e.event.kind === GroupModelChangeKind.EDITOR_CLOSE && interactiveWindowOpen.get()) { + if (!editorService.editors.find(editor => editor.editorId === 'interactive')) { + interactiveWindowOpen.set(false); + } + } + })); } dispose() { diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookExecutionStateServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookExecutionStateServiceImpl.ts index 9ebea229e18..a5736966b8a 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookExecutionStateServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookExecutionStateServiceImpl.ts @@ -84,7 +84,7 @@ export class NotebookExecutionStateService extends Disposable implements INotebo getCellExecutionsByHandleForNotebook(notebook: URI): Map | undefined { const exeMap = this._executions.get(notebook); - return exeMap ?? undefined; + return exeMap ? new Map(exeMap.entries()) : undefined; } private _onCellExecutionDidChange(notebookUri: URI, cellHandle: number, exe: CellExecution): void { diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts index 0c82eb7cb7f..c01228ddc92 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts @@ -290,13 +290,13 @@ export class NotebookProviderInfoStore extends Disposable { mementoObject[NotebookProviderInfoStore.CUSTOM_EDITORS_ENTRY_ID] = Array.from(this._contributedEditors.values()); this._memento.saveMemento(); - return toDisposable(() => { + return this._register(toDisposable(() => { const mementoObject = this._memento.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE); mementoObject[NotebookProviderInfoStore.CUSTOM_EDITORS_ENTRY_ID] = Array.from(this._contributedEditors.values()); this._memento.saveMemento(); editorRegistration?.dispose(); this._contributedEditors.delete(info.id); - }); + })); } getContributedNotebook(resource: URI): readonly NotebookProviderInfo[] { diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts index 2426b24c850..257ead51887 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts @@ -152,7 +152,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD private readonly _onDidChangeSelection = this._register(new Emitter()); get onDidChangeSelection(): Event { return this._onDidChangeSelection.event; } - private _selectionCollection = new NotebookCellSelectionCollection(); + private _selectionCollection = this._register(new NotebookCellSelectionCollection()); private get selectionHandles() { const handlesSet = new Set(); diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index 417114d32e2..3a1d9faaddf 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -294,6 +294,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel }); this._cellListeners.set(mainCells[i].handle, dirtyStateListener); + this._register(mainCells[i]); } this._cells.splice(0, 0, ...mainCells); diff --git a/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts b/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts index c226a32fe0f..42b5d294c13 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts @@ -11,6 +11,7 @@ import { INTERACTIVE_WINDOW_EDITOR_ID, NOTEBOOK_EDITOR_ID } from 'vs/workbench/c //#region Context Keys export const HAS_OPENED_NOTEBOOK = new RawContextKey('userHasOpenedNotebook', false); export const KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED = new RawContextKey('notebookFindWidgetFocused', false); +export const InteractiveWindowOpen = new RawContextKey('interactiveWindowOpen', false); // Is Notebook export const NOTEBOOK_IS_ACTIVE_EDITOR = ContextKeyExpr.equals('activeEditor', NOTEBOOK_EDITOR_ID); diff --git a/src/vs/workbench/contrib/notebook/test/browser/cellOperations.test.ts b/src/vs/workbench/contrib/notebook/test/browser/cellOperations.test.ts index 2de6c8fcf3c..357927dbf60 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/cellOperations.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/cellOperations.test.ts @@ -13,8 +13,11 @@ import { ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { ITextBuffer, ValidAnnotatedEditOperation } from 'vs/editor/common/model'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('CellOperations', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + test('Move cells - single cell', async function () { await withTestNotebook( [ @@ -61,8 +64,8 @@ suite('CellOperations', () => { ['var b = 2;', 'javascript', CellKind.Code, [], {}], ['var c = 3;', 'javascript', CellKind.Code, [], {}] ], - async (editor, viewModel) => { - const foldingModel = new FoldingModel(); + async (editor, viewModel, ds) => { + const foldingModel = ds.add(new FoldingModel()); foldingModel.attachViewModel(viewModel); updateFoldingStateAtIndex(foldingModel, 0, true); updateFoldingStateAtIndex(foldingModel, 1, true); @@ -144,8 +147,8 @@ suite('CellOperations', () => { ['var b = 2;', 'javascript', CellKind.Code, [], {}], ['var c = 3;', 'javascript', CellKind.Code, [], {}] ], - async (editor, viewModel) => { - const foldingModel = new FoldingModel(); + async (editor, viewModel, ds) => { + const foldingModel = ds.add(new FoldingModel()); foldingModel.attachViewModel(viewModel); updateFoldingStateAtIndex(foldingModel, 0, true); updateFoldingStateAtIndex(foldingModel, 1, true); @@ -536,7 +539,7 @@ suite('CellOperations', () => { ['var b = 2;', 'javascript', CellKind.Code, [], {}], ['var c = 3;', 'javascript', CellKind.Code, [], {}] ], - async (editor, viewModel, accessor) => { + async (editor, viewModel, _ds, accessor) => { const languageService = accessor.get(ILanguageService); const insertedCellAbove = insertCell(languageService, editor, 4, CellKind.Code, 'above', 'var a = 0;'); diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/contributedStatusBarItemController.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/contributedStatusBarItemController.test.ts index b37d157699b..71a5b40715c 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/contributedStatusBarItemController.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/contributedStatusBarItemController.test.ts @@ -26,7 +26,7 @@ suite('Notebook Statusbar', () => { ['var b = 1;', 'javascript', CellKind.Code, [], {}], ['# header a', 'markdown', CellKind.Markup, [], {}], ], - async (editor, viewModel, accessor) => { + async (editor, viewModel, _ds, accessor) => { const cellStatusbarSvc = accessor.get(INotebookCellStatusBarService); testDisposables.add(accessor.createInstance(ContributedStatusBarItemController, editor)); diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/find.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/find.test.ts index b941e70675e..0154bb69eb3 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/find.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/find.test.ts @@ -55,7 +55,7 @@ suite('Notebook Find', () => { ['paragraph 1', 'markdown', CellKind.Markup, [], {}], ['paragraph 2', 'markdown', CellKind.Markup, [], {}], ], - async (editor, viewModel, accessor) => { + async (editor, viewModel, _ds, accessor) => { accessor.stub(IConfigurationService, configurationService); const state = new FindReplaceState(); const model = new FindModel(editor, state, accessor.get(IConfigurationService)); @@ -101,7 +101,7 @@ suite('Notebook Find', () => { ['paragraph 1.3', 'markdown', CellKind.Markup, [], {}], ['paragraph 2', 'markdown', CellKind.Markup, [], {}], ], - async (editor, viewModel, accessor) => { + async (editor, viewModel, _ds, accessor) => { setupEditorForTest(editor, viewModel); accessor.stub(IConfigurationService, configurationService); const state = new FindReplaceState(); @@ -152,7 +152,7 @@ suite('Notebook Find', () => { ['paragraph 1.3', 'markdown', CellKind.Markup, [], {}], ['paragraph 2', 'markdown', CellKind.Markup, [], {}], ], - async (editor, viewModel, accessor) => { + async (editor, viewModel, _ds, accessor) => { setupEditorForTest(editor, viewModel); accessor.stub(IConfigurationService, configurationService); const state = new FindReplaceState(); @@ -194,7 +194,7 @@ suite('Notebook Find', () => { ['paragraph 1.3', 'markdown', CellKind.Markup, [], {}], ['paragraph 2', 'markdown', CellKind.Markup, [], {}], ], - async (editor, viewModel, accessor) => { + async (editor, viewModel, _ds, accessor) => { setupEditorForTest(editor, viewModel); accessor.stub(IConfigurationService, configurationService); const state = new FindReplaceState(); @@ -232,7 +232,7 @@ suite('Notebook Find', () => { ['paragraph 1', 'markdown', CellKind.Markup, [], {}], ['paragraph 2', 'markdown', CellKind.Markup, [], {}], ], - async (editor, viewModel, accessor) => { + async (editor, viewModel, _ds, accessor) => { accessor.stub(IConfigurationService, configurationService); const state = new FindReplaceState(); const model = new FindModel(editor, state, accessor.get(IConfigurationService)); diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookClipboard.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookClipboard.test.ts index 43d96a3990d..541dd70ff09 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookClipboard.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookClipboard.test.ts @@ -42,7 +42,7 @@ suite('Notebook Clipboard', () => { ['paragraph 1', 'markdown', CellKind.Markup, [], {}], ['paragraph 2', 'markdown', CellKind.Markup, [], {}], ], - async (editor, viewModel, accessor) => { + async (editor, viewModel, _ds, accessor) => { accessor.stub(INotebookService, new class extends mock() { override setToCopy() { } }); const clipboardContrib = new NotebookClipboardContribution(createEditorService(editor)); @@ -66,7 +66,7 @@ suite('Notebook Clipboard', () => { ['# header d', 'markdown', CellKind.Markup, [], {}], ['var e = 4;', 'javascript', CellKind.Code, [], {}], ], - async (editor, viewModel, accessor) => { + async (editor, viewModel, _ds, accessor) => { const foldingModel = new FoldingModel(); foldingModel.attachViewModel(viewModel); @@ -97,7 +97,7 @@ suite('Notebook Clipboard', () => { ['# header d', 'markdown', CellKind.Markup, [], {}], ['var e = 4;', 'javascript', CellKind.Code, [], {}], ], - async (editor, viewModel, accessor) => { + async (editor, viewModel, _ds, accessor) => { const foldingModel = new FoldingModel(); foldingModel.attachViewModel(viewModel); @@ -130,7 +130,7 @@ suite('Notebook Clipboard', () => { ['paragraph 1', 'markdown', CellKind.Markup, [], {}], ['paragraph 2', 'markdown', CellKind.Markup, [], {}], ], - async (editor, viewModel, accessor) => { + async (editor, viewModel, _ds, accessor) => { accessor.stub(INotebookService, new class extends mock() { override setToCopy() { } }); const clipboardContrib = new NotebookClipboardContribution(createEditorService(editor)); @@ -148,7 +148,7 @@ suite('Notebook Clipboard', () => { ['paragraph 1', 'markdown', CellKind.Markup, [], {}], ['paragraph 2', 'markdown', CellKind.Markup, [], {}], ], - async (editor, viewModel, accessor) => { + async (editor, viewModel, _ds, accessor) => { accessor.stub(INotebookService, new class extends mock() { override setToCopy() { } override getToCopy() { @@ -182,7 +182,7 @@ suite('Notebook Clipboard', () => { ['paragraph 1', 'markdown', CellKind.Markup, [], {}], ['paragraph 2', 'markdown', CellKind.Markup, [], {}], ], - async (editor, viewModel, accessor) => { + async (editor, viewModel, _ds, accessor) => { let _toCopy: NotebookCellTextModel[] = []; accessor.stub(INotebookService, new class extends mock() { override setToCopy(toCopy: NotebookCellTextModel[]) { _toCopy = toCopy; } @@ -212,7 +212,7 @@ suite('Notebook Clipboard', () => { ['paragraph 2', 'markdown', CellKind.Markup, [], {}], ['paragraph 3', 'markdown', CellKind.Markup, [], {}], ], - async (editor, viewModel, accessor) => { + async (editor, viewModel, _ds, accessor) => { accessor.stub(INotebookService, new class extends mock() { override setToCopy() { } override getToCopy() { @@ -253,7 +253,7 @@ suite('Notebook Clipboard', () => { ['paragraph 2', 'markdown', CellKind.Markup, [], {}], ['paragraph 3', 'markdown', CellKind.Markup, [], {}], ], - async (editor, viewModel, accessor) => { + async (editor, viewModel, _ds, accessor) => { accessor.stub(INotebookService, new class extends mock() { override setToCopy() { } override getToCopy() { @@ -277,7 +277,7 @@ suite('Notebook Clipboard', () => { ['paragraph 2', 'markdown', CellKind.Markup, [], {}], ['paragraph 3', 'markdown', CellKind.Markup, [], {}], ], - async (editor, viewModel, accessor) => { + async (editor, viewModel, _ds, accessor) => { accessor.stub(INotebookService, new class extends mock() { override setToCopy() { } override getToCopy() { diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookUndoRedo.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookUndoRedo.test.ts index b40b1e63633..3f1a6f021fa 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookUndoRedo.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookUndoRedo.test.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { CellEditType, CellKind, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { createNotebookCellList, TestCell, withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; @@ -15,7 +16,7 @@ suite('Notebook Undo/Redo', () => { ['# header 1', 'markdown', CellKind.Markup, [], {}], ['body', 'markdown', CellKind.Markup, [], {}], ], - async (editor, viewModel, accessor) => { + async (editor, viewModel, _ds, accessor) => { const languageService = accessor.get(ILanguageService); assert.strictEqual(viewModel.length, 2); assert.strictEqual(viewModel.getVersionId(), 0); @@ -59,7 +60,7 @@ suite('Notebook Undo/Redo', () => { ['# header 1', 'markdown', CellKind.Markup, [], {}], ['body', 'markdown', CellKind.Markup, [], {}], ], - async (editor, viewModel, accessor) => { + async (editor, viewModel, _ds, accessor) => { const languageService = accessor.get(ILanguageService); editor.textModel.applyEdits([{ editType: CellEditType.Replace, index: 0, count: 2, cells: [] @@ -100,7 +101,7 @@ suite('Notebook Undo/Redo', () => { ['# header 1', 'markdown', CellKind.Markup, [], {}], ['body', 'markdown', CellKind.Markup, [], {}], ], - async (editor, viewModel, accessor) => { + async (editor, viewModel, _ds, accessor) => { const languageService = accessor.get(ILanguageService); editor.textModel.applyEdits([{ editType: CellEditType.Replace, index: 0, count: 2, cells: [] @@ -132,9 +133,9 @@ suite('Notebook Undo/Redo', () => { ['# header 1', 'markdown', CellKind.Markup, [], {}], ['body', 'markdown', CellKind.Markup, [], {}], ], - async (editor, viewModel, accessor) => { + async (editor, viewModel, _ds, accessor) => { const languageService = accessor.get(ILanguageService); - const cellList = createNotebookCellList(accessor); + const cellList = createNotebookCellList(accessor, new DisposableStore()); cellList.attachViewModel(viewModel); cellList.setFocus([1]); @@ -173,7 +174,7 @@ suite('Notebook Undo/Redo', () => { ['# header 1', 'markdown', CellKind.Markup, [], {}], ['body', 'markdown', CellKind.Markup, [], {}], ], - async (editor, viewModel, accessor) => { + async (editor, viewModel, _ds, accessor) => { const languageService = accessor.get(ILanguageService); editor.textModel.applyEdits([{ diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookCellList.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookCellList.test.ts index 0d8e843411f..fb3db12dace 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookCellList.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookCellList.test.ts @@ -5,20 +5,25 @@ import * as assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { createNotebookCellList, setupInstantiationService, withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; suite('NotebookCellList', () => { - let disposables: DisposableStore; + let testDisposables: DisposableStore; let instantiationService: TestInstantiationService; - suiteSetup(() => { - disposables = new DisposableStore(); - instantiationService = setupInstantiationService(disposables); + teardown(() => { + testDisposables.dispose(); }); - suiteTeardown(() => disposables.dispose()); + ensureNoDisposablesAreLeakedInTestSuite(); + + setup(() => { + testDisposables = new DisposableStore(); + instantiationService = setupInstantiationService(testDisposables); + }); test('revealElementsInView: reveal fully visible cell should not scroll', async function () { await withTestNotebook( @@ -29,7 +34,7 @@ suite('NotebookCellList', () => { ['var b = 2;', 'javascript', CellKind.Code, [], {}], ['# header c', 'markdown', CellKind.Markup, [], {}] ], - async (editor, viewModel) => { + async (editor, viewModel, disposables) => { viewModel.restoreEditorViewState({ editingCells: [false, false, false, false, false], cellLineNumberStates: {}, @@ -39,7 +44,7 @@ suite('NotebookCellList', () => { collapsedOutputCells: {}, }); - const cellList = createNotebookCellList(instantiationService); + const cellList = createNotebookCellList(instantiationService, disposables); cellList.attachViewModel(viewModel); // render height 210, it can render 3 full cells and 1 partial cell @@ -76,7 +81,7 @@ suite('NotebookCellList', () => { ['var b = 2;', 'javascript', CellKind.Code, [], {}], ['# header c', 'markdown', CellKind.Markup, [], {}] ], - async (editor, viewModel) => { + async (editor, viewModel, disposables) => { viewModel.restoreEditorViewState({ editingCells: [false, false, false, false, false], editorViewStates: [null, null, null, null, null], @@ -86,7 +91,7 @@ suite('NotebookCellList', () => { collapsedOutputCells: {}, }); - const cellList = createNotebookCellList(instantiationService); + const cellList = createNotebookCellList(instantiationService, disposables); cellList.attachViewModel(viewModel); // render height 210, it can render 3 full cells and 1 partial cell @@ -120,7 +125,7 @@ suite('NotebookCellList', () => { ['var b = 2;', 'javascript', CellKind.Code, [], {}], ['# header c', 'markdown', CellKind.Markup, [], {}] ], - async (editor, viewModel) => { + async (editor, viewModel, disposables) => { viewModel.restoreEditorViewState({ editingCells: [false, false, false, false, false], editorViewStates: [null, null, null, null, null], @@ -130,7 +135,7 @@ suite('NotebookCellList', () => { collapsedOutputCells: {}, }); - const cellList = createNotebookCellList(instantiationService); + const cellList = createNotebookCellList(instantiationService, disposables); // without paddingBottom, the last 20 px will always be hidden due to `topInsertToolbarHeight` cellList.updateOptions({ paddingBottom: 100 }); cellList.attachViewModel(viewModel); @@ -157,7 +162,7 @@ suite('NotebookCellList', () => { ['var b = 2;', 'javascript', CellKind.Code, [], {}], ['# header c', 'markdown', CellKind.Markup, [], {}] ], - async (editor, viewModel) => { + async (editor, viewModel, disposables) => { viewModel.restoreEditorViewState({ editingCells: [false, false, false, false, false], editorViewStates: [null, null, null, null, null], @@ -167,7 +172,7 @@ suite('NotebookCellList', () => { collapsedOutputCells: {}, }); - const cellList = createNotebookCellList(instantiationService); + const cellList = createNotebookCellList(instantiationService, disposables); cellList.attachViewModel(viewModel); // render height 210, it can render 3 full cells and 1 partial cell @@ -199,7 +204,7 @@ suite('NotebookCellList', () => { ['var b = 2;', 'javascript', CellKind.Code, [], {}], ['# header c', 'markdown', CellKind.Markup, [], {}] ], - async (editor, viewModel) => { + async (editor, viewModel, disposables) => { viewModel.restoreEditorViewState({ editingCells: [false, false, false, false, false], editorViewStates: [null, null, null, null, null], @@ -209,7 +214,7 @@ suite('NotebookCellList', () => { collapsedOutputCells: {}, }); - const cellList = createNotebookCellList(instantiationService); + const cellList = createNotebookCellList(instantiationService, disposables); cellList.attachViewModel(viewModel); // render height 210, it can render 3 full cells and 1 partial cell @@ -252,7 +257,7 @@ suite('NotebookCellList', () => { ['var b = 2;', 'javascript', CellKind.Code, [], {}], ['# header c', 'markdown', CellKind.Markup, [], {}] ], - async (editor, viewModel) => { + async (editor, viewModel, disposables) => { viewModel.restoreEditorViewState({ editingCells: [false, false, false, false, false], editorViewStates: [null, null, null, null, null], @@ -262,7 +267,7 @@ suite('NotebookCellList', () => { collapsedOutputCells: {}, }); - const cellList = createNotebookCellList(instantiationService); + const cellList = createNotebookCellList(instantiationService, disposables); cellList.attachViewModel(viewModel); // render height 210, it can render 3 full cells and 1 partial cell @@ -288,7 +293,7 @@ suite('NotebookCellList', () => { ['var b = 2;', 'javascript', CellKind.Code, [], {}], ['# header c', 'markdown', CellKind.Markup, [], {}] ], - async (editor, viewModel) => { + async (editor, viewModel, disposables) => { viewModel.restoreEditorViewState({ editingCells: [false, false, false, false, false], editorViewStates: [null, null, null, null, null], @@ -298,7 +303,7 @@ suite('NotebookCellList', () => { collapsedOutputCells: {}, }); - const cellList = createNotebookCellList(instantiationService); + const cellList = createNotebookCellList(instantiationService, disposables); cellList.attachViewModel(viewModel); // render height 210, it can render 3 full cells and 1 partial cell @@ -321,8 +326,8 @@ suite('NotebookCellList', () => { await withTestNotebook( [ ], - async (editor, viewModel) => { - const cellList = createNotebookCellList(instantiationService); + async (editor, viewModel, disposables) => { + const cellList = createNotebookCellList(instantiationService, disposables); cellList.attachViewModel(viewModel); // render height 210, it can render 3 full cells and 1 partial cell @@ -337,7 +342,7 @@ suite('NotebookCellList', () => { [ ['# header a', 'markdown', CellKind.Markup, [], {}], ], - async (editor, viewModel) => { + async (editor, viewModel, disposables) => { viewModel.restoreEditorViewState({ editingCells: [false], editorViewStates: [null], @@ -347,7 +352,7 @@ suite('NotebookCellList', () => { collapsedOutputCells: {}, }); - const cellList = createNotebookCellList(instantiationService); + const cellList = createNotebookCellList(instantiationService, disposables); cellList.attachViewModel(viewModel); // render height 210, it can render 3 full cells and 1 partial cell diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookCommon.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookCommon.test.ts index 1fa57c44f31..83d0164b035 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookCommon.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookCommon.test.ts @@ -7,6 +7,7 @@ import * as assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { Mimes } from 'vs/base/common/mime'; import { URI } from 'vs/base/common/uri'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { CellKind, CellUri, diff, MimeTypeDisplayOrder, NotebookWorkingCopyTypeIdentifier } from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -14,18 +15,18 @@ import { cellIndexesToRanges, cellRangesToIndexes, reduceCellRanges } from 'vs/w import { setupInstantiationService, TestCell } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; suite('NotebookCommon', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + let disposables: DisposableStore; let instantiationService: TestInstantiationService; let languageService: ILanguageService; - suiteSetup(() => { + setup(() => { disposables = new DisposableStore(); instantiationService = setupInstantiationService(disposables); languageService = instantiationService.get(ILanguageService); }); - suiteTeardown(() => disposables.dispose()); - test('sortMimeTypes default orders', function () { assert.deepStrictEqual(new MimeTypeDisplayOrder().sort( [ @@ -99,6 +100,8 @@ suite('NotebookCommon', () => { Mimes.text ] ); + + disposables.dispose(); }); @@ -163,6 +166,8 @@ suite('NotebookCommon', () => { Mimes.text ] ); + + disposables.dispose(); }); test('prioritizes mimetypes', () => { @@ -197,6 +202,8 @@ suite('NotebookCommon', () => { const m2 = new MimeTypeDisplayOrder(['a', 'b']); m2.prioritize('b', ['a', 'b', 'a', 'q']); assert.deepStrictEqual(m2.toArray(), ['b', 'a']); + + disposables.dispose(); }); test('sortMimeTypes glob', function () { @@ -224,6 +231,8 @@ suite('NotebookCommon', () => { ], 'glob *' ); + + disposables.dispose(); }); test('diff cells', function () { @@ -231,7 +240,7 @@ suite('NotebookCommon', () => { for (let i = 0; i < 5; i++) { cells.push( - new TestCell('notebook', i, `var a = ${i};`, 'javascript', CellKind.Code, [], languageService) + disposables.add(new TestCell('notebook', i, `var a = ${i};`, 'javascript', CellKind.Code, [], languageService)) ); } @@ -257,8 +266,8 @@ suite('NotebookCommon', () => { ] ); - const cellA = new TestCell('notebook', 6, 'var a = 6;', 'javascript', CellKind.Code, [], languageService); - const cellB = new TestCell('notebook', 7, 'var a = 7;', 'javascript', CellKind.Code, [], languageService); + const cellA = disposables.add(new TestCell('notebook', 6, 'var a = 6;', 'javascript', CellKind.Code, [], languageService)); + const cellB = disposables.add(new TestCell('notebook', 7, 'var a = 7;', 'javascript', CellKind.Code, [], languageService)); const modifiedCells = [ cells[0], @@ -287,7 +296,10 @@ suite('NotebookCommon', () => { } ] ); + + disposables.dispose(); }); + }); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookDiff.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookDiff.test.ts index f92b02ba51e..30bf9dbe3a4 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookDiff.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookDiff.test.ts @@ -6,7 +6,9 @@ import * as assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { ISequence, LcsDiff } from 'vs/base/common/diff/diff'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { Mimes } from 'vs/base/common/mime'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { NotebookDiffEditorEventDispatcher } from 'vs/workbench/contrib/notebook/browser/diff/eventDispatcher'; import { NotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor'; @@ -30,10 +32,21 @@ class CellSequence implements ISequence { suite('NotebookCommon', () => { + let disposables: DisposableStore; const configurationService = new TestConfigurationService(); + teardown(() => { + disposables.dispose(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + setup(() => { + disposables = new DisposableStore(); + }); + test('diff different source', async () => { - await withTestNotebookDiffModel([ + await withTestNotebookDiffModel(disposables, [ ['x', 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { custom: { metadata: { collapsed: false } }, executionOrder: 3 }], ], [ ['y', 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { custom: { metadata: { collapsed: false } }, executionOrder: 3 }], @@ -53,17 +66,24 @@ suite('NotebookCommon', () => { modifiedLength: 1 }]); - const eventDispatcher = new NotebookDiffEditorEventDispatcher(); + const eventDispatcher = disposables.add(new NotebookDiffEditorEventDispatcher()); const diffViewModels = NotebookTextDiffEditor.computeDiff(accessor, configurationService, model, eventDispatcher, { cellsDiff: diffResult }, undefined); assert.strictEqual(diffViewModels.viewModels.length, 1); assert.strictEqual(diffViewModels.viewModels[0].type, 'modified'); + diffViewModels.viewModels.forEach(vm => { + vm.original?.dispose(); + vm.modified?.dispose(); + vm.dispose(); + }); + model.original.notebook.dispose(); + model.modified.notebook.dispose(); }); }); test('diff different output', async () => { - await withTestNotebookDiffModel([ + await withTestNotebookDiffModel(disposables, [ ['x', 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([5])) }] }], { custom: { metadata: { collapsed: false } }, executionOrder: 5 }], ['', 'javascript', CellKind.Code, [], {}] ], [ @@ -85,18 +105,26 @@ suite('NotebookCommon', () => { modifiedLength: 1 }]); - const eventDispatcher = new NotebookDiffEditorEventDispatcher(); + const eventDispatcher = disposables.add(new NotebookDiffEditorEventDispatcher()); const diffViewModels = NotebookTextDiffEditor.computeDiff(accessor, configurationService, model, eventDispatcher, { cellsDiff: diffResult }, undefined); assert.strictEqual(diffViewModels.viewModels.length, 2); assert.strictEqual(diffViewModels.viewModels[0].type, 'modified'); assert.strictEqual(diffViewModels.viewModels[1].type, 'unchanged'); + + diffViewModels.viewModels.forEach(vm => { + vm.original?.dispose(); + vm.modified?.dispose(); + vm.dispose(); + }); + model.original.notebook.dispose(); + model.modified.notebook.dispose(); }); }); test('diff test small source', async () => { - await withTestNotebookDiffModel([ + await withTestNotebookDiffModel(disposables, [ ['123456789', 'javascript', CellKind.Code, [], {}] ], [ ['987654321', 'javascript', CellKind.Code, [], {}], @@ -116,17 +144,25 @@ suite('NotebookCommon', () => { modifiedLength: 1 }]); - const eventDispatcher = new NotebookDiffEditorEventDispatcher(); + const eventDispatcher = disposables.add(new NotebookDiffEditorEventDispatcher()); const diffViewModels = NotebookTextDiffEditor.computeDiff(accessor, configurationService, model, eventDispatcher, { cellsDiff: diffResult }, undefined); assert.strictEqual(diffViewModels.viewModels.length, 1); assert.strictEqual(diffViewModels.viewModels[0].type, 'modified'); + + diffViewModels.viewModels.forEach(vm => { + vm.original?.dispose(); + vm.modified?.dispose(); + vm.dispose(); + }); + model.original.notebook.dispose(); + model.modified.notebook.dispose(); }); }); test('diff test data single cell', async () => { - await withTestNotebookDiffModel([ + await withTestNotebookDiffModel(disposables, [ [[ '# This version has a bug\n', 'def mult(a, b):\n', @@ -154,17 +190,25 @@ suite('NotebookCommon', () => { modifiedLength: 1 }]); - const eventDispatcher = new NotebookDiffEditorEventDispatcher(); + const eventDispatcher = disposables.add(new NotebookDiffEditorEventDispatcher()); const diffViewModels = NotebookTextDiffEditor.computeDiff(accessor, configurationService, model, eventDispatcher, { cellsDiff: diffResult }, undefined); assert.strictEqual(diffViewModels.viewModels.length, 1); assert.strictEqual(diffViewModels.viewModels[0].type, 'modified'); + + diffViewModels.viewModels.forEach(vm => { + vm.original?.dispose(); + vm.modified?.dispose(); + vm.dispose(); + }); + model.original.notebook.dispose(); + model.modified.notebook.dispose(); }); }); test('diff foo/foe', async () => { - await withTestNotebookDiffModel([ + await withTestNotebookDiffModel(disposables, [ [['def foe(x, y):\n', ' return x + y\n', 'foe(3, 2)'].join(''), 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([6])) }] }], { custom: { metadata: { collapsed: false } }, executionOrder: 5 }], [['def foo(x, y):\n', ' return x * y\n', 'foo(1, 2)'].join(''), 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([2])) }] }], { custom: { metadata: { collapsed: false } }, executionOrder: 6 }], ['', 'javascript', CellKind.Code, [], {}] @@ -175,7 +219,7 @@ suite('NotebookCommon', () => { ], (model, accessor) => { const diff = new LcsDiff(new CellSequence(model.original.notebook), new CellSequence(model.modified.notebook)); const diffResult = diff.ComputeDiff(false); - const eventDispatcher = new NotebookDiffEditorEventDispatcher(); + const eventDispatcher = disposables.add(new NotebookDiffEditorEventDispatcher()); const diffViewModels = NotebookTextDiffEditor.computeDiff(accessor, configurationService, model, eventDispatcher, { cellsDiff: diffResult }, undefined); @@ -183,11 +227,19 @@ suite('NotebookCommon', () => { assert.strictEqual(diffViewModels.viewModels[0].type, 'modified'); assert.strictEqual(diffViewModels.viewModels[1].type, 'modified'); assert.strictEqual(diffViewModels.viewModels[2].type, 'unchanged'); + + diffViewModels.viewModels.forEach(vm => { + vm.original?.dispose(); + vm.modified?.dispose(); + vm.dispose(); + }); + model.original.notebook.dispose(); + model.modified.notebook.dispose(); }); }); test('diff markdown', async () => { - await withTestNotebookDiffModel([ + await withTestNotebookDiffModel(disposables, [ ['This is a test notebook with only markdown cells', 'markdown', CellKind.Markup, [], {}], ['Lorem ipsum dolor sit amet', 'markdown', CellKind.Markup, [], {}], ['In other news', 'markdown', CellKind.Markup, [], {}], @@ -198,7 +250,7 @@ suite('NotebookCommon', () => { ], (model, accessor) => { const diff = new LcsDiff(new CellSequence(model.original.notebook), new CellSequence(model.modified.notebook)); const diffResult = diff.ComputeDiff(false); - const eventDispatcher = new NotebookDiffEditorEventDispatcher(); + const eventDispatcher = disposables.add(new NotebookDiffEditorEventDispatcher()); const diffViewModels = NotebookTextDiffEditor.computeDiff(accessor, configurationService, model, eventDispatcher, { cellsDiff: diffResult }, undefined); @@ -206,11 +258,19 @@ suite('NotebookCommon', () => { assert.strictEqual(diffViewModels.viewModels[0].type, 'modified'); assert.strictEqual(diffViewModels.viewModels[1].type, 'unchanged'); assert.strictEqual(diffViewModels.viewModels[2].type, 'modified'); + + diffViewModels.viewModels.forEach(vm => { + vm.original?.dispose(); + vm.modified?.dispose(); + vm.dispose(); + }); + model.original.notebook.dispose(); + model.modified.notebook.dispose(); }); }); test('diff insert', async () => { - await withTestNotebookDiffModel([ + await withTestNotebookDiffModel(disposables, [ ['var a = 1;', 'javascript', CellKind.Code, [], {}], ['var b = 2;', 'javascript', CellKind.Code, [], {}] ], [ @@ -218,7 +278,7 @@ suite('NotebookCommon', () => { ['var a = 1;', 'javascript', CellKind.Code, [], {}], ['var b = 2;', 'javascript', CellKind.Code, [], {}] ], (model, accessor) => { - const eventDispatcher = new NotebookDiffEditorEventDispatcher(); + const eventDispatcher = disposables.add(new NotebookDiffEditorEventDispatcher()); const diffResult = NotebookTextDiffEditor.computeDiff(accessor, configurationService, model, eventDispatcher, { cellsDiff: { changes: [{ @@ -235,12 +295,20 @@ suite('NotebookCommon', () => { assert.strictEqual(diffResult.viewModels[0].type, 'insert'); assert.strictEqual(diffResult.viewModels[1].type, 'unchanged'); assert.strictEqual(diffResult.viewModels[2].type, 'unchanged'); + + diffResult.viewModels.forEach(vm => { + vm.original?.dispose(); + vm.modified?.dispose(); + vm.dispose(); + }); + model.original.notebook.dispose(); + model.modified.notebook.dispose(); }); }); test('diff insert 2', async () => { - await withTestNotebookDiffModel([ + await withTestNotebookDiffModel(disposables, [ ['var a = 1;', 'javascript', CellKind.Code, [], {}], ['var b = 2;', 'javascript', CellKind.Code, [], {}], ['var c = 3;', 'javascript', CellKind.Code, [], {}], @@ -258,7 +326,7 @@ suite('NotebookCommon', () => { ['var f = 6;', 'javascript', CellKind.Code, [], {}], ['var g = 7;', 'javascript', CellKind.Code, [], {}], ], async (model, accessor) => { - const eventDispatcher = new NotebookDiffEditorEventDispatcher(); + const eventDispatcher = disposables.add(new NotebookDiffEditorEventDispatcher()); const diffResult = NotebookTextDiffEditor.computeDiff(accessor, configurationService, model, eventDispatcher, { cellsDiff: { changes: [{ @@ -285,12 +353,20 @@ suite('NotebookCommon', () => { assert.strictEqual(diffResult.viewModels[5].type, 'unchanged'); assert.strictEqual(diffResult.viewModels[6].type, 'unchanged'); assert.strictEqual(diffResult.viewModels[7].type, 'unchanged'); + + diffResult.viewModels.forEach(vm => { + vm.original?.dispose(); + vm.modified?.dispose(); + vm.dispose(); + }); + model.original.notebook.dispose(); + model.modified.notebook.dispose(); }); }); test('diff insert 3', async () => { - await withTestNotebookDiffModel([ + await withTestNotebookDiffModel(disposables, [ ['var a = 1;', 'javascript', CellKind.Code, [], {}], ['var b = 2;', 'javascript', CellKind.Code, [], {}], ['var c = 3;', 'javascript', CellKind.Code, [], {}], @@ -308,7 +384,7 @@ suite('NotebookCommon', () => { ['var f = 6;', 'javascript', CellKind.Code, [], {}], ['var g = 7;', 'javascript', CellKind.Code, [], {}], ], async (model, accessor) => { - const eventDispatcher = new NotebookDiffEditorEventDispatcher(); + const eventDispatcher = disposables.add(new NotebookDiffEditorEventDispatcher()); const diffResult = NotebookTextDiffEditor.computeDiff(accessor, configurationService, model, eventDispatcher, { cellsDiff: { changes: [{ @@ -330,11 +406,19 @@ suite('NotebookCommon', () => { assert.strictEqual(diffResult.viewModels[5].type, 'unchanged'); assert.strictEqual(diffResult.viewModels[6].type, 'unchanged'); assert.strictEqual(diffResult.viewModels[7].type, 'unchanged'); + + diffResult.viewModels.forEach(vm => { + vm.original?.dispose(); + vm.modified?.dispose(); + vm.dispose(); + }); + model.original.notebook.dispose(); + model.modified.notebook.dispose(); }); }); test('LCS', async () => { - await withTestNotebookDiffModel([ + await withTestNotebookDiffModel(disposables, [ ['# Description', 'markdown', CellKind.Markup, [], { custom: { metadata: {} } }], ['x = 3', 'javascript', CellKind.Code, [], { custom: { metadata: { collapsed: true } }, executionOrder: 1 }], ['x', 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { custom: { metadata: { collapsed: false } }, executionOrder: 1 }], @@ -367,7 +451,7 @@ suite('NotebookCommon', () => { }); test('LCS 2', async () => { - await withTestNotebookDiffModel([ + await withTestNotebookDiffModel(disposables, [ ['# Description', 'markdown', CellKind.Markup, [], { custom: { metadata: {} } }], ['x = 3', 'javascript', CellKind.Code, [], { custom: { metadata: { collapsed: true } }, executionOrder: 1 }], ['x', 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { custom: { metadata: { collapsed: false } }, executionOrder: 1 }], @@ -418,7 +502,7 @@ suite('NotebookCommon', () => { }); test('LCS 3', async () => { - await withTestNotebookDiffModel([ + await withTestNotebookDiffModel(disposables, [ ['var a = 1;', 'javascript', CellKind.Code, [], {}], ['var b = 2;', 'javascript', CellKind.Code, [], {}], ['var c = 3;', 'javascript', CellKind.Code, [], {}], @@ -455,7 +539,7 @@ suite('NotebookCommon', () => { }); test('diff output', async () => { - await withTestNotebookDiffModel([ + await withTestNotebookDiffModel(disposables, [ ['x', 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { custom: { metadata: { collapsed: false } }, executionOrder: 3 }], ['y', 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([4])) }] }], { custom: { metadata: { collapsed: false } }, executionOrder: 3 }], ], [ @@ -464,7 +548,7 @@ suite('NotebookCommon', () => { ], (model, accessor) => { const diff = new LcsDiff(new CellSequence(model.original.notebook), new CellSequence(model.modified.notebook)); const diffResult = diff.ComputeDiff(false); - const eventDispatcher = new NotebookDiffEditorEventDispatcher(); + const eventDispatcher = disposables.add(new NotebookDiffEditorEventDispatcher()); const diffViewModels = NotebookTextDiffEditor.computeDiff(accessor, configurationService, model, eventDispatcher, { cellsDiff: diffResult }, undefined); @@ -472,11 +556,19 @@ suite('NotebookCommon', () => { assert.strictEqual(diffViewModels.viewModels[0].type, 'unchanged'); assert.strictEqual(diffViewModels.viewModels[0].checkIfOutputsModified(), false); assert.strictEqual(diffViewModels.viewModels[1].type, 'modified'); + + diffViewModels.viewModels.forEach(vm => { + vm.original?.dispose(); + vm.modified?.dispose(); + vm.dispose(); + }); + model.original.notebook.dispose(); + model.modified.notebook.dispose(); }); }); test('diff output fast check', async () => { - await withTestNotebookDiffModel([ + await withTestNotebookDiffModel(disposables, [ ['x', 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { custom: { metadata: { collapsed: false } }, executionOrder: 3 }], ['y', 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([4])) }] }], { custom: { metadata: { collapsed: false } }, executionOrder: 3 }], ], [ @@ -485,13 +577,20 @@ suite('NotebookCommon', () => { ], (model, accessor) => { const diff = new LcsDiff(new CellSequence(model.original.notebook), new CellSequence(model.modified.notebook)); const diffResult = diff.ComputeDiff(false); - const eventDispatcher = new NotebookDiffEditorEventDispatcher(); + const eventDispatcher = disposables.add(new NotebookDiffEditorEventDispatcher()); const diffViewModels = NotebookTextDiffEditor.computeDiff(accessor, configurationService, model, eventDispatcher, { cellsDiff: diffResult }, undefined); assert.strictEqual(diffViewModels.viewModels.length, 2); assert.strictEqual(diffViewModels.viewModels[0].original!.textModel.equal(diffViewModels.viewModels[0].modified!.textModel), true); assert.strictEqual(diffViewModels.viewModels[1].original!.textModel.equal(diffViewModels.viewModels[1].modified!.textModel), false); + diffViewModels.viewModels.forEach(vm => { + vm.original?.dispose(); + vm.modified?.dispose(); + vm.dispose(); + }); + model.original.notebook.dispose(); + model.modified.notebook.dispose(); }); }); }); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookEditor.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookEditor.test.ts index c1c39b0b693..1bd5ca8f03c 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookEditor.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookEditor.test.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { DisposableStore } from 'vs/base/common/lifecycle'; import { mock } from 'vs/base/test/common/mock'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { FoldingModel, updateFoldingStateAtIndex } from 'vs/workbench/contrib/notebook/browser/viewModel/foldingModel'; @@ -12,18 +11,24 @@ import { expandCellRangesWithHiddenCells, INotebookEditor } from 'vs/workbench/c import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { createNotebookCellList, setupInstantiationService, withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; import { ListViewInfoAccessor } from 'vs/workbench/contrib/notebook/browser/view/notebookCellList'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { DisposableStore } from 'vs/base/common/lifecycle'; suite('ListViewInfoAccessor', () => { let disposables: DisposableStore; let instantiationService: TestInstantiationService; - suiteSetup(() => { + teardown(() => { + disposables.dispose(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + setup(() => { disposables = new DisposableStore(); instantiationService = setupInstantiationService(disposables); }); - suiteTeardown(() => disposables.dispose()); - test('basics', async function () { await withTestNotebook( [ @@ -33,13 +38,13 @@ suite('ListViewInfoAccessor', () => { ['var b = 2;', 'javascript', CellKind.Code, [], {}], ['var c = 3;', 'javascript', CellKind.Code, [], {}] ], - (editor, viewModel) => { - const foldingModel = new FoldingModel(); + (editor, viewModel, ds) => { + const foldingModel = ds.add(new FoldingModel()); foldingModel.attachViewModel(viewModel); - const cellList = createNotebookCellList(instantiationService); + const cellList = ds.add(createNotebookCellList(instantiationService, ds)); cellList.attachViewModel(viewModel); - const listViewInfoAccessor = new ListViewInfoAccessor(cellList); + const listViewInfoAccessor = ds.add(new ListViewInfoAccessor(cellList)); assert.strictEqual(listViewInfoAccessor.getViewIndex(viewModel.cellAt(0)!), 0); assert.strictEqual(listViewInfoAccessor.getViewIndex(viewModel.cellAt(1)!), 1); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts index d350a742d9c..bbcd30876b1 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts @@ -25,13 +25,13 @@ suite('NotebookFileWorkingCopyModel', function () { let instantiationService: TestInstantiationService; const configurationService = new TestConfigurationService(); - suiteSetup(() => { + teardown(() => disposables.dispose()); + + setup(() => { disposables = new DisposableStore(); instantiationService = setupInstantiationService(disposables); }); - suiteTeardown(() => disposables.dispose()); - test('no transient output is send to serializer', async function () { const notebook = instantiationService.createInstance(NotebookTextModel, @@ -44,7 +44,7 @@ suite('NotebookFileWorkingCopyModel', function () { { // transient output let callCount = 0; - const model = new NotebookFileWorkingCopyModel( + const model = disposables.add(new NotebookFileWorkingCopyModel( notebook, mockNotebookService(notebook, new class extends mock() { @@ -58,7 +58,7 @@ suite('NotebookFileWorkingCopyModel', function () { } ), configurationService - ); + )); await model.snapshot(CancellationToken.None); assert.strictEqual(callCount, 1); @@ -66,7 +66,7 @@ suite('NotebookFileWorkingCopyModel', function () { { // NOT transient output let callCount = 0; - const model = new NotebookFileWorkingCopyModel( + const model = disposables.add(new NotebookFileWorkingCopyModel( notebook, mockNotebookService(notebook, new class extends mock() { @@ -80,7 +80,7 @@ suite('NotebookFileWorkingCopyModel', function () { } ), configurationService - ); + )); await model.snapshot(CancellationToken.None); assert.strictEqual(callCount, 1); } diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionStateService.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionStateService.test.ts index 13c2d908756..8874152bdb9 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionStateService.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionStateService.test.ts @@ -241,7 +241,7 @@ suite('NotebookExecutionStateService', () => { }); }); - test('getExecution and onDidChangeExecution', async function () { + test('getExecution and onDidChangeExecution 2', async function () { return withTestNotebook([], async viewModel => { testNotebookModel = viewModel.notebookDocument; diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookFolding.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookFolding.test.ts index 11555890b55..4acfd482bf8 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookFolding.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookFolding.test.ts @@ -10,18 +10,22 @@ import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { FoldingModel, updateFoldingStateAtIndex } from 'vs/workbench/contrib/notebook/browser/viewModel/foldingModel'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('Notebook Folding', () => { let disposables: DisposableStore; let instantiationService: TestInstantiationService; - suiteSetup(() => { + teardown(() => disposables.dispose()); + + ensureNoDisposablesAreLeakedInTestSuite(); + + setup(() => { disposables = new DisposableStore(); instantiationService = setupInstantiationService(disposables); instantiationService.spy(IUndoRedoService, 'pushElement'); }); - suiteTeardown(() => disposables.dispose()); test('Folding based on markdown cells', async function () { await withTestNotebook( @@ -34,8 +38,8 @@ suite('Notebook Folding', () => { ['## header 2.2', 'markdown', CellKind.Markup, [], {}], ['var e = 7;', 'markdown', CellKind.Markup, [], {}], ], - (editor, viewModel) => { - const foldingController = new FoldingModel(); + (editor, viewModel, ds) => { + const foldingController = ds.add(new FoldingModel()); foldingController.attachViewModel(viewModel); assert.strictEqual(foldingController.regions.findRange(1), 0); @@ -61,8 +65,8 @@ suite('Notebook Folding', () => { ['## header 2.1', 'markdown', CellKind.Markup, [], {}], ['var e = 7;', 'python', CellKind.Code, [], {}], ], - (editor, viewModel) => { - const foldingController = new FoldingModel(); + (editor, viewModel, ds) => { + const foldingController = ds.add(new FoldingModel()); foldingController.attachViewModel(viewModel); assert.strictEqual(foldingController.regions.findRange(1), 0); @@ -88,8 +92,8 @@ suite('Notebook Folding', () => { ['## header 2.2', 'markdown', CellKind.Markup, [], {}], ['var e = 7;', 'markdown', CellKind.Markup, [], {}], ], - (editor, viewModel) => { - const foldingController = new FoldingModel(); + (editor, viewModel, ds) => { + const foldingController = ds.add(new FoldingModel()); foldingController.attachViewModel(viewModel); assert.strictEqual(foldingController.regions.findRange(1), 0); @@ -119,8 +123,8 @@ suite('Notebook Folding', () => { ['## header 2.2', 'markdown', CellKind.Markup, [], {}], ['var e = 7;', 'markdown', CellKind.Markup, [], {}], ], - (editor, viewModel) => { - const foldingModel = new FoldingModel(); + (editor, viewModel, ds) => { + const foldingModel = ds.add(new FoldingModel()); foldingModel.attachViewModel(viewModel); updateFoldingStateAtIndex(foldingModel, 0, true); viewModel.updateFoldingRanges(foldingModel.regions); @@ -140,8 +144,8 @@ suite('Notebook Folding', () => { ['## header 2.2', 'markdown', CellKind.Markup, [], {}], ['var e = 7;', 'markdown', CellKind.Markup, [], {}], ], - (editor, viewModel) => { - const foldingModel = new FoldingModel(); + (editor, viewModel, ds) => { + const foldingModel = ds.add(new FoldingModel()); foldingModel.attachViewModel(viewModel); updateFoldingStateAtIndex(foldingModel, 2, true); viewModel.updateFoldingRanges(foldingModel.regions); @@ -162,8 +166,8 @@ suite('Notebook Folding', () => { ['## header 2.2', 'markdown', CellKind.Markup, [], {}], ['var e = 7;', 'markdown', CellKind.Markup, [], {}], ], - (editor, viewModel) => { - const foldingModel = new FoldingModel(); + (editor, viewModel, ds) => { + const foldingModel = ds.add(new FoldingModel()); foldingModel.attachViewModel(viewModel); updateFoldingStateAtIndex(foldingModel, 2, true); viewModel.updateFoldingRanges(foldingModel.regions); @@ -186,8 +190,8 @@ suite('Notebook Folding', () => { ['## header 2.2', 'markdown', CellKind.Markup, [], {}], ['var e = 7;', 'markdown', CellKind.Markup, [], {}], ], - (editor, viewModel) => { - const foldingModel = new FoldingModel(); + (editor, viewModel, ds) => { + const foldingModel = ds.add(new FoldingModel()); foldingModel.attachViewModel(viewModel); updateFoldingStateAtIndex(foldingModel, 0, true); viewModel.updateFoldingRanges(foldingModel.regions); @@ -245,8 +249,8 @@ suite('Notebook Folding', () => { ['## header 2.2', 'markdown', CellKind.Markup, [], {}], ['var e = 7;', 'markdown', CellKind.Markup, [], {}], ], - (editor, viewModel) => { - const foldingModel = new FoldingModel(); + (editor, viewModel, ds) => { + const foldingModel = ds.add(new FoldingModel()); foldingModel.attachViewModel(viewModel); foldingModel.applyMemento([{ start: 2, end: 6 }]); viewModel.updateFoldingRanges(foldingModel.regions); @@ -273,8 +277,8 @@ suite('Notebook Folding', () => { ['## header 2.2', 'markdown', CellKind.Markup, [], {}], ['var e = 7;', 'markdown', CellKind.Markup, [], {}], ], - (editor, viewModel) => { - const foldingModel = new FoldingModel(); + (editor, viewModel, ds) => { + const foldingModel = ds.add(new FoldingModel()); foldingModel.attachViewModel(viewModel); foldingModel.applyMemento([ { start: 5, end: 6 }, @@ -305,8 +309,8 @@ suite('Notebook Folding', () => { ['## header 2.2', 'markdown', CellKind.Markup, [], {}], ['var e = 7;', 'markdown', CellKind.Markup, [], {}], ], - (editor, viewModel) => { - const foldingModel = new FoldingModel(); + (editor, viewModel, ds) => { + const foldingModel = ds.add(new FoldingModel()); foldingModel.attachViewModel(viewModel); foldingModel.applyMemento([ { start: 5, end: 6 }, @@ -339,8 +343,8 @@ suite('Notebook Folding', () => { ['## header 2.2', 'markdown', CellKind.Markup, [], {}], ['var e = 7;', 'markdown', CellKind.Markup, [], {}], ], - (editor, viewModel) => { - const foldingModel = new FoldingModel(); + (editor, viewModel, ds) => { + const foldingModel = ds.add(new FoldingModel()); foldingModel.attachViewModel(viewModel); foldingModel.applyMemento([{ start: 2, end: 6 }]); viewModel.updateFoldingRanges(foldingModel.regions); @@ -375,8 +379,8 @@ suite('Notebook Folding', () => { ['## header 2.2', 'markdown', CellKind.Markup, [], {}], ['var e = 7;', 'markdown', CellKind.Markup, [], {}], ], - (editor, viewModel) => { - const foldingModel = new FoldingModel(); + (editor, viewModel, ds) => { + const foldingModel = ds.add(new FoldingModel()); foldingModel.attachViewModel(viewModel); foldingModel.applyMemento([ { start: 5, end: 6 }, diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookKernelHistory.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookKernelHistory.test.ts index 52eadf224ae..24d459f9eef 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookKernelHistory.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookKernelHistory.test.ts @@ -20,18 +20,24 @@ import { IMenu, IMenuService } from 'vs/platform/actions/common/actions'; import { NotebookKernelHistoryService } from 'vs/workbench/contrib/notebook/browser/services/notebookKernelHistoryServiceImpl'; import { IApplicationStorageValueChangeEvent, IProfileStorageValueChangeEvent, IStorageService, IStorageValueChangeEvent, IWillSaveStateEvent, IWorkspaceStorageValueChangeEvent, StorageScope } from 'vs/platform/storage/common/storage'; import { INotebookLoggingService } from 'vs/workbench/contrib/notebook/common/notebookLoggingService'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('NotebookKernelHistoryService', () => { + let disposables: DisposableStore; let instantiationService: TestInstantiationService; let kernelService: INotebookKernelService; - let disposables: DisposableStore; let onDidAddNotebookDocument: Emitter; + teardown(() => { + disposables.dispose(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + setup(function () { disposables = new DisposableStore(); - onDidAddNotebookDocument = new Emitter(); disposables.add(onDidAddNotebookDocument); @@ -50,14 +56,10 @@ suite('NotebookKernelHistoryService', () => { }; } }); - kernelService = instantiationService.createInstance(NotebookKernelService); + kernelService = disposables.add(instantiationService.createInstance(NotebookKernelService)); instantiationService.set(INotebookKernelService, kernelService); }); - teardown(() => { - disposables.dispose(); - }); - test('notebook kernel empty history', function () { const u1 = URI.parse('foo:///one'); @@ -65,8 +67,8 @@ suite('NotebookKernelHistoryService', () => { const k1 = new TestNotebookKernel({ label: 'z', viewType: 'foo' }); const k2 = new TestNotebookKernel({ label: 'a', viewType: 'foo' }); - kernelService.registerKernel(k1); - kernelService.registerKernel(k2); + disposables.add(kernelService.registerKernel(k1)); + disposables.add(kernelService.registerKernel(k2)); instantiationService.stub(IStorageService, new class extends mock() { override onWillSaveState: Event = Event.None; @@ -96,7 +98,7 @@ suite('NotebookKernelHistoryService', () => { override debug() { } }); - const kernelHistoryService = instantiationService.createInstance(NotebookKernelHistoryService); + const kernelHistoryService = disposables.add(instantiationService.createInstance(NotebookKernelHistoryService)); let info = kernelHistoryService.getKernels({ uri: u1, viewType: 'foo' }); assert.equal(info.all.length, 0); @@ -119,9 +121,9 @@ suite('NotebookKernelHistoryService', () => { const k2 = new TestNotebookKernel({ label: 'a', viewType: 'foo' }); const k3 = new TestNotebookKernel({ label: 'b', viewType: 'foo' }); - kernelService.registerKernel(k1); - kernelService.registerKernel(k2); - kernelService.registerKernel(k3); + disposables.add(kernelService.registerKernel(k1)); + disposables.add(kernelService.registerKernel(k2)); + disposables.add(kernelService.registerKernel(k3)); instantiationService.stub(IStorageService, new class extends mock() { override onWillSaveState: Event = Event.None; @@ -153,7 +155,7 @@ suite('NotebookKernelHistoryService', () => { override debug() { } }); - const kernelHistoryService = instantiationService.createInstance(NotebookKernelHistoryService); + const kernelHistoryService = disposables.add(instantiationService.createInstance(NotebookKernelHistoryService)); let info = kernelHistoryService.getKernels({ uri: u1, viewType: 'foo' }); assert.equal(info.all.length, 1); assert.deepStrictEqual(info.selected, undefined); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookKernelService.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookKernelService.test.ts index 37b67d8f526..c964f7b59da 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookKernelService.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookKernelService.test.ts @@ -26,6 +26,9 @@ suite('NotebookKernelService', () => { let disposables: DisposableStore; let onDidAddNotebookDocument: Emitter; + teardown(() => { + disposables.dispose(); + }); setup(function () { disposables = new DisposableStore(); @@ -52,10 +55,6 @@ suite('NotebookKernelService', () => { instantiationService.set(INotebookKernelService, kernelService); }); - teardown(() => { - disposables.dispose(); - }); - test('notebook priorities', function () { const u1 = URI.parse('foo:///one'); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookSelection.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookSelection.test.ts index d661240cf3c..b1aa61118fd 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookSelection.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookSelection.test.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { DisposableStore } from 'vs/base/common/lifecycle'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { FoldingModel, updateFoldingStateAtIndex } from 'vs/workbench/contrib/notebook/browser/viewModel/foldingModel'; @@ -12,6 +11,8 @@ import { runDeleteAction } from 'vs/workbench/contrib/notebook/browser/controlle import { NotebookCellSelectionCollection } from 'vs/workbench/contrib/notebook/browser/viewModel/cellSelectionCollection'; import { CellEditType, CellKind, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { createNotebookCellList, setupInstantiationService, TestCell, withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { DisposableStore } from 'vs/base/common/lifecycle'; suite('NotebookSelection', () => { test('focus is never empty', function () { @@ -24,18 +25,22 @@ suite('NotebookSelection', () => { }); suite('NotebookCellList focus/selection', () => { - let disposables: DisposableStore; let instantiationService: TestInstantiationService; let languageService: ILanguageService; - suiteSetup(() => { + teardown(() => { + disposables.dispose(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + setup(() => { disposables = new DisposableStore(); instantiationService = setupInstantiationService(disposables); languageService = instantiationService.get(ILanguageService); }); - suiteTeardown(() => disposables.dispose()); test('notebook cell list setFocus', async function () { await withTestNotebook( @@ -43,8 +48,8 @@ suite('NotebookCellList focus/selection', () => { ['var a = 1;', 'javascript', CellKind.Code, [], {}], ['var b = 2;', 'javascript', CellKind.Code, [], {}] ], - (editor, viewModel) => { - const cellList = createNotebookCellList(instantiationService); + (editor, viewModel, ds) => { + const cellList = createNotebookCellList(instantiationService, ds); cellList.attachViewModel(viewModel); assert.strictEqual(cellList.length, 2); @@ -63,8 +68,8 @@ suite('NotebookCellList focus/selection', () => { ['var a = 1;', 'javascript', CellKind.Code, [], {}], ['var b = 2;', 'javascript', CellKind.Code, [], {}] ], - (editor, viewModel) => { - const cellList = createNotebookCellList(instantiationService); + (editor, viewModel, ds) => { + const cellList = createNotebookCellList(instantiationService, ds); cellList.attachViewModel(viewModel); assert.strictEqual(cellList.length, 2); @@ -84,8 +89,8 @@ suite('NotebookCellList focus/selection', () => { ['var a = 1;', 'javascript', CellKind.Code, [], {}], ['var b = 2;', 'javascript', CellKind.Code, [], {}] ], - (editor, viewModel) => { - const cellList = createNotebookCellList(instantiationService); + (editor, viewModel, ds) => { + const cellList = createNotebookCellList(instantiationService, ds); cellList.attachViewModel(viewModel); assert.strictEqual(cellList.length, 2); @@ -111,8 +116,8 @@ suite('NotebookCellList focus/selection', () => { ['var b = 2;', 'javascript', CellKind.Code, [], {}], ['# header c', 'markdown', CellKind.Markup, [], {}] ], - (editor, viewModel) => { - const cellList = createNotebookCellList(instantiationService); + (editor, viewModel, ds) => { + const cellList = createNotebookCellList(instantiationService, ds); cellList.attachViewModel(viewModel); assert.deepStrictEqual(viewModel.getFocus(), { start: 0, end: 1 }); assert.deepStrictEqual(viewModel.getSelections(), [{ start: 0, end: 1 }]); @@ -146,11 +151,11 @@ suite('NotebookCellList focus/selection', () => { ['var b = 2;', 'javascript', CellKind.Code, [], {}], ['# header c', 'markdown', CellKind.Markup, [], {}] ], - (editor, viewModel) => { - const foldingModel = new FoldingModel(); + (editor, viewModel, ds) => { + const foldingModel = ds.add(new FoldingModel()); foldingModel.attachViewModel(viewModel); - const cellList = createNotebookCellList(instantiationService); + const cellList = createNotebookCellList(instantiationService, ds); cellList.attachViewModel(viewModel); assert.strictEqual(cellList.length, 5); assert.deepStrictEqual(viewModel.getFocus(), { start: 0, end: 1 }); @@ -192,11 +197,11 @@ suite('NotebookCellList focus/selection', () => { ['# header d', 'markdown', CellKind.Markup, [], {}], ['var e = 4;', 'javascript', CellKind.Code, [], {}], ], - (editor, viewModel) => { - const foldingModel = new FoldingModel(); + (editor, viewModel, ds) => { + const foldingModel = ds.add(new FoldingModel()); foldingModel.attachViewModel(viewModel); - const cellList = createNotebookCellList(instantiationService); + const cellList = createNotebookCellList(instantiationService, ds); cellList.attachViewModel(viewModel); cellList.setFocus([0]); cellList.setSelection([0]); @@ -220,8 +225,8 @@ suite('NotebookCellList focus/selection', () => { // mimic undo editor.textModel.applyEdits([{ editType: CellEditType.Replace, index: 0, count: 0, cells: [ - new TestCell(viewModel.viewType, 7, '# header f', 'markdown', CellKind.Code, [], languageService), - new TestCell(viewModel.viewType, 8, 'var g = 5;', 'javascript', CellKind.Code, [], languageService) + ds.add(new TestCell(viewModel.viewType, 7, '# header f', 'markdown', CellKind.Code, [], languageService)), + ds.add(new TestCell(viewModel.viewType, 8, 'var g = 5;', 'javascript', CellKind.Code, [], languageService)) ] }], true, undefined, () => undefined, undefined, false); viewModel.updateFoldingRanges(foldingModel.regions); @@ -229,8 +234,6 @@ suite('NotebookCellList focus/selection', () => { assert.strictEqual(cellList.getModelIndex2(0), 0); assert.strictEqual(cellList.getModelIndex2(1), 1); assert.strictEqual(cellList.getModelIndex2(2), 2); - - }); }); @@ -243,11 +246,11 @@ suite('NotebookCellList focus/selection', () => { ['var b = 2;', 'javascript', CellKind.Code, [], {}], ['# header c', 'markdown', CellKind.Markup, [], {}] ], - (editor, viewModel) => { - const foldingModel = new FoldingModel(); + (editor, viewModel, ds) => { + const foldingModel = ds.add(new FoldingModel()); foldingModel.attachViewModel(viewModel); - const cellList = createNotebookCellList(instantiationService); + const cellList = createNotebookCellList(instantiationService, ds); cellList.attachViewModel(viewModel); updateFoldingStateAtIndex(foldingModel, 0, true); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookServiceImpl.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookServiceImpl.test.ts index 225069a56d7..f8e70728176 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookServiceImpl.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookServiceImpl.test.ts @@ -8,6 +8,7 @@ import { Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IFileService } from 'vs/platform/files/common/files'; @@ -21,9 +22,9 @@ import { IExtensionService, nullExtensionDescription } from 'vs/workbench/servic import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; suite('NotebookProviderInfoStore', function () { + const disposables = ensureNoDisposablesAreLeakedInTestSuite() as Pick; test('Can\'t open untitled notebooks in test #119363', function () { - const disposables = new DisposableStore(); const instantiationService = workbenchInstantiationService(undefined, disposables); const store = new NotebookProviderInfoStore( new class extends mock() { @@ -33,7 +34,7 @@ suite('NotebookProviderInfoStore', function () { new class extends mock() { override onDidRegisterExtensions = Event.None; }, - instantiationService.createInstance(EditorResolverService), + disposables.add(instantiationService.createInstance(EditorResolverService)), new TestConfigurationService(), new class extends mock() { override onDidChangeScreenReaderOptimized: Event = Event.None; @@ -44,6 +45,7 @@ suite('NotebookProviderInfoStore', function () { }, new class extends mock() { } ); + disposables.add(store); const fooInfo = new NotebookProviderInfo({ extension: nullExtensionDescription.identifier, @@ -87,8 +89,6 @@ suite('NotebookProviderInfoStore', function () { providers = store.getContributedNotebook(URI.parse('untitled:///test/nb.bar')); assert.strictEqual(providers.length, 1); assert.strictEqual(providers[0] === barInfo, true); - - disposables.dispose(); }); }); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookStickyScroll.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookStickyScroll.test.ts index 99cb8117b61..f80bdb205c4 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookStickyScroll.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookStickyScroll.test.ts @@ -4,11 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { isWeb } from 'vs/base/common/platform'; import { Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { isWeb } from 'vs/base/common/platform'; import { mock } from 'vs/base/test/common/mock'; import { assertSnapshot } from 'vs/base/test/common/snapshot'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { NotebookCellOutline } from 'vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline'; import { INotebookEditor, INotebookEditorPane } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; @@ -21,19 +22,22 @@ import { OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; (isWeb ? suite.skip : suite)('NotebookEditorStickyScroll', () => { - let disposables: DisposableStore; let instantiationService: TestInstantiationService; const domNode: HTMLElement = document.createElement('div'); - suiteSetup(() => { + teardown(() => { + disposables.dispose(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + setup(() => { disposables = new DisposableStore(); instantiationService = setupInstantiationService(disposables); }); - suiteTeardown(() => disposables.dispose()); - function getOutline(editor: any) { if (!editor.hasModel()) { assert.ok(false, 'MUST have active text editor'); @@ -47,8 +51,11 @@ import { OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; return outline; } - function nbStickyTestHelper(domNode: HTMLElement, notebookEditor: INotebookEditor, notebookCellList: INotebookCellList, notebookOutlineEntries: OutlineEntry[]) { + function nbStickyTestHelper(domNode: HTMLElement, notebookEditor: INotebookEditor, notebookCellList: INotebookCellList, notebookOutlineEntries: OutlineEntry[], disposables: Pick) { const output = computeContent(domNode, notebookEditor, notebookCellList, notebookOutlineEntries); + for (const stickyLine of output.values()) { + disposables.add(stickyLine.line); + } return createStickyTestElement(output.values()); } @@ -84,16 +91,18 @@ import { OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; collapsedOutputCells: {}, }); - const cellList = createNotebookCellList(instantiationService); + const cellList = disposables.add(createNotebookCellList(instantiationService, disposables)); cellList.attachViewModel(viewModel); cellList.layout(400, 100); editor.setScrollTop(0); editor.visibleRanges = [{ start: 0, end: 8 }]; - const notebookOutlineEntries = getOutline(editor).entries; - const resultingMap = nbStickyTestHelper(domNode, editor, cellList, notebookOutlineEntries); + const outline = getOutline(editor); + const notebookOutlineEntries = outline.entries; + const resultingMap = nbStickyTestHelper(domNode, editor, cellList, notebookOutlineEntries, disposables); await assertSnapshot(resultingMap); + outline.dispose(); }); }); @@ -109,7 +118,7 @@ import { OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; ['# header b', 'markdown', CellKind.Markup, [], {}], // 300 ['var c = 2;', 'javascript', CellKind.Code, [], {}] // 350 ], - async (editor, viewModel) => { + async (editor, viewModel, ds) => { viewModel.restoreEditorViewState({ editingCells: Array.from({ length: 8 }, () => false), editorViewStates: Array.from({ length: 8 }, () => null), @@ -119,17 +128,19 @@ import { OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; collapsedOutputCells: {}, }); - const cellList = createNotebookCellList(instantiationService); + const cellList = ds.add(createNotebookCellList(instantiationService, ds)); cellList.attachViewModel(viewModel); cellList.layout(400, 100); editor.setScrollTop(175); editor.visibleRanges = [{ start: 3, end: 8 }]; - const notebookOutlineEntries = getOutline(editor).entries; - const resultingMap = nbStickyTestHelper(domNode, editor, cellList, notebookOutlineEntries); + const outline = getOutline(editor); + const notebookOutlineEntries = outline.entries; + const resultingMap = nbStickyTestHelper(domNode, editor, cellList, notebookOutlineEntries, ds); await assertSnapshot(resultingMap); + outline.dispose(); }); }); @@ -146,7 +157,7 @@ import { OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; ['# header b', 'markdown', CellKind.Markup, [], {}], // 350 ['var c = 2;', 'javascript', CellKind.Code, [], {}] // 400 ], - async (editor, viewModel) => { + async (editor, viewModel, ds) => { viewModel.restoreEditorViewState({ editingCells: Array.from({ length: 9 }, () => false), editorViewStates: Array.from({ length: 9 }, () => null), @@ -156,17 +167,19 @@ import { OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; collapsedOutputCells: {}, }); - const cellList = createNotebookCellList(instantiationService); + const cellList = ds.add(createNotebookCellList(instantiationService, ds)); cellList.attachViewModel(viewModel); cellList.layout(400, 100); editor.setScrollTop(325); // room for a single header editor.visibleRanges = [{ start: 6, end: 9 }]; - const notebookOutlineEntries = getOutline(editor).entries; - const resultingMap = nbStickyTestHelper(domNode, editor, cellList, notebookOutlineEntries); + const outline = getOutline(editor); + const notebookOutlineEntries = outline.entries; + const resultingMap = nbStickyTestHelper(domNode, editor, cellList, notebookOutlineEntries, ds); await assertSnapshot(resultingMap); + outline.dispose(); }); }); @@ -184,7 +197,7 @@ import { OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; ['# header b', 'markdown', CellKind.Markup, [], {}], // 400 ['var c = 2;', 'javascript', CellKind.Code, [], {}] // 450 ], - async (editor, viewModel) => { + async (editor, viewModel, ds) => { viewModel.restoreEditorViewState({ editingCells: Array.from({ length: 10 }, () => false), editorViewStates: Array.from({ length: 10 }, () => null), @@ -194,17 +207,19 @@ import { OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; collapsedOutputCells: {}, }); - const cellList = createNotebookCellList(instantiationService); + const cellList = ds.add(createNotebookCellList(instantiationService, ds)); cellList.attachViewModel(viewModel); cellList.layout(400, 100); editor.setScrollTop(175); // room for a single header editor.visibleRanges = [{ start: 3, end: 10 }]; - const notebookOutlineEntries = getOutline(editor).entries; - const resultingMap = nbStickyTestHelper(domNode, editor, cellList, notebookOutlineEntries); + const outline = getOutline(editor); + const notebookOutlineEntries = outline.entries; + const resultingMap = nbStickyTestHelper(domNode, editor, cellList, notebookOutlineEntries, ds); await assertSnapshot(resultingMap); + outline.dispose(); }); }); @@ -221,7 +236,7 @@ import { OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; ['# header b', 'markdown', CellKind.Markup, [], {}], ['var c = 2;', 'javascript', CellKind.Code, [], {}] ], - async (editor, viewModel) => { + async (editor, viewModel, ds) => { viewModel.restoreEditorViewState({ editingCells: Array.from({ length: 8 }, () => false), editorViewStates: Array.from({ length: 8 }, () => null), @@ -231,17 +246,19 @@ import { OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; collapsedOutputCells: {}, }); - const cellList = createNotebookCellList(instantiationService); + const cellList = ds.add(createNotebookCellList(instantiationService, ds)); cellList.attachViewModel(viewModel); cellList.layout(400, 100); editor.setScrollTop(50); editor.visibleRanges = [{ start: 0, end: 8 }]; - const notebookOutlineEntries = getOutline(editor).entries; - const resultingMap = nbStickyTestHelper(domNode, editor, cellList, notebookOutlineEntries); + const outline = getOutline(editor); + const notebookOutlineEntries = outline.entries; + const resultingMap = nbStickyTestHelper(domNode, editor, cellList, notebookOutlineEntries, ds); await assertSnapshot(resultingMap); + outline.dispose(); }); }); @@ -260,7 +277,7 @@ import { OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; ['# header b', 'markdown', CellKind.Markup, [], {}], ['var c = 2;', 'javascript', CellKind.Code, [], {}] ], - async (editor, viewModel) => { + async (editor, viewModel, ds) => { viewModel.restoreEditorViewState({ editingCells: Array.from({ length: 10 }, () => false), editorViewStates: Array.from({ length: 10 }, () => null), @@ -270,17 +287,19 @@ import { OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; collapsedOutputCells: {}, }); - const cellList = createNotebookCellList(instantiationService); + const cellList = ds.add(createNotebookCellList(instantiationService, ds)); cellList.attachViewModel(viewModel); cellList.layout(400, 100); editor.setScrollTop(125); editor.visibleRanges = [{ start: 2, end: 10 }]; - const notebookOutlineEntries = getOutline(editor).entries; - const resultingMap = nbStickyTestHelper(domNode, editor, cellList, notebookOutlineEntries); + const outline = getOutline(editor); + const notebookOutlineEntries = outline.entries; + const resultingMap = nbStickyTestHelper(domNode, editor, cellList, notebookOutlineEntries, ds); await assertSnapshot(resultingMap); + outline.dispose(); }); }); @@ -299,7 +318,7 @@ import { OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; ['### header bbb', 'markdown', CellKind.Markup, [], {}], ['var c = 2;', 'javascript', CellKind.Code, [], {}] ], - async (editor, viewModel) => { + async (editor, viewModel, ds) => { viewModel.restoreEditorViewState({ editingCells: Array.from({ length: 10 }, () => false), editorViewStates: Array.from({ length: 10 }, () => null), @@ -309,17 +328,19 @@ import { OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; collapsedOutputCells: {}, }); - const cellList = createNotebookCellList(instantiationService); + const cellList = ds.add(createNotebookCellList(instantiationService, ds)); cellList.attachViewModel(viewModel); cellList.layout(400, 100); editor.setScrollTop(375); editor.visibleRanges = [{ start: 7, end: 10 }]; - const notebookOutlineEntries = getOutline(editor).entries; - const resultingMap = nbStickyTestHelper(domNode, editor, cellList, notebookOutlineEntries); + const outline = getOutline(editor); + const notebookOutlineEntries = outline.entries; + const resultingMap = nbStickyTestHelper(domNode, editor, cellList, notebookOutlineEntries, ds); await assertSnapshot(resultingMap); + outline.dispose(); }); }); @@ -340,7 +361,7 @@ import { OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; ['### header bbb', 'markdown', CellKind.Markup, [], {}], ['var c = 2;', 'javascript', CellKind.Code, [], {}] ], - async (editor, viewModel) => { + async (editor, viewModel, ds) => { viewModel.restoreEditorViewState({ editingCells: Array.from({ length: 12 }, () => false), editorViewStates: Array.from({ length: 12 }, () => null), @@ -350,17 +371,19 @@ import { OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; collapsedOutputCells: {}, }); - const cellList = createNotebookCellList(instantiationService); + const cellList = ds.add(createNotebookCellList(instantiationService, ds)); cellList.attachViewModel(viewModel); cellList.layout(400, 100); editor.setScrollTop(350); editor.visibleRanges = [{ start: 7, end: 12 }]; - const notebookOutlineEntries = getOutline(editor).entries; - const resultingMap = nbStickyTestHelper(domNode, editor, cellList, notebookOutlineEntries); + const outline = getOutline(editor); + const notebookOutlineEntries = outline.entries; + const resultingMap = nbStickyTestHelper(domNode, editor, cellList, notebookOutlineEntries, ds); await assertSnapshot(resultingMap); + outline.dispose(); }); }); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookTextModel.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookTextModel.test.ts index 184e3ea1951..46e50d6ed4e 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookTextModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookTextModel.test.ts @@ -39,8 +39,8 @@ suite('NotebookTextModel', () => { (editor) => { const textModel = editor.textModel; textModel.applyEdits([ - { editType: CellEditType.Replace, index: 1, count: 0, cells: [new TestCell(textModel.viewType, 5, 'var e = 5;', 'javascript', CellKind.Code, [], languageService)] }, - { editType: CellEditType.Replace, index: 3, count: 0, cells: [new TestCell(textModel.viewType, 6, 'var f = 6;', 'javascript', CellKind.Code, [], languageService)] }, + { editType: CellEditType.Replace, index: 1, count: 0, cells: [disposables.add(new TestCell(textModel.viewType, 5, 'var e = 5;', 'javascript', CellKind.Code, [], languageService))] }, + { editType: CellEditType.Replace, index: 3, count: 0, cells: [disposables.add(new TestCell(textModel.viewType, 6, 'var f = 6;', 'javascript', CellKind.Code, [], languageService))] }, ], true, undefined, () => undefined, undefined, true); assert.strictEqual(textModel.cells.length, 6); @@ -62,8 +62,8 @@ suite('NotebookTextModel', () => { (editor) => { const textModel = editor.textModel; textModel.applyEdits([ - { editType: CellEditType.Replace, index: 1, count: 0, cells: [new TestCell(textModel.viewType, 5, 'var e = 5;', 'javascript', CellKind.Code, [], languageService)] }, - { editType: CellEditType.Replace, index: 1, count: 0, cells: [new TestCell(textModel.viewType, 6, 'var f = 6;', 'javascript', CellKind.Code, [], languageService)] }, + { editType: CellEditType.Replace, index: 1, count: 0, cells: [disposables.add(new TestCell(textModel.viewType, 5, 'var e = 5;', 'javascript', CellKind.Code, [], languageService))] }, + { editType: CellEditType.Replace, index: 1, count: 0, cells: [disposables.add(new TestCell(textModel.viewType, 6, 'var f = 6;', 'javascript', CellKind.Code, [], languageService))] }, ], true, undefined, () => undefined, undefined, true); assert.strictEqual(textModel.cells.length, 6); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts index b5134c88bdf..13dc30523c7 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts @@ -28,8 +28,11 @@ import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { NotebookEditorTestModel, setupInstantiationService, withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { IBaseCellEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('NotebookViewModel', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + let disposables: DisposableStore; let instantiationService: TestInstantiationService; let textModelService: ITextModelService; @@ -58,9 +61,16 @@ suite('NotebookViewModel', () => { test('ctor', function () { const notebook = new NotebookTextModel('notebook', URI.parse('test'), [], {}, { transientCellMetadata: {}, transientDocumentMetadata: {}, transientOutputs: false, cellContentMetadata: {} }, undoRedoService, modelService, languageService); const model = new NotebookEditorTestModel(notebook); - const viewContext = new ViewContext(new NotebookOptions(instantiationService.get(IConfigurationService), instantiationService.get(INotebookExecutionStateService), false), new NotebookEventDispatcher(), () => ({} as IBaseCellEditorOptions)); + const options = new NotebookOptions(instantiationService.get(IConfigurationService), instantiationService.get(INotebookExecutionStateService), false); + const eventDispatcher = new NotebookEventDispatcher(); + const viewContext = new ViewContext(options, eventDispatcher, () => ({} as IBaseCellEditorOptions)); const viewModel = new NotebookViewModel('notebook', model.notebook, viewContext, null, { isReadOnly: false }, instantiationService, bulkEditService, undoRedoService, textModelService, notebookExecutionStateService); assert.strictEqual(viewModel.viewType, 'notebook'); + notebook.dispose(); + model.dispose(); + options.dispose(); + eventDispatcher.dispose(); + viewModel.dispose(); }); test('insert/delete', async function () { @@ -79,6 +89,9 @@ suite('NotebookViewModel', () => { assert.strictEqual(viewModel.length, 2); assert.strictEqual(viewModel.notebookDocument.cells.length, 2); assert.strictEqual(viewModel.getCellIndex(cell), -1); + + cell.dispose(); + cell.model.dispose(); } ); }); @@ -105,6 +118,11 @@ suite('NotebookViewModel', () => { assert.strictEqual(viewModel.length, 3); assert.strictEqual(viewModel.notebookDocument.cells.length, 3); assert.strictEqual(viewModel.getCellIndex(cell2), 2); + + cell.dispose(); + cell.model.dispose(); + cell2.dispose(); + cell2.model.dispose(); } ); }); @@ -136,6 +154,8 @@ function getVisibleCells(cells: T[], hiddenRanges: ICellRange[]) { } suite('NotebookViewModel Decorations', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + test('tracking range', async function () { await withTestNotebook( [ @@ -153,7 +173,7 @@ suite('NotebookViewModel Decorations', () => { end: 2, }); - insertCellAtIndex(viewModel, 0, 'var d = 6;', 'javascript', CellKind.Code, {}, [], true, true); + const cell1 = insertCellAtIndex(viewModel, 0, 'var d = 6;', 'javascript', CellKind.Code, {}, [], true, true); assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), { start: 2, @@ -167,7 +187,7 @@ suite('NotebookViewModel Decorations', () => { end: 2 }); - insertCellAtIndex(viewModel, 3, 'var d = 7;', 'javascript', CellKind.Code, {}, [], true, true); + const cell2 = insertCellAtIndex(viewModel, 3, 'var d = 7;', 'javascript', CellKind.Code, {}, [], true, true); assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), { start: 1, @@ -187,6 +207,11 @@ suite('NotebookViewModel Decorations', () => { end: 1 }); + + cell1.dispose(); + cell1.model.dispose(); + cell2.dispose(); + cell2.model.dispose(); } ); }); @@ -268,13 +293,11 @@ suite('NotebookViewModel Decorations', () => { return original.indexOf(a) >= 0; }), [{ start: 1, deleteCount: 1, toInsert: [2, 6] }]); }); - - test('hidden ranges', async function () { - - }); }); suite('NotebookViewModel API', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + test('#115432, get nearest code cell', async function () { await withTestNotebook( [ diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookWorkbenchToolbar.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookWorkbenchToolbar.test.ts index f36258029d7..34c0a8c1a01 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookWorkbenchToolbar.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookWorkbenchToolbar.test.ts @@ -6,6 +6,7 @@ import { workbenchCalculateActions, workbenchDynamicCalculateActions } from 'vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar'; import { Action, IAction, Separator } from 'vs/base/common/actions'; import * as assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; interface IActionModel { action: IAction; @@ -25,6 +26,7 @@ interface IActionModel { * ex: action with size 50 requires 58px of space */ suite('Workbench Toolbar calculateActions (strategy always + never)', () => { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); const defaultSecondaryActionModels: IActionModel[] = [ { action: new Action('secondaryAction0', 'Secondary Action 0'), size: 50, visible: true, renderLabel: true }, @@ -34,6 +36,9 @@ suite('Workbench Toolbar calculateActions (strategy always + never)', () => { const defaultSecondaryActions: IAction[] = defaultSecondaryActionModels.map(action => action.action); const separator: IActionModel = { action: new Separator(), size: 1, visible: true, renderLabel: true }; + setup(function () { + defaultSecondaryActionModels.forEach(action => disposables.add(action.action)); + }); test('should return empty primary and secondary actions when given empty initial actions', () => { const result = workbenchCalculateActions([], [], 100); @@ -43,9 +48,9 @@ suite('Workbench Toolbar calculateActions (strategy always + never)', () => { test('should return all primary actions when they fit within the container width', () => { const actions: IActionModel[] = [ - { action: new Action('action0', 'Action 0'), size: 50, visible: true, renderLabel: true }, - { action: new Action('action1', 'Action 1'), size: 50, visible: true, renderLabel: true }, - { action: new Action('action2', 'Action 2'), size: 50, visible: true, renderLabel: true }, + { action: disposables.add(new Action('action0', 'Action 0')), size: 50, visible: true, renderLabel: true }, + { action: disposables.add(new Action('action1', 'Action 1')), size: 50, visible: true, renderLabel: true }, + { action: disposables.add(new Action('action2', 'Action 2')), size: 50, visible: true, renderLabel: true }, ]; const result = workbenchCalculateActions(actions, defaultSecondaryActions, 200); assert.deepEqual(result.primaryActions, actions); @@ -54,9 +59,9 @@ suite('Workbench Toolbar calculateActions (strategy always + never)', () => { test('should move actions to secondary when they do not fit within the container width', () => { const actions: IActionModel[] = [ - { action: new Action('action0', 'Action 0'), size: 50, visible: true, renderLabel: true }, - { action: new Action('action1', 'Action 1'), size: 50, visible: true, renderLabel: true }, - { action: new Action('action2', 'Action 2'), size: 50, visible: true, renderLabel: true }, + { action: disposables.add(new Action('action0', 'Action 0')), size: 50, visible: true, renderLabel: true }, + { action: disposables.add(new Action('action1', 'Action 1')), size: 50, visible: true, renderLabel: true }, + { action: disposables.add(new Action('action2', 'Action 2')), size: 50, visible: true, renderLabel: true }, ]; const result = workbenchCalculateActions(actions, defaultSecondaryActions, 100); assert.deepEqual(result.primaryActions, [actions[0]]); @@ -65,10 +70,10 @@ suite('Workbench Toolbar calculateActions (strategy always + never)', () => { test('should ignore second separator when two separators are in a row', () => { const actions: IActionModel[] = [ - { action: new Action('action0', 'Action 0'), size: 50, visible: true, renderLabel: true }, + { action: disposables.add(new Action('action0', 'Action 0')), size: 50, visible: true, renderLabel: true }, { action: new Separator(), size: 1, visible: true, renderLabel: true }, { action: new Separator(), size: 1, visible: true, renderLabel: true }, - { action: new Action('action1', 'Action 1'), size: 50, visible: true, renderLabel: true }, + { action: disposables.add(new Action('action1', 'Action 1')), size: 50, visible: true, renderLabel: true }, ]; const result = workbenchCalculateActions(actions, defaultSecondaryActions, 125); assert.deepEqual(result.primaryActions, [actions[0], actions[1], actions[3]]); @@ -77,9 +82,9 @@ suite('Workbench Toolbar calculateActions (strategy always + never)', () => { test('should ignore separators when they are at the end of the resulting primary actions', () => { const actions: IActionModel[] = [ - { action: new Action('action0', 'Action 0'), size: 50, visible: true, renderLabel: true }, + { action: disposables.add(new Action('action0', 'Action 0')), size: 50, visible: true, renderLabel: true }, { action: new Separator(), size: 1, visible: true, renderLabel: true }, - { action: new Action('action1', 'Action 1'), size: 50, visible: true, renderLabel: true }, + { action: disposables.add(new Action('action1', 'Action 1')), size: 50, visible: true, renderLabel: true }, { action: new Separator(), size: 1, visible: true, renderLabel: true }, ]; const result = workbenchCalculateActions(actions, defaultSecondaryActions, 200); @@ -89,10 +94,10 @@ suite('Workbench Toolbar calculateActions (strategy always + never)', () => { test('should keep actions with size 0 in primary actions', () => { const actions: IActionModel[] = [ - { action: new Action('action0', 'Action 0'), size: 50, visible: true, renderLabel: true }, - { action: new Action('action1', 'Action 1'), size: 50, visible: true, renderLabel: true }, - { action: new Action('action2', 'Action 2'), size: 50, visible: true, renderLabel: true }, - { action: new Action('action3', 'Action 3'), size: 0, visible: true, renderLabel: true }, + { action: disposables.add(new Action('action0', 'Action 0')), size: 50, visible: true, renderLabel: true }, + { action: disposables.add(new Action('action1', 'Action 1')), size: 50, visible: true, renderLabel: true }, + { action: disposables.add(new Action('action2', 'Action 2')), size: 50, visible: true, renderLabel: true }, + { action: disposables.add(new Action('action3', 'Action 3')), size: 0, visible: true, renderLabel: true }, ]; const result = workbenchCalculateActions(actions, defaultSecondaryActions, 116); assert.deepEqual(result.primaryActions, [actions[0], actions[1], actions[3]]); @@ -101,9 +106,9 @@ suite('Workbench Toolbar calculateActions (strategy always + never)', () => { test('should not render separator if preceeded by size 0 action(s).', () => { const actions: IActionModel[] = [ - { action: new Action('action0', 'Action 0'), size: 0, visible: true, renderLabel: true }, + { action: disposables.add(new Action('action0', 'Action 0')), size: 0, visible: true, renderLabel: true }, { action: new Separator(), size: 1, visible: true, renderLabel: true }, - { action: new Action('action1', 'Action 1'), size: 50, visible: true, renderLabel: true }, + { action: disposables.add(new Action('action1', 'Action 1')), size: 50, visible: true, renderLabel: true }, ]; const result = workbenchCalculateActions(actions, defaultSecondaryActions, 116); assert.deepEqual(result.primaryActions, [actions[0], actions[2]]); @@ -112,12 +117,12 @@ suite('Workbench Toolbar calculateActions (strategy always + never)', () => { test('should not render second separator if space between is hidden (size 0) actions.', () => { const actions: IActionModel[] = [ - { action: new Action('action0', 'Action 0'), size: 50, visible: true, renderLabel: true }, + { action: disposables.add(new Action('action0', 'Action 0')), size: 50, visible: true, renderLabel: true }, { action: new Separator(), size: 1, visible: true, renderLabel: true }, - { action: new Action('action1', 'Action 1'), size: 0, visible: true, renderLabel: true }, - { action: new Action('action2', 'Action 2'), size: 0, visible: true, renderLabel: true }, + { action: disposables.add(new Action('action1', 'Action 1')), size: 0, visible: true, renderLabel: true }, + { action: disposables.add(new Action('action2', 'Action 2')), size: 0, visible: true, renderLabel: true }, { action: new Separator(), size: 1, visible: true, renderLabel: true }, - { action: new Action('action3', 'Action 3'), size: 50, visible: true, renderLabel: true }, + { action: disposables.add(new Action('action3', 'Action 3')), size: 50, visible: true, renderLabel: true }, ]; const result = workbenchCalculateActions(actions, defaultSecondaryActions, 300); assert.deepEqual(result.primaryActions, [actions[0], actions[1], actions[2], actions[3], actions[5]]); @@ -126,6 +131,7 @@ suite('Workbench Toolbar calculateActions (strategy always + never)', () => { }); suite('Workbench Toolbar Dynamic calculateActions (strategy dynamic)', () => { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); const actionTemplate = [ new Action('action0', 'Action 0'), @@ -141,6 +147,9 @@ suite('Workbench Toolbar Dynamic calculateActions (strategy dynamic)', () => { ]; const defaultSecondaryActions: IAction[] = defaultSecondaryActionModels.map(action => action.action); + setup(function () { + defaultSecondaryActionModels.forEach(action => disposables.add(action.action)); + }); test('should return empty primary and secondary actions when given empty initial actions', () => { const result = workbenchDynamicCalculateActions([], [], 100); diff --git a/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts index c7bcfcf6d3b..d9dc02d5bc5 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts @@ -58,9 +58,8 @@ import { NotebookOptions } from 'vs/workbench/contrib/notebook/browser/notebookO import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { TextModelResolverService } from 'vs/workbench/services/textmodelResolver/common/textModelResolverService'; import { IWorkingCopySaveEvent } from 'vs/workbench/services/workingCopy/common/workingCopy'; -import { TestWorkspaceTrustRequestService } from 'vs/workbench/services/workspaces/test/common/testWorkspaceTrustService'; import { TestLayoutService } from 'vs/workbench/test/browser/workbenchTestServices'; -import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; +import { TestStorageService, TestWorkspaceTrustRequestService } from 'vs/workbench/test/common/workbenchTestServices'; import { FontInfo } from 'vs/editor/common/config/fontInfo'; import { EditorFontLigatures, EditorFontVariations } from 'vs/editor/common/config/editorOptions'; @@ -174,33 +173,33 @@ export class NotebookEditorTestModel extends EditorModel implements INotebookEdi } } -export function setupInstantiationService(disposables = new DisposableStore()) { +export function setupInstantiationService(disposables: DisposableStore) { const instantiationService = disposables.add(new TestInstantiationService()); instantiationService.stub(ILanguageService, disposables.add(new LanguageService())); instantiationService.stub(IUndoRedoService, instantiationService.createInstance(UndoRedoService)); instantiationService.stub(IConfigurationService, new TestConfigurationService()); instantiationService.stub(IThemeService, new TestThemeService()); - instantiationService.stub(ILanguageConfigurationService, new TestLanguageConfigurationService()); - instantiationService.stub(IModelService, instantiationService.createInstance(ModelService)); - instantiationService.stub(ITextModelService, instantiationService.createInstance(TextModelResolverService)); - instantiationService.stub(IContextKeyService, instantiationService.createInstance(ContextKeyService)); - instantiationService.stub(IListService, instantiationService.createInstance(ListService)); + instantiationService.stub(ILanguageConfigurationService, disposables.add(new TestLanguageConfigurationService())); + instantiationService.stub(IModelService, disposables.add(instantiationService.createInstance(ModelService))); + instantiationService.stub(ITextModelService, disposables.add(instantiationService.createInstance(TextModelResolverService))); + instantiationService.stub(IContextKeyService, disposables.add(instantiationService.createInstance(ContextKeyService))); + instantiationService.stub(IListService, disposables.add(instantiationService.createInstance(ListService))); instantiationService.stub(ILayoutService, new TestLayoutService()); instantiationService.stub(ILogService, new NullLogService()); instantiationService.stub(IClipboardService, TestClipboardService); - instantiationService.stub(IStorageService, new TestStorageService()); - instantiationService.stub(IWorkspaceTrustRequestService, new TestWorkspaceTrustRequestService(true)); + instantiationService.stub(IStorageService, disposables.add(new TestStorageService())); + instantiationService.stub(IWorkspaceTrustRequestService, disposables.add(new TestWorkspaceTrustRequestService(true))); instantiationService.stub(INotebookExecutionStateService, new TestNotebookExecutionStateService()); instantiationService.stub(IKeybindingService, new MockKeybindingService()); - instantiationService.stub(INotebookCellStatusBarService, new NotebookCellStatusBarService()); + instantiationService.stub(INotebookCellStatusBarService, disposables.add(new NotebookCellStatusBarService())); return instantiationService; } -function _createTestNotebookEditor(instantiationService: TestInstantiationService, cells: [source: string, lang: string, kind: CellKind, output?: IOutputDto[], metadata?: NotebookCellMetadata][]): { editor: IActiveNotebookEditorDelegate; viewModel: NotebookViewModel } { +function _createTestNotebookEditor(instantiationService: TestInstantiationService, disposables: DisposableStore, cells: [source: string, lang: string, kind: CellKind, output?: IOutputDto[], metadata?: NotebookCellMetadata][]): { editor: IActiveNotebookEditorDelegate; viewModel: NotebookViewModel } { const viewType = 'notebook'; - const notebook = instantiationService.createInstance(NotebookTextModel, viewType, URI.parse('test'), cells.map((cell): ICellDto2 => { + const notebook = disposables.add(instantiationService.createInstance(NotebookTextModel, viewType, URI.parse('test'), cells.map((cell): ICellDto2 => { return { source: cell[0], mime: undefined, @@ -209,16 +208,16 @@ function _createTestNotebookEditor(instantiationService: TestInstantiationServic outputs: cell[3] ?? [], metadata: cell[4] }; - }), {}, { transientCellMetadata: {}, transientDocumentMetadata: {}, cellContentMetadata: {}, transientOutputs: false }); + }), {}, { transientCellMetadata: {}, transientDocumentMetadata: {}, cellContentMetadata: {}, transientOutputs: false })); - const model = new NotebookEditorTestModel(notebook); - const notebookOptions = new NotebookOptions(instantiationService.get(IConfigurationService), instantiationService.get(INotebookExecutionStateService), false); - const viewContext = new ViewContext(notebookOptions, new NotebookEventDispatcher(), () => ({} as IBaseCellEditorOptions)); - const viewModel: NotebookViewModel = instantiationService.createInstance(NotebookViewModel, viewType, model.notebook, viewContext, null, { isReadOnly: false }); + const model = disposables.add(new NotebookEditorTestModel(notebook)); + const notebookOptions = disposables.add(new NotebookOptions(instantiationService.get(IConfigurationService), instantiationService.get(INotebookExecutionStateService), false)); + const viewContext = new ViewContext(notebookOptions, disposables.add(new NotebookEventDispatcher()), () => ({} as IBaseCellEditorOptions)); + const viewModel: NotebookViewModel = disposables.add(instantiationService.createInstance(NotebookViewModel, viewType, model.notebook, viewContext, null, { isReadOnly: false })); - const cellList = createNotebookCellList(instantiationService, viewContext); + const cellList = disposables.add(createNotebookCellList(instantiationService, disposables, viewContext)); cellList.attachViewModel(viewModel); - const listViewInfoAccessor = new ListViewInfoAccessor(cellList); + const listViewInfoAccessor = disposables.add(new ListViewInfoAccessor(cellList)); let visibleRanges: ICellRange[] = [{ start: 0, end: 100 }]; @@ -340,15 +339,14 @@ function _createTestNotebookEditor(instantiationService: TestInstantiationServic return { editor: notebookEditor, viewModel }; } -export function createTestNotebookEditor(instantiationService: TestInstantiationService, cells: [source: string, lang: string, kind: CellKind, output?: IOutputDto[], metadata?: NotebookCellMetadata][]): { editor: INotebookEditorDelegate; viewModel: NotebookViewModel } { - return _createTestNotebookEditor(instantiationService, cells); +export function createTestNotebookEditor(instantiationService: TestInstantiationService, disposables: DisposableStore, cells: [source: string, lang: string, kind: CellKind, output?: IOutputDto[], metadata?: NotebookCellMetadata][]): { editor: INotebookEditorDelegate; viewModel: NotebookViewModel } { + return _createTestNotebookEditor(instantiationService, disposables, cells); } -export async function withTestNotebookDiffModel(originalCells: [source: string, lang: string, kind: CellKind, output?: IOutputDto[], metadata?: NotebookCellMetadata][], modifiedCells: [source: string, lang: string, kind: CellKind, output?: IOutputDto[], metadata?: NotebookCellMetadata][], callback: (diffModel: INotebookDiffEditorModel, accessor: TestInstantiationService) => Promise | R): Promise { - const disposables = new DisposableStore(); +export async function withTestNotebookDiffModel(disposables: DisposableStore, originalCells: [source: string, lang: string, kind: CellKind, output?: IOutputDto[], metadata?: NotebookCellMetadata][], modifiedCells: [source: string, lang: string, kind: CellKind, output?: IOutputDto[], metadata?: NotebookCellMetadata][], callback: (diffModel: INotebookDiffEditorModel, accessor: TestInstantiationService) => Promise | R): Promise { const instantiationService = setupInstantiationService(disposables); - const originalNotebook = createTestNotebookEditor(instantiationService, originalCells); - const modifiedNotebook = createTestNotebookEditor(instantiationService, modifiedCells); + const originalNotebook = createTestNotebookEditor(instantiationService, disposables, originalCells); + const modifiedNotebook = createTestNotebookEditor(instantiationService, disposables, modifiedCells); const originalResource = new class extends mock() { override get notebook() { return originalNotebook.viewModel.notebookDocument; @@ -393,29 +391,31 @@ interface IActiveTestNotebookEditorDelegate extends IActiveNotebookEditorDelegat visibleRanges: ICellRange[]; } -export async function withTestNotebook(cells: [source: string, lang: string, kind: CellKind, output?: IOutputDto[], metadata?: NotebookCellMetadata][], callback: (editor: IActiveTestNotebookEditorDelegate, viewModel: NotebookViewModel, accessor: TestInstantiationService) => Promise | R, accessor?: TestInstantiationService): Promise { - const disposables = new DisposableStore(); +export async function withTestNotebook(cells: [source: string, lang: string, kind: CellKind, output?: IOutputDto[], metadata?: NotebookCellMetadata][], callback: (editor: IActiveTestNotebookEditorDelegate, viewModel: NotebookViewModel, disposables: DisposableStore, accessor: TestInstantiationService) => Promise | R, accessor?: TestInstantiationService): Promise { + const disposables: DisposableStore = new DisposableStore(); const instantiationService = accessor ?? setupInstantiationService(disposables); - const notebookEditor = _createTestNotebookEditor(instantiationService, cells); + const notebookEditor = _createTestNotebookEditor(instantiationService, disposables, cells); return runWithFakedTimers({ useFakeTimers: true }, async () => { - const res = await callback(notebookEditor.editor, notebookEditor.viewModel, instantiationService); + const res = await callback(notebookEditor.editor, notebookEditor.viewModel, disposables, instantiationService); if (res instanceof Promise) { res.finally(() => { notebookEditor.editor.dispose(); notebookEditor.viewModel.dispose(); + notebookEditor.editor.textModel.dispose(); disposables.dispose(); }); } else { notebookEditor.editor.dispose(); notebookEditor.viewModel.dispose(); + notebookEditor.editor.textModel.dispose(); disposables.dispose(); } return res; }); } -export function createNotebookCellList(instantiationService: TestInstantiationService, viewContext?: ViewContext) { +export function createNotebookCellList(instantiationService: TestInstantiationService, disposables: DisposableStore, viewContext?: ViewContext) { const delegate: IListVirtualDelegate = { getHeight(element: CellViewModel) { return element.getHeight(17); }, getTemplateId() { return 'template'; } @@ -428,11 +428,11 @@ export function createNotebookCellList(instantiationService: TestInstantiationSe disposeTemplate() { } }; - const cellList: NotebookCellList = instantiationService.createInstance( + const cellList: NotebookCellList = disposables.add(instantiationService.createInstance( NotebookCellList, 'NotebookCellList', DOM.$('container'), - viewContext ?? new ViewContext(new NotebookOptions(instantiationService.get(IConfigurationService), instantiationService.get(INotebookExecutionStateService), false), new NotebookEventDispatcher(), () => ({} as IBaseCellEditorOptions)), + viewContext ?? new ViewContext(disposables.add(new NotebookOptions(instantiationService.get(IConfigurationService), instantiationService.get(INotebookExecutionStateService), false)), disposables.add(new NotebookEventDispatcher()), () => ({} as IBaseCellEditorOptions)), delegate, [renderer], instantiationService.get(IContextKeyService), @@ -440,7 +440,7 @@ export function createNotebookCellList(instantiationService: TestInstantiationSe supportDynamicHeights: true, multipleSelectionSupport: true, } - ); + )); return cellList; } diff --git a/src/vs/workbench/contrib/output/test/browser/outputLinkProvider.test.ts b/src/vs/workbench/contrib/output/test/browser/outputLinkProvider.test.ts index 594c21561eb..1c92342f5bb 100644 --- a/src/vs/workbench/contrib/output/test/browser/outputLinkProvider.test.ts +++ b/src/vs/workbench/contrib/output/test/browser/outputLinkProvider.test.ts @@ -8,6 +8,7 @@ import { URI } from 'vs/base/common/uri'; import { isMacintosh, isLinux, isWindows } from 'vs/base/common/platform'; import { OutputLinkComputer } from 'vs/workbench/contrib/output/common/outputLinkComputer'; import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('OutputLinkProvider', () => { @@ -297,4 +298,6 @@ suite('OutputLinkProvider', () => { assert.ok(res.range.startColumn > 0 && res.range.endColumn > 0); } }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingWidgets.ts b/src/vs/workbench/contrib/preferences/browser/keybindingWidgets.ts index 894b9e8f74d..70920e27b23 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingWidgets.ts @@ -301,7 +301,7 @@ export class DefineKeybindingOverlayWidget extends Disposable implements IOverla ) { super(); - this._widget = instantiationService.createInstance(DefineKeybindingWidget, null); + this._widget = this._register(instantiationService.createInstance(DefineKeybindingWidget, null)); this._editor.addOverlayWidget(this); } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index d751cf35bdd..8c9da329807 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -37,7 +37,7 @@ import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { IEditorMemento, IEditorOpenContext, IEditorPane } from 'vs/workbench/common/editor'; import { SuggestEnabledInput } from 'vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput'; import { SettingsTarget, SettingsTargetsWidget } from 'vs/workbench/contrib/preferences/browser/preferencesWidgets'; -import { getCommonlyUsedData, tocData } from 'vs/workbench/contrib/preferences/browser/settingsLayout'; +import { ITOCEntry, getCommonlyUsedData, tocData } from 'vs/workbench/contrib/preferences/browser/settingsLayout'; import { AbstractSettingRenderer, HeightChangeParams, ISettingLinkClickEvent, resolveConfiguredUntrustedSettings, createTocTreeForExtensionSettings, resolveSettingsTree, SettingsTree, SettingTreeRenderers } from 'vs/workbench/contrib/preferences/browser/settingsTree'; import { ISettingsEditorViewState, parseQuery, SearchResultIdx, SearchResultModel, SettingsTreeElement, SettingsTreeGroupChild, SettingsTreeGroupElement, SettingsTreeModel, SettingsTreeSettingElement } from 'vs/workbench/contrib/preferences/browser/settingsTreeModels'; import { createTOCIterator, TOCTree, TOCTreeModel } from 'vs/workbench/contrib/preferences/browser/tocTree'; @@ -1242,6 +1242,11 @@ export class SettingsEditor2 extends EditorPane { return undefined; } + private refreshModels(resolvedSettingsRoot: ITOCEntry) { + this.settingsTreeModel.update(resolvedSettingsRoot); + this.tocTreeModel.settingsTreeRoot = this.settingsTreeModel.root as SettingsTreeGroupElement; + } + private async onConfigUpdate(keys?: ReadonlySet, forceRefresh = false, schemaChange = false): Promise { if (keys && this.settingsTreeModel) { return this.updateElementsByKey(keys); @@ -1340,7 +1345,7 @@ export class SettingsEditor2 extends EditorPane { this.searchResultModel?.updateChildren(); if (this.settingsTreeModel) { - this.settingsTreeModel.update(resolvedSettingsRoot); + this.refreshModels(resolvedSettingsRoot); if (schemaChange && !!this.searchResultModel) { // If an extension's settings were just loaded and a search is active, retrigger the search so it shows up @@ -1351,8 +1356,7 @@ export class SettingsEditor2 extends EditorPane { this.renderTree(undefined, forceRefresh); } else { this.settingsTreeModel = this.instantiationService.createInstance(SettingsTreeModel, this.viewState, this.workspaceTrustManagementService.isWorkspaceTrusted()); - this.settingsTreeModel.update(resolvedSettingsRoot); - this.tocTreeModel.settingsTreeRoot = this.settingsTreeModel.root as SettingsTreeGroupElement; + this.refreshModels(resolvedSettingsRoot); // Don't restore the cached state if we already have a query value from calling _setOptions(). const cachedState = !this.viewState.query ? this.restoreCachedState() : undefined; diff --git a/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts b/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts index beff3204133..1dc6c5919bf 100644 --- a/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts +++ b/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts @@ -36,6 +36,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { ASK_QUICK_QUESTION_ACTION_ID } from 'vs/workbench/contrib/chat/browser/actions/chatQuickInputActions'; import { CommandInformationResult, IAiRelatedInformationService, RelatedInformationType } from 'vs/workbench/services/aiRelatedInformation/common/aiRelatedInformation'; +import { CHAT_OPEN_ACTION_ID } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; export class CommandsQuickAccessProvider extends AbstractEditorCommandsQuickAccessProvider { @@ -181,7 +182,7 @@ export class CommandsQuickAccessProvider extends AbstractEditorCommandsQuickAcce if (info) { additionalPicks.push({ label: localize('askXInChat', "Ask {0}: {1}", info.displayName, filter), - commandId: ASK_QUICK_QUESTION_ACTION_ID, + commandId: this.configuration.experimental.askChatLocation === 'quickChat' ? ASK_QUICK_QUESTION_ACTION_ID : CHAT_OPEN_ACTION_ID, args: [filter] }); } diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index 2289a2ece04..be89affe5e2 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -1465,7 +1465,7 @@ namespace ChangeLocalPortAction { function validateInput(tunnelService: ITunnelService, value: string, canElevate: boolean): { content: string; severity: Severity } | null { if (!value.match(/^[0-9]+$/)) { - return { content: invalidPortString, severity: Severity.Error }; + return { content: nls.localize('remote.tunnelsView.portShouldBeNumber', "Local port should be a number."), severity: Severity.Error }; } else if (Number(value) >= maxPortNumber) { return { content: invalidPortNumberString, severity: Severity.Error }; } else if (canElevate && tunnelService.isPortPrivileged(Number(value))) { diff --git a/src/vs/workbench/contrib/scm/common/quickDiffService.ts b/src/vs/workbench/contrib/scm/common/quickDiffService.ts index 137913c91b3..50e26ed2907 100644 --- a/src/vs/workbench/contrib/scm/common/quickDiffService.ts +++ b/src/vs/workbench/contrib/scm/common/quickDiffService.ts @@ -43,6 +43,10 @@ export class QuickDiffService extends Disposable implements IQuickDiffService { private readonly _onDidChangeQuickDiffProviders = this._register(new Emitter()); readonly onDidChangeQuickDiffProviders = this._onDidChangeQuickDiffProviders.event; + // It is common to get many requests for the same resource back to back (ex. when editing a file) + // Cache the last resource so to avoid unneeded extension host round trips. + private cachedOriginalResource: { uri: URI; resources: Map } | undefined; + constructor(@IUriIdentityService private readonly uriIdentityService: IUriIdentityService) { super(); } @@ -62,6 +66,19 @@ export class QuickDiffService extends Disposable implements IQuickDiffService { return !!diff.originalResource && (typeof diff.label === 'string') && (typeof diff.isSCM === 'boolean'); } + private getOriginalResourceFromCache(provider: string, uri: URI): URI | undefined { + if (this.cachedOriginalResource?.uri.toString() === uri.toString()) { + return this.cachedOriginalResource.resources.get(provider); + } + return undefined; + } + + private updateOriginalResourceCache(uri: URI, quickDiffs: QuickDiff[]) { + if (this.cachedOriginalResource?.uri.toString() !== uri.toString()) { + this.cachedOriginalResource = { uri, resources: new Map(quickDiffs.map(diff => ([diff.label, diff.originalResource]))) }; + } + } + async getQuickDiffs(uri: URI, language: string = '', isSynchronized: boolean = false): Promise { const providers = Array.from(this.quickDiffProviders) .filter(provider => !provider.rootUri || this.uriIdentityService.extUri.isEqualOrParent(uri, provider.rootUri)) @@ -70,12 +87,14 @@ export class QuickDiffService extends Disposable implements IQuickDiffService { const diffs = await Promise.all(providers.map(async provider => { const scoreValue = provider.selector ? score(provider.selector, uri, language, isSynchronized, undefined, undefined) : 10; const diff: Partial = { - originalResource: scoreValue > 0 ? await provider.getOriginalResource(uri) ?? undefined : undefined, + originalResource: scoreValue > 0 ? (this.getOriginalResourceFromCache(provider.label, uri) ?? await provider.getOriginalResource(uri) ?? undefined) : undefined, label: provider.label, isSCM: provider.isSCM }; return diff; })); - return diffs.filter(this.isQuickDiff); + const quickDiffs = diffs.filter(this.isQuickDiff); + this.updateOriginalResourceCache(uri, quickDiffs); + return quickDiffs; } } diff --git a/src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts b/src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts index 14b1636c0b8..3e23fe5fa73 100644 --- a/src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts @@ -5,6 +5,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { IMatch } from 'vs/base/common/filters'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { ResourceSet } from 'vs/base/common/map'; import { basenameOrAuthority, dirname } from 'vs/base/common/resources'; import { ThemeIcon } from 'vs/base/common/themables'; import { IRange, Range } from 'vs/editor/common/core/range'; @@ -128,7 +129,8 @@ export class TextSearchQuickAccess extends PickerQuickAccessProvider { await result.asyncResults; - return this.searchModel.searchResult.matches().filter(e => result.syncResults.indexOf(e) === -1); + const syncResultURIs = new ResourceSet(result.syncResults.map(e => e.resource)); + return this.searchModel.searchResult.matches().filter(e => !syncResultURIs.has(e.resource)); }; return { syncResults: this.searchModel.searchResult.matches(), diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index dd150169376..76d0f4824dd 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -2076,6 +2076,9 @@ export class SearchView extends ViewPane { } private _saveSearchHistoryService() { + if (this.searchWidget === undefined) { + return; + } const history: ISearchHistoryValues = Object.create(null); const searchHistory = this.searchWidget.getSearchHistory(); diff --git a/src/vs/workbench/contrib/search/test/browser/searchModel.test.ts b/src/vs/workbench/contrib/search/test/browser/searchModel.test.ts index ca0a09f0c31..715554ed96a 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchModel.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchModel.test.ts @@ -27,7 +27,7 @@ import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentitySe import { ILabelService } from 'vs/platform/label/common/label'; import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { TestEditorGroupsService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestEditorGroupsService, TestEditorService } from 'vs/workbench/test/browser/workbenchTestServices'; import { NotebookEditorWidgetService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorServiceImpl'; import { createFileUriFromPathFromRoot, getRootName } from 'vs/workbench/contrib/search/test/browser/searchTestCommon'; import { ICellMatch, IFileMatchWithCells, contentMatchesToTextSearchMatches, webviewMatchesToTextSearchMatches } from 'vs/workbench/contrib/search/browser/searchNotebookHelpers'; @@ -37,6 +37,9 @@ import { FindMatch, IReadonlyTextBuffer } from 'vs/editor/common/model'; import { ResourceMap, ResourceSet } from 'vs/base/common/map'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { INotebookSearchService } from 'vs/workbench/contrib/search/common/notebookSearch'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; const nullEvent = new class { id: number = -1; @@ -160,14 +163,10 @@ suite('SearchModel', () => { function canceleableSearchService(tokenSource: CancellationTokenSource): ISearchService { return { - textSearch(query: ISearchQuery, token?: CancellationToken, onProgress?: (result: ISearchProgressItem) => void): Promise { + textSearch(query: ITextQuery, token?: CancellationToken, onProgress?: (result: ISearchProgressItem) => void): Promise { token?.onCancellationRequested(() => tokenSource.cancel()); - return new Promise(resolve => { - queueMicrotask(() => { - resolve({}); - }); - }); + return this.textSearchSplitSyncAsync(query, token, onProgress).asyncResults; }, fileSearch(query: IFileQuery, token?: CancellationToken): Promise { token?.onCancellationRequested(() => tokenSource.cancel()); @@ -186,7 +185,10 @@ suite('SearchModel', () => { }, asyncResults: new Promise(resolve => { queueMicrotask(() => { - resolve({}); + resolve({ + results: [], + messages: [] + }); }); }) }; @@ -460,7 +462,7 @@ suite('SearchModel', () => { const target1 = sinon.stub().returns(nullEvent); instantiationService.stub(ITelemetryService, 'publicLog', target1); - instantiationService.stub(ISearchService, searchServiceWithError(new Error('error'))); + instantiationService.stub(ISearchService, searchServiceWithError(new Error('This error should be thrown by this test.'))); instantiationService.stub(INotebookSearchService, notebookSearchServiceWithInfo([], undefined)); const testObject = instantiationService.createInstance(SearchModel); @@ -581,6 +583,8 @@ suite('SearchModel', () => { function stubNotebookEditorService(instantiationService: TestInstantiationService): INotebookEditorService { instantiationService.stub(IEditorGroupsService, new TestEditorGroupsService()); + instantiationService.stub(IContextKeyService, new MockContextKeyService()); + instantiationService.stub(IEditorService, new TestEditorService()); return instantiationService.createInstance(NotebookEditorWidgetService); } }); diff --git a/src/vs/workbench/contrib/search/test/browser/searchResult.test.ts b/src/vs/workbench/contrib/search/test/browser/searchResult.test.ts index 50500494d9a..923d74b550d 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchResult.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchResult.test.ts @@ -26,12 +26,15 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { MockLabelService } from 'vs/workbench/services/label/test/common/mockLabelService'; import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { TestEditorGroupsService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestEditorGroupsService, TestEditorService } from 'vs/workbench/test/browser/workbenchTestServices'; import { NotebookEditorWidgetService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorServiceImpl'; import { ICellMatch, IFileMatchWithCells } from 'vs/workbench/contrib/search/browser/searchNotebookHelpers'; import { ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { addToSearchResult, createFileUriFromPathFromRoot, getRootName } from 'vs/workbench/contrib/search/test/browser/searchTestCommon'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; const lineOneRange = new OneLineRange(1, 0, 1); @@ -553,6 +556,8 @@ suite('SearchResult', () => { function stubNotebookEditorService(instantiationService: TestInstantiationService): INotebookEditorService { instantiationService.stub(IEditorGroupsService, new TestEditorGroupsService()); + instantiationService.stub(IContextKeyService, new MockContextKeyService()); + instantiationService.stub(IEditorService, new TestEditorService()); return instantiationService.createInstance(NotebookEditorWidgetService); } diff --git a/src/vs/workbench/contrib/search/test/browser/searchTestCommon.ts b/src/vs/workbench/contrib/search/test/browser/searchTestCommon.ts index 026df47ea9c..71e860b9f75 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchTestCommon.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchTestCommon.ts @@ -9,15 +9,18 @@ import { IModelService } from 'vs/editor/common/services/model'; import { ModelService } from 'vs/editor/common/services/modelService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService'; import { NotebookEditorWidgetService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorServiceImpl'; import { SearchResult } from 'vs/workbench/contrib/search/browser/searchModel'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IFileMatch } from 'vs/workbench/services/search/common/search'; -import { TestEditorGroupsService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestEditorGroupsService, TestEditorService } from 'vs/workbench/test/browser/workbenchTestServices'; export function createFileUriFromPathFromRoot(path?: string): URI { const rootName = getRootName(); @@ -50,6 +53,8 @@ export function stubModelService(instantiationService: TestInstantiationService) export function stubNotebookEditorService(instantiationService: TestInstantiationService): INotebookEditorService { instantiationService.stub(IEditorGroupsService, new TestEditorGroupsService()); + instantiationService.stub(IContextKeyService, new MockContextKeyService()); + instantiationService.stub(IEditorService, new TestEditorService()); return instantiationService.createInstance(NotebookEditorWidgetService); } diff --git a/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts b/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts index 279ee49e361..f73f00799f6 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts @@ -26,9 +26,12 @@ import { IFileMatch, ITextSearchMatch, OneLineRange, QueryType, SearchSortOrder import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { TestEditorGroupsService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestEditorGroupsService, TestEditorService } from 'vs/workbench/test/browser/workbenchTestServices'; import { NotebookEditorWidgetService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorServiceImpl'; import { createFileUriFromPathFromRoot, getRootName } from 'vs/workbench/contrib/search/test/browser/searchTestCommon'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; suite('Search - Viewlet', () => { let instantiation: TestInstantiationService; @@ -215,6 +218,8 @@ suite('Search - Viewlet', () => { function stubNotebookEditorService(instantiationService: TestInstantiationService): INotebookEditorService { instantiationService.stub(IEditorGroupsService, new TestEditorGroupsService()); + instantiationService.stub(IContextKeyService, new MockContextKeyService()); + instantiationService.stub(IEditorService, new TestEditorService()); return instantiationService.createInstance(NotebookEditorWidgetService); } }); diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorModel.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorModel.ts index 4a6b5d4d598..39b51590658 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorModel.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorModel.ts @@ -16,6 +16,7 @@ import { createTextBufferFactoryFromStream } from 'vs/editor/common/model/textMo import { SearchEditorWorkingCopyTypeId } from 'vs/workbench/contrib/searchEditor/browser/constants'; import { Emitter } from 'vs/base/common/event'; import { ResourceMap } from 'vs/base/common/map'; +import { SEARCH_RESULT_LANGUAGE_ID } from 'vs/workbench/services/search/common/search'; export type SearchEditorData = { resultsModel: ITextModel; configurationModel: SearchConfigurationModel }; @@ -65,7 +66,7 @@ class SearchEditorModelFactory { } return Promise.resolve({ - resultsModel: modelService.getModel(resource) ?? modelService.createModel('', languageService.createById('search-result'), resource), + resultsModel: modelService.getModel(resource) ?? modelService.createModel('', languageService.createById(SEARCH_RESULT_LANGUAGE_ID), resource), configurationModel: new SearchConfigurationModel(config) }); })(); @@ -98,7 +99,7 @@ class SearchEditorModelFactory { } return Promise.resolve({ - resultsModel: modelService.createModel(contents ?? '', languageService.createById('search-result'), resource), + resultsModel: modelService.createModel(contents ?? '', languageService.createById(SEARCH_RESULT_LANGUAGE_ID), resource), configurationModel: new SearchConfigurationModel(config) }); })(); @@ -132,7 +133,7 @@ class SearchEditorModelFactory { const { text, config } = await instantiationService.invokeFunction(parseSavedSearchEditor, existingFile); return ({ - resultsModel: modelService.createModel(text ?? '', languageService.createById('search-result'), resource), + resultsModel: modelService.createModel(text ?? '', languageService.createById(SEARCH_RESULT_LANGUAGE_ID), resource), configurationModel: new SearchConfigurationModel(config) }); })(); @@ -149,7 +150,7 @@ class SearchEditorModelFactory { if (!model && backup) { const factory = await createTextBufferFactoryFromStream(backup.value); - model = modelService.createModel(factory, languageService.createById('search-result'), resource); + model = modelService.createModel(factory, languageService.createById(SEARCH_RESULT_LANGUAGE_ID), resource); } if (model) { @@ -157,7 +158,7 @@ class SearchEditorModelFactory { const { text, config } = parseSerializedSearchEditor(existingFile); modelService.destroyModel(resource); return ({ - resultsModel: modelService.createModel(text ?? '', languageService.createById('search-result'), resource), + resultsModel: modelService.createModel(text ?? '', languageService.createById(SEARCH_RESULT_LANGUAGE_ID), resource), configurationModel: new SearchConfigurationModel(config) }); } diff --git a/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts b/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts index 406ff2a20b1..e74a499f9c0 100644 --- a/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts +++ b/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts @@ -16,6 +16,7 @@ import { EditOperation } from 'vs/editor/common/core/editOperation'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { generateUuid } from 'vs/base/common/uuid'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; class SimpleSnippetService implements ISnippetsService { declare readonly _serviceBrand: undefined; @@ -83,9 +84,11 @@ suite('SnippetsService', function () { disposables.dispose(); }); + ensureNoDisposablesAreLeakedInTestSuite(); + test('snippet completions - simple', function () { - const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); + const provider = new SnippetCompletionProvider(languageService, snippetService, disposables.add(new TestLanguageConfigurationService())); const model = disposables.add(instantiateTextModel(instantiationService, '', 'fooLang')); return provider.provideCompletionItems(model, new Position(1, 1), context)!.then(result => { @@ -96,7 +99,7 @@ suite('SnippetsService', function () { test('snippet completions - simple 2', async function () { - const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); + const provider = new SnippetCompletionProvider(languageService, snippetService, disposables.add(new TestLanguageConfigurationService())); const model = disposables.add(instantiateTextModel(instantiationService, 'hello ', 'fooLang')); await provider.provideCompletionItems(model, new Position(1, 6) /* hello| */, context)!.then(result => { @@ -112,7 +115,7 @@ suite('SnippetsService', function () { test('snippet completions - with prefix', function () { - const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); + const provider = new SnippetCompletionProvider(languageService, snippetService, disposables.add(new TestLanguageConfigurationService())); const model = disposables.add(instantiateTextModel(instantiationService, 'bar', 'fooLang')); return provider.provideCompletionItems(model, new Position(1, 4), context)!.then(result => { @@ -150,7 +153,7 @@ suite('SnippetsService', function () { generateUuid() )]); - const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); + const provider = new SnippetCompletionProvider(languageService, snippetService, disposables.add(new TestLanguageConfigurationService())); const model = disposables.add(instantiateTextModel(instantiationService, 'bar-bar', 'fooLang')); await provider.provideCompletionItems(model, new Position(1, 3), context)!.then(result => { @@ -222,7 +225,7 @@ suite('SnippetsService', function () { generateUuid() )]); - const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); + const provider = new SnippetCompletionProvider(languageService, snippetService, disposables.add(new TestLanguageConfigurationService())); let model = instantiateTextModel(instantiationService, '\t { @@ -259,7 +262,7 @@ suite('SnippetsService', function () { generateUuid() )]); - const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); + const provider = new SnippetCompletionProvider(languageService, snippetService, disposables.add(new TestLanguageConfigurationService())); const model = disposables.add(instantiateTextModel(instantiationService, '\n\t\n>/head>', 'fooLang')); return provider.provideCompletionItems(model, new Position(1, 1), context)!.then(result => { @@ -293,7 +296,7 @@ suite('SnippetsService', function () { generateUuid() )]); - const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); + const provider = new SnippetCompletionProvider(languageService, snippetService, disposables.add(new TestLanguageConfigurationService())); const model = disposables.add(instantiateTextModel(instantiationService, '', 'fooLang')); return provider.provideCompletionItems(model, new Position(1, 1), context)!.then(result => { @@ -322,7 +325,7 @@ suite('SnippetsService', function () { SnippetSource.User, generateUuid() )]); - const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); + const provider = new SnippetCompletionProvider(languageService, snippetService, disposables.add(new TestLanguageConfigurationService())); const model = disposables.add(instantiateTextModel(instantiationService, 'p-', 'fooLang')); @@ -349,7 +352,7 @@ suite('SnippetsService', function () { generateUuid() )]); - const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); + const provider = new SnippetCompletionProvider(languageService, snippetService, disposables.add(new TestLanguageConfigurationService())); const model = disposables.add(instantiateTextModel(instantiationService, 'Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea b', 'fooLang')); const result = await provider.provideCompletionItems(model, new Position(1, 158), context)!; @@ -370,7 +373,7 @@ suite('SnippetsService', function () { generateUuid() )]); - const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); + const provider = new SnippetCompletionProvider(languageService, snippetService, disposables.add(new TestLanguageConfigurationService())); const model = disposables.add(instantiateTextModel(instantiationService, ':', 'fooLang')); const result = await provider.provideCompletionItems(model, new Position(1, 2), context)!; @@ -391,7 +394,7 @@ suite('SnippetsService', function () { generateUuid() )]); - const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); + const provider = new SnippetCompletionProvider(languageService, snippetService, disposables.add(new TestLanguageConfigurationService())); const model = disposables.add(instantiateTextModel(instantiationService, 'template', 'fooLang')); const result = await provider.provideCompletionItems(model, new Position(1, 9), context)!; @@ -416,7 +419,7 @@ suite('SnippetsService', function () { generateUuid() )]); - const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); + const provider = new SnippetCompletionProvider(languageService, snippetService, disposables.add(new TestLanguageConfigurationService())); const model = disposables.add(instantiateTextModel(instantiationService, 'Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea b text_after_b', 'fooLang')); const result = await provider.provideCompletionItems(model, new Position(1, 158), context)!; @@ -425,7 +428,7 @@ suite('SnippetsService', function () { }); test('issue #61296: VS code freezes when editing CSS file with emoji', async function () { - const languageConfigurationService = new TestLanguageConfigurationService(); + const languageConfigurationService = disposables.add(new TestLanguageConfigurationService()); disposables.add(languageConfigurationService.register('fooLang', { wordPattern: /(#?-?\d*\.\d\w*%?)|(::?[\w-]*(?=[^,{;]*[,{]))|(([@#.!])?[\w-?]+%?|[@#!.])/g })); @@ -463,7 +466,7 @@ suite('SnippetsService', function () { generateUuid() )]); - const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); + const provider = new SnippetCompletionProvider(languageService, snippetService, disposables.add(new TestLanguageConfigurationService())); const model = disposables.add(instantiateTextModel(instantiationService, 'a ', 'fooLang')); const result = await provider.provideCompletionItems(model, new Position(1, 3), context)!; @@ -494,7 +497,7 @@ suite('SnippetsService', function () { generateUuid() )]); - const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); + const provider = new SnippetCompletionProvider(languageService, snippetService, disposables.add(new TestLanguageConfigurationService())); let model = instantiateTextModel(instantiationService, ' <', 'fooLang'); let result = await provider.provideCompletionItems(model, new Position(1, 3), context)!; @@ -526,7 +529,7 @@ suite('SnippetsService', function () { generateUuid() )]); - const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); + const provider = new SnippetCompletionProvider(languageService, snippetService, disposables.add(new TestLanguageConfigurationService())); let model = instantiateTextModel(instantiationService, 'not wordFoo bar', 'fooLang'); let result = await provider.provideCompletionItems(model, new Position(1, 3), context)!; @@ -570,7 +573,7 @@ suite('SnippetsService', function () { generateUuid() )]); - const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); + const provider = new SnippetCompletionProvider(languageService, snippetService, disposables.add(new TestLanguageConfigurationService())); const model = instantiateTextModel(instantiationService, 'filler e KEEP ng filler', 'fooLang'); const result = await provider.provideCompletionItems(model, new Position(1, 9), context)!; @@ -583,7 +586,7 @@ suite('SnippetsService', function () { }); test('Snippet will replace auto-closing pair if specified in prefix', async function () { - const languageConfigurationService = new TestLanguageConfigurationService(); + const languageConfigurationService = disposables.add(new TestLanguageConfigurationService()); disposables.add(languageConfigurationService.register('fooLang', { brackets: [ ['{', '}'], @@ -631,7 +634,7 @@ suite('SnippetsService', function () { generateUuid() )]); - const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); + const provider = new SnippetCompletionProvider(languageService, snippetService, disposables.add(new TestLanguageConfigurationService())); const model = instantiateTextModel(instantiationService, ' ci', 'fooLang'); const result = await provider.provideCompletionItems(model, new Position(1, 4), context)!; @@ -652,7 +655,7 @@ suite('SnippetsService', function () { // new Snippet(['fooLang'], '\'ccc', '\'ccc', '', 'value', '', SnippetSource.User, generateUuid()) ]); - const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); + const provider = new SnippetCompletionProvider(languageService, snippetService, disposables.add(new TestLanguageConfigurationService())); const model = instantiateTextModel(instantiationService, '\'\'', 'fooLang'); const result = await provider.provideCompletionItems( @@ -674,7 +677,7 @@ suite('SnippetsService', function () { new Snippet(false, ['fooLang'], '\'ccc', '\'ccc', '', 'value', '', SnippetSource.User, generateUuid()) ]); - const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); + const provider = new SnippetCompletionProvider(languageService, snippetService, disposables.add(new TestLanguageConfigurationService())); const model = instantiateTextModel(instantiationService, '\'\'', 'fooLang'); @@ -695,7 +698,7 @@ suite('SnippetsService', function () { new Snippet(false, ['fooLang'], '^y', '^y', '', 'value', '', SnippetSource.User, generateUuid()), ]); - const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); + const provider = new SnippetCompletionProvider(languageService, snippetService, disposables.add(new TestLanguageConfigurationService())); const model = instantiateTextModel(instantiationService, '\'hellot\'', 'fooLang'); const result = await provider.provideCompletionItems( @@ -716,7 +719,7 @@ suite('SnippetsService', function () { new Snippet(false, ['fooLang'], '^y', '^y', '', 'value', '', SnippetSource.User, generateUuid()), ]); - const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); + const provider = new SnippetCompletionProvider(languageService, snippetService, disposables.add(new TestLanguageConfigurationService())); const model = instantiateTextModel(instantiationService, ')*&^', 'fooLang'); const result = await provider.provideCompletionItems( @@ -736,7 +739,7 @@ suite('SnippetsService', function () { new Snippet(false, ['fooLang'], 'foobarrrrrr', 'foobarrrrrr', '', 'value', '', SnippetSource.User, generateUuid()), ]); - const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); + const provider = new SnippetCompletionProvider(languageService, snippetService, disposables.add(new TestLanguageConfigurationService())); const model = instantiateTextModel(instantiationService, 'foobar', 'fooLang'); const result = await provider.provideCompletionItems( @@ -756,7 +759,7 @@ suite('SnippetsService', function () { ]); - const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); + const provider = new SnippetCompletionProvider(languageService, snippetService, disposables.add(new TestLanguageConfigurationService())); const model = instantiateTextModel(instantiationService, 'function abc(w)', 'fooLang'); const result = await provider.provideCompletionItems( model, @@ -775,7 +778,7 @@ suite('SnippetsService', function () { new Snippet(false, ['fooLang'], 'div#', 'div#', '', 'div#', '', SnippetSource.User, generateUuid()), ]); - const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); + const provider = new SnippetCompletionProvider(languageService, snippetService, disposables.add(new TestLanguageConfigurationService())); const model = instantiateTextModel(instantiationService, 'di', 'fooLang'); const result = await provider.provideCompletionItems( model, @@ -804,8 +807,8 @@ suite('SnippetsService', function () { snippetService = new SimpleSnippetService([ new Snippet(false, ['fooLang'], 'foo', 'Foo- Bar', '', 'Foo', '', SnippetSource.User, generateUuid()), ]); - const model = instantiateTextModel(instantiationService, ' bar', 'fooLang'); - const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); + const model = disposables.add(instantiateTextModel(instantiationService, ' bar', 'fooLang')); + const provider = new SnippetCompletionProvider(languageService, snippetService, disposables.add(new TestLanguageConfigurationService())); const result = await provider.provideCompletionItems( model, new Position(1, 8), diff --git a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh index 687b9ee025a..4d1479ca9f5 100755 --- a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh +++ b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh @@ -47,7 +47,7 @@ fi # Apply EnvironmentVariableCollections if needed if [ -n "${VSCODE_ENV_REPLACE:-}" ]; then - IFS=':' read -ra ADDR <<< "$VSCODE_ENV_REPLACE" + IFS=':' read -ra ADDR <<<"$VSCODE_ENV_REPLACE" for ITEM in "${ADDR[@]}"; do VARNAME="$(echo $ITEM | cut -d "=" -f 1)" VALUE="$(echo -e "$ITEM" | cut -d "=" -f 2)" @@ -56,7 +56,7 @@ if [ -n "${VSCODE_ENV_REPLACE:-}" ]; then builtin unset VSCODE_ENV_REPLACE fi if [ -n "${VSCODE_ENV_PREPEND:-}" ]; then - IFS=':' read -ra ADDR <<< "$VSCODE_ENV_PREPEND" + IFS=':' read -ra ADDR <<<"$VSCODE_ENV_PREPEND" for ITEM in "${ADDR[@]}"; do VARNAME="$(echo $ITEM | cut -d "=" -f 1)" VALUE="$(echo -e "$ITEM" | cut -d "=" -f 2)" @@ -65,7 +65,7 @@ if [ -n "${VSCODE_ENV_PREPEND:-}" ]; then builtin unset VSCODE_ENV_PREPEND fi if [ -n "${VSCODE_ENV_APPEND:-}" ]; then - IFS=':' read -ra ADDR <<< "$VSCODE_ENV_APPEND" + IFS=':' read -ra ADDR <<<"$VSCODE_ENV_APPEND" for ITEM in "${ADDR[@]}"; do VARNAME="$(echo $ITEM | cut -d "=" -f 1)" VALUE="$(echo -e "$ITEM" | cut -d "=" -f 2)" @@ -95,13 +95,52 @@ __vsc_get_trap() { builtin printf '%s' "${terms[2]:-}" } +__vsc_command_available() { + builtin local trash + trash=$(builtin command -v "$1" 2>&1) + builtin return $? +} + +# We provide two faster escaping functions here. +# The first one escapes each byte 0xab into '\xab', which is most scalable and has promising runtime +# efficiency, except that it relies on external commands od and tr. +# The second one is much faster and has zero dependency, except that it escapes only +# '\\' -> '\\\\' and ';' -> '\x3b' and scales up badly when more patterns are needed. +# We default to use the first function if od and tr are available, and fallback to the second otherwise. +if __vsc_command_available od && __vsc_command_available tr; then + __vsc_escape_value_fast() { + builtin local out + # -An removes line number + # -v do not use * to mark line suppression + # -tx1 prints each byte as two-digit hex + # tr -d '\n' concats all output lines + out=$(od -An -vtx1 <<<"$1" | tr -d '\n') + out=${out// /\\x} + # <<<"$1" prepends a trailing newline already, so we don't need to printf '%s\n' + builtin printf '%s' "${out}" + } +else + __vsc_escape_value_fast() { + builtin local LC_ALL=C out + out=${1//\\/\\\\} + out=${out//;/\\x3b} + builtin printf '%s\n' "${out}" + } +fi + # The property (P) and command (E) codes embed values which require escaping. # Backslashes are doubled. Non-alphanumeric characters are converted to escaped hex. __vsc_escape_value() { + # If the input being too large, switch to the faster function + if [ "${#1}" -ge 2000 ]; then + __vsc_escape_value_fast "$1" + builtin return + fi + # Process text byte by byte, not by codepoint. builtin local LC_ALL=C str="${1}" i byte token out='' - for (( i=0; i < "${#str}"; ++i )); do + for ((i = 0; i < "${#str}"; ++i)); do byte="${str:$i:1}" # Escape backslashes and semi-colons diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 8e58f90c8fc..3b01c31dff1 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -61,6 +61,7 @@ import { TerminalCapability } from 'vs/platform/terminal/common/capabilities/cap import { killTerminalIcon, newTerminalIcon } from 'vs/workbench/contrib/terminal/browser/terminalIcons'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { Iterable } from 'vs/base/common/iterator'; +import { AccessibleViewProviderId, accessibleViewCurrentProviderId, accessibleViewOnLastLine } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; export const switchTerminalActionViewItemSeparator = '\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'; export const switchTerminalShowTabsTitle = localize('showTerminalTabs', "Show Tabs"); @@ -479,7 +480,7 @@ export function registerTerminalActions() { id: TerminalCommandId.Focus, title: terminalStrings.focus, keybinding: { - when: ContextKeyExpr.and(CONTEXT_ACCESSIBILITY_MODE_ENABLED, TerminalContextKeys.accessibleBufferOnLastLine), + when: ContextKeyExpr.and(CONTEXT_ACCESSIBILITY_MODE_ENABLED, accessibleViewOnLastLine, accessibleViewCurrentProviderId.isEqualTo(AccessibleViewProviderId.Terminal)), primary: KeyMod.CtrlCmd | KeyCode.DownArrow, weight: KeybindingWeight.WorkbenchContrib }, diff --git a/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts b/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts index 39d3a0ceb03..fd27c75f4f7 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts @@ -50,11 +50,11 @@ export class TerminalConfigHelper extends Disposable implements IBrowserTerminal ) { super(); this._updateConfig(); - this._configurationService.onDidChangeConfiguration(e => { + this._register(this._configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(TERMINAL_CONFIG_SECTION)) { this._updateConfig(); } - }); + })); if (isLinux) { if (navigator.userAgent.includes('Ubuntu')) { this._linuxDistro = LinuxDistro.Ubuntu; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts b/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts index bf6dc33b430..5c4b8b19b1f 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts @@ -5,7 +5,7 @@ import { TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal'; import { Event, Emitter } from 'vs/base/common/event'; -import { IDisposable, Disposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; +import { IDisposable, Disposable, DisposableStore, dispose, toDisposable } from 'vs/base/common/lifecycle'; import { SplitView, Orientation, IView, Sizing } from 'vs/base/browser/ui/splitview/splitview'; import { IWorkbenchLayoutService, Parts, Position } from 'vs/workbench/services/layout/browser/layoutService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -267,6 +267,7 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { get terminalInstances(): ITerminalInstance[] { return this._terminalInstances; } private _initialRelativeSizes: number[] | undefined; + private _visible: boolean = false; private readonly _onDidDisposeInstance: Emitter = this._register(new Emitter()); readonly onDidDisposeInstance = this._onDidDisposeInstance.event; @@ -300,6 +301,12 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { this.attachToElement(this._container); } this._onPanelOrientationChanged.fire(this._terminalLocation === ViewContainerLocation.Panel && this._panelPosition === Position.BOTTOM ? Orientation.HORIZONTAL : Orientation.VERTICAL); + this._register(toDisposable(() => { + if (this._container && this._groupElement) { + this._container.removeChild(this._groupElement); + this._groupElement = undefined; + } + })); } addInstance(shellLaunchConfigOrInstance: IShellLaunchConfig | ITerminalInstance, parentTerminalId?: number): void { @@ -328,13 +335,9 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { } override dispose(): void { - super.dispose(); - if (this._container && this._groupElement) { - this._container.removeChild(this._groupElement); - this._groupElement = undefined; - } this._terminalInstances = []; this._onInstancesChanged.fire(); + super.dispose(); } get activeInstance(): ITerminalInstance | undefined { @@ -481,10 +484,6 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { const orientation = this._terminalLocation === ViewContainerLocation.Panel && this._panelPosition === Position.BOTTOM ? Orientation.HORIZONTAL : Orientation.VERTICAL; this._splitPaneContainer = this._instantiationService.createInstance(SplitPaneContainer, this._groupElement, orientation); this.terminalInstances.forEach(instance => this._splitPaneContainer!.split(instance, this._activeInstanceIndex + 1)); - if (this._initialRelativeSizes) { - this.resizePanes(this._initialRelativeSizes); - this._initialRelativeSizes = undefined; - } } } @@ -518,6 +517,7 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { } setVisible(visible: boolean): void { + this._visible = visible; if (this._groupElement) { this._groupElement.style.display = visible ? '' : 'none'; } @@ -549,6 +549,10 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { this._onPanelOrientationChanged.fire(this._splitPaneContainer.orientation); } this._splitPaneContainer.layout(width, height); + if (this._initialRelativeSizes && this._visible) { + this.resizePanes(this._initialRelativeSizes); + this._initialRelativeSizes = undefined; + } } } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 3a8d8a3d34c..e37b1ba4d2b 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -235,7 +235,7 @@ export class TerminalService extends Disposable implements ITerminalService { this.initializePrimaryBackend(); // Create async as the class depends on `this` - timeout(0).then(() => this._instantiationService.createInstance(TerminalEditorStyle, document.head)); + timeout(0).then(() => this._register(this._instantiationService.createInstance(TerminalEditorStyle, document.head))); } async showProfileQuickPick(type: 'setDefault' | 'createInstance', cwd?: string | URI): Promise { @@ -421,7 +421,7 @@ export class TerminalService extends Disposable implements ITerminalService { } } return new Promise(r => { - instance.onExit(() => r()); + Event.once(instance.onExit)(() => r()); instance.dispose(TerminalExitReason.User); }); } @@ -503,30 +503,23 @@ export class TerminalService extends Disposable implements ITerminalService { private async _recreateTerminalGroup(tabLayout: IRawTerminalTabLayoutInfo, terminalLayouts: IRawTerminalInstanceLayoutInfo[]): Promise { let lastInstance: Promise | undefined; - let group: Promise | undefined; for (const terminalLayout of terminalLayouts) { const attachPersistentProcess = terminalLayout.terminal!; if (this._lifecycleService.startupKind !== StartupKind.ReloadedWindow && attachPersistentProcess.type === 'Task') { continue; } mark(`code/terminal/willRecreateTerminal/${attachPersistentProcess.id}-${attachPersistentProcess.pid}`); - if (!lastInstance) { - // create group and terminal - lastInstance = this.createTerminal({ - config: { attachPersistentProcess }, - location: TerminalLocation.Panel - }); - group = lastInstance.then(instance => this._terminalGroupService.getGroupForInstance(instance)); - } else { - // add split terminals to this group - lastInstance = this.createTerminal({ - config: { attachPersistentProcess }, - location: { parentTerminal: lastInstance } - }); - } - mark(`code/terminal/didRecreateTerminal/${attachPersistentProcess.id}-${attachPersistentProcess.pid}`); + lastInstance = this.createTerminal({ + config: { attachPersistentProcess }, + location: lastInstance ? { parentTerminal: lastInstance } : TerminalLocation.Panel + }); + lastInstance.then(() => mark(`code/terminal/didRecreateTerminal/${attachPersistentProcess.id}-${attachPersistentProcess.pid}`)); } - group?.then(g => g?.resizePanes(tabLayout.terminals.map(terminal => terminal.relativeSize))); + const group = lastInstance?.then(instance => { + const g = this._terminalGroupService.getGroupForInstance(instance); + g?.resizePanes(tabLayout.terminals.map(terminal => terminal.relativeSize)); + return g; + }); return group; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalView.ts b/src/vs/workbench/contrib/terminal/browser/terminalView.ts index db205d31f34..b56979d0da9 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalView.ts @@ -244,9 +244,7 @@ export class TerminalViewPane extends ViewPane { super(action.id, action.label, action.class, action.enabled); this.checked = action.checked; this.tooltip = action.tooltip; - } - override dispose(): void { - action.dispose(); + this._register(action); } override async run() { const instance = that._terminalGroupService.activeInstance; diff --git a/src/vs/workbench/contrib/terminal/browser/widgets/terminalHoverWidget.ts b/src/vs/workbench/contrib/terminal/browser/widgets/terminalHoverWidget.ts index c2de2c4d1d6..9e6a23950b0 100644 --- a/src/vs/workbench/contrib/terminal/browser/widgets/terminalHoverWidget.ts +++ b/src/vs/workbench/contrib/terminal/browser/widgets/terminalHoverWidget.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { Widget } from 'vs/base/browser/ui/widget'; import { ITerminalWidget } from 'vs/workbench/contrib/terminal/browser/widgets/widgets'; @@ -37,10 +37,6 @@ export class TerminalHover extends Disposable implements ITerminalWidget { super(); } - override dispose() { - super.dispose(); - } - attach(container: HTMLElement): void { const showLinkHover = this._configurationService.getValue(TerminalSettingId.ShowLinkHover); if (!showLinkHover) { @@ -62,7 +58,7 @@ export class TerminalHover extends Disposable implements ITerminalWidget { } class CellHoverTarget extends Widget implements IHoverTarget { - private _domNode: HTMLElement | undefined; + private _domNode: HTMLElement; private readonly _targetElements: HTMLElement[] = []; get targetElements(): readonly HTMLElement[] { return this._targetElements; } @@ -122,10 +118,6 @@ class CellHoverTarget extends Widget implements IHoverTarget { } container.appendChild(this._domNode); - } - - override dispose(): void { - this._domNode?.parentElement?.removeChild(this._domNode); - super.dispose(); + this._register(toDisposable(() => this._domNode?.remove())); } } diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts index 12aaa0b0bb6..ac1a2096320 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts @@ -14,7 +14,7 @@ import * as dom from 'vs/base/browser/dom'; import { IXtermCore } from 'vs/workbench/contrib/terminal/browser/xterm-private'; import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IShellIntegration, ITerminalLogService, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; import { ITerminalFont, ITerminalConfiguration } from 'vs/workbench/contrib/terminal/common/terminal'; @@ -115,7 +115,7 @@ function getFullBufferLineAsString(lineIndex: number, buffer: IBuffer): { lineDa * Wraps the xterm object with additional functionality. Interaction with the backing process is out * of the scope of this class. */ -export class XtermTerminal extends DisposableStore implements IXtermTerminal, IDetachedXtermTerminal, IInternalXtermTerminal { +export class XtermTerminal extends Disposable implements IXtermTerminal, IDetachedXtermTerminal, IInternalXtermTerminal { /** The raw xterm.js instance */ readonly raw: RawXtermTerminal; private _core: IXtermCore; @@ -138,7 +138,7 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, ID private _serializeAddon?: SerializeAddonType; private _imageAddon?: ImageAddonType; - private readonly _attachedDisposables = this.add(new DisposableStore()); + private readonly _attachedDisposables = this._register(new DisposableStore()); private readonly _anyTerminalFocusContextKey: IContextKey; private readonly _anyFocusedTerminalHasSelection: IContextKey; @@ -147,21 +147,21 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, ID get isStdinDisabled(): boolean { return !!this.raw.options.disableStdin; } - private readonly _onDidRequestRunCommand = this.add(new Emitter<{ command: ITerminalCommand; copyAsHtml?: boolean; noNewLine?: boolean }>()); + private readonly _onDidRequestRunCommand = this._register(new Emitter<{ command: ITerminalCommand; copyAsHtml?: boolean; noNewLine?: boolean }>()); readonly onDidRequestRunCommand = this._onDidRequestRunCommand.event; - private readonly _onDidRequestFocus = this.add(new Emitter()); + private readonly _onDidRequestFocus = this._register(new Emitter()); readonly onDidRequestFocus = this._onDidRequestFocus.event; - private readonly _onDidRequestSendText = this.add(new Emitter()); + private readonly _onDidRequestSendText = this._register(new Emitter()); readonly onDidRequestSendText = this._onDidRequestSendText.event; - private readonly _onDidRequestFreePort = this.add(new Emitter()); + private readonly _onDidRequestFreePort = this._register(new Emitter()); readonly onDidRequestFreePort = this._onDidRequestFreePort.event; - private readonly _onDidChangeFindResults = this.add(new Emitter<{ resultIndex: number; resultCount: number }>()); + private readonly _onDidChangeFindResults = this._register(new Emitter<{ resultIndex: number; resultCount: number }>()); readonly onDidChangeFindResults = this._onDidChangeFindResults.event; - private readonly _onDidChangeSelection = this.add(new Emitter()); + private readonly _onDidChangeSelection = this._register(new Emitter()); readonly onDidChangeSelection = this._onDidChangeSelection.event; - private readonly _onDidChangeFocus = this.add(new Emitter()); + private readonly _onDidChangeFocus = this._register(new Emitter()); readonly onDidChangeFocus = this._onDidChangeFocus.event; - private readonly _onDidDispose = this.add(new Emitter()); + private readonly _onDidDispose = this._register(new Emitter()); readonly onDidDispose = this._onDidDispose.event; get markTracker(): IMarkTracker { return this._markNavigationAddon; } @@ -209,7 +209,7 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, ID const config = this._configHelper.config; const editorOptions = this._configurationService.getValue('editor'); - this.raw = this.add(new xtermCtor({ + this.raw = this._register(new xtermCtor({ allowProposedApi: true, cols, rows, @@ -244,7 +244,7 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, ID this._updateSmoothScrolling(); this._core = (this.raw as any)._core as IXtermCore; - this.add(this._configurationService.onDidChangeConfiguration(async e => { + this._register(this._configurationService.onDidChangeConfiguration(async e => { if (e.affectsConfiguration(TerminalSettingId.GpuAcceleration)) { XtermTerminal._suggestedRendererType = undefined; } @@ -256,11 +256,11 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, ID } })); - this.add(this._themeService.onDidColorThemeChange(theme => this._updateTheme(theme))); - this.add(this._logService.onDidChangeLogLevel(e => this.raw.options.logLevel = vscodeToXtermLogLevel(e))); + this._register(this._themeService.onDidColorThemeChange(theme => this._updateTheme(theme))); + this._register(this._logService.onDidChangeLogLevel(e => this.raw.options.logLevel = vscodeToXtermLogLevel(e))); // Refire events - this.add(this.raw.onSelectionChange(() => { + this._register(this.raw.onSelectionChange(() => { this._onDidChangeSelection.fire(); if (this.isFocused) { this._anyFocusedTerminalHasSelection.set(this.raw.hasSelection()); @@ -272,7 +272,7 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, ID this._markNavigationAddon = this._instantiationService.createInstance(MarkNavigationAddon, _capabilities); this.raw.loadAddon(this._markNavigationAddon); this._decorationAddon = this._instantiationService.createInstance(DecorationAddon, this._capabilities); - this._decorationAddon.onDidRequestRunCommand(e => this._onDidRequestRunCommand.fire(e)); + this._register(this._decorationAddon.onDidRequestRunCommand(e => this._onDidRequestRunCommand.fire(e))); this.raw.loadAddon(this._decorationAddon); this._shellIntegrationAddon = new ShellIntegrationAddon(shellIntegrationNonce, disableShellIntegrationReporting, this._telemetryService, this._logService); this.raw.loadAddon(this._shellIntegrationAddon); @@ -283,12 +283,12 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, ID // Load the suggest addon, this should be loaded regardless of the setting as the sequences // may still come in if (this._terminalSuggestWidgetVisibleContextKey) { - this._suggestAddon = this._instantiationService.createInstance(SuggestAddon, this._terminalSuggestWidgetVisibleContextKey); + this._suggestAddon = this._register(this._instantiationService.createInstance(SuggestAddon, this._terminalSuggestWidgetVisibleContextKey)); this.raw.loadAddon(this._suggestAddon); - this._suggestAddon.onAcceptedCompletion(async text => { + this._register(this._suggestAddon.onAcceptedCompletion(async text => { this._onDidRequestFocus.fire(); this._onDidRequestSendText.fire(text); - }); + })); } } @@ -953,7 +953,7 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, ID this.raw.write(data); } - public override dispose(): void { + override dispose(): void { this._anyTerminalFocusContextKey.reset(); this._anyFocusedTerminalHasSelection.reset(); this._onDidDispose.fire(); diff --git a/src/vs/workbench/contrib/terminal/common/environmentVariableService.ts b/src/vs/workbench/contrib/terminal/common/environmentVariableService.ts index 0aaa48f6b05..ddb64c1f0b8 100644 --- a/src/vs/workbench/contrib/terminal/common/environmentVariableService.ts +++ b/src/vs/workbench/contrib/terminal/common/environmentVariableService.ts @@ -56,7 +56,7 @@ export class EnvironmentVariableService extends Disposable implements IEnvironme this.mergedCollection = this._resolveMergedCollection(); // Listen for uninstalled/disabled extensions - this._extensionService.onDidChangeExtensions(() => this._invalidateExtensionCollections()); + this._register(this._extensionService.onDidChangeExtensions(() => this._invalidateExtensionCollections())); } set(extensionIdentifier: string, collection: IEnvironmentVariableCollectionWithPersistence): void { diff --git a/src/vs/workbench/contrib/terminal/common/history.ts b/src/vs/workbench/contrib/terminal/common/history.ts index 1f82b4d0640..cf0d940e716 100644 --- a/src/vs/workbench/contrib/terminal/common/history.ts +++ b/src/vs/workbench/contrib/terminal/common/history.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { env } from 'vs/base/common/process'; -import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { LRUCache } from 'vs/base/common/map'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { FileOperationError, FileOperationResult, IFileContent, IFileService } from 'vs/platform/files/common/files'; @@ -135,7 +135,7 @@ export class TerminalPersistedHistory extends Disposable implements ITerminal })); // Listen to cache changes from other windows - this._register(this._storageService.onDidChangeValue(StorageScope.APPLICATION, this._getTimestampStorageKey(), this._register(new DisposableStore()))(() => { + this._register(this._storageService.onDidChangeValue(StorageScope.APPLICATION, this._getTimestampStorageKey(), this._store)(() => { if (!this._isStale) { this._isStale = this._storageService.getNumber(this._getTimestampStorageKey(), StorageScope.APPLICATION, 0) !== this._timestamp; } diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index 2c67eda18cb..efe0d3be971 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -399,7 +399,6 @@ export const enum TerminalCommandId { OpenWebLink = 'workbench.action.terminal.openUrlLink', RunRecentCommand = 'workbench.action.terminal.runRecentCommand', FocusAccessibleBuffer = 'workbench.action.terminal.focusAccessibleBuffer', - NavigateAccessibleBuffer = 'workbench.action.terminal.navigateAccessibleBuffer', AccessibleBufferGoToNextCommand = 'workbench.action.terminal.accessibleBufferGoToNextCommand', AccessibleBufferGoToPreviousCommand = 'workbench.action.terminal.accessibleBufferGoToPreviousCommand', CopyLastCommandOutput = 'workbench.action.terminal.copyLastCommandOutput', diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index ea7c0335df5..cdb90775373 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -299,7 +299,7 @@ const terminalConfiguration: IConfigurationNode = { [TerminalSettingId.TerminalTitleSeparator]: { 'type': 'string', 'default': ' - ', - 'markdownDescription': localize("terminal.integrated.tabs.separator", "Separator used by {0} and {0}.", `\`${TerminalSettingId.TerminalTitle}\``, `\`${TerminalSettingId.TerminalDescription}\``) + 'markdownDescription': localize("terminal.integrated.tabs.separator", "Separator used by {0} and {1}.", `\`#${TerminalSettingId.TerminalTitle}#\``, `\`#${TerminalSettingId.TerminalDescription}#\``) }, [TerminalSettingId.TerminalTitle]: { 'type': 'string', @@ -541,7 +541,7 @@ const terminalConfiguration: IConfigurationNode = { default: 'never' }, [TerminalSettingId.CustomGlyphs]: { - description: localize('terminal.integrated.customGlyphs', "Whether to draw custom glyphs for block element and box drawing characters instead of using the font, which typically yields better rendering with continuous lines. Note that this doesn't work when {0} is disabled.", `\`#${TerminalSettingId.GpuAcceleration}#\``), + markdownDescription: localize('terminal.integrated.customGlyphs', "Whether to draw custom glyphs for block element and box drawing characters instead of using the font, which typically yields better rendering with continuous lines. Note that this doesn't work when {0} is disabled.", `\`#${TerminalSettingId.GpuAcceleration}#\``), type: 'boolean', default: true }, diff --git a/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts index ce4f7a4168c..1073cf8767d 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts @@ -49,12 +49,6 @@ export namespace TerminalContextKeys { /** Whether any terminal is focused, including detached terminals used in other UI. */ export const focusInAny = new RawContextKey(TerminalContextKeyStrings.FocusInAny, false, localize('terminalFocusInAnyContextKey', "Whether any terminal is focused, including detached terminals used in other UI.")); - /** Whether the accessible buffer is focused. */ - export const accessibleBufferFocus = new RawContextKey(TerminalContextKeyStrings.AccessibleBufferFocus, false, localize('terminalAccessibleBufferFocusContextKey', "Whether the terminal accessible buffer is focused.")); - - /** Whether the accessible buffer focus is on the last line. */ - export const accessibleBufferOnLastLine = new RawContextKey(TerminalContextKeyStrings.AccessibleBufferOnLastLine, false, localize('terminalAccessibleBufferOnLastLineContextKey', "Whether the accessible buffer focus is on the last line.")); - /** Whether a terminal in the editor area is focused. */ export const editorFocus = new RawContextKey(TerminalContextKeyStrings.EditorFocus, false, localize('terminalEditorFocusContextKey', "Whether a terminal in the editor area is focused.")); diff --git a/src/vs/workbench/contrib/terminal/test/browser/capabilities/commandDetectionCapability.test.ts b/src/vs/workbench/contrib/terminal/test/browser/capabilities/commandDetectionCapability.test.ts index 8ce2b64a810..e5f357d7780 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/capabilities/commandDetectionCapability.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/capabilities/commandDetectionCapability.test.ts @@ -13,6 +13,8 @@ import { TestInstantiationService } from 'vs/platform/instantiation/test/common/ import { IContextMenuDelegate } from 'vs/base/browser/contextmenu'; import { importAMDNodeModule } from 'vs/amdX'; import { writeP } from 'vs/workbench/contrib/terminal/browser/terminalTestHelpers'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { DisposableStore } from 'vs/base/common/lifecycle'; type TestTerminalCommandMatch = Pick & { marker: { line: number } }; @@ -23,6 +25,8 @@ class TestCommandDetectionCapability extends CommandDetectionCapability { } suite('CommandDetectionCapability', () => { + let disposables: DisposableStore; + let xterm: Terminal; let capability: TestCommandDetectionCapability; let addEvents: ITerminalCommand[]; @@ -57,20 +61,21 @@ suite('CommandDetectionCapability', () => { } setup(async () => { + disposables = new DisposableStore(); const TerminalCtor = (await importAMDNodeModule('xterm', 'lib/xterm.js')).Terminal; xterm = new TerminalCtor({ allowProposedApi: true, cols: 80 }); - instantiationService = new TestInstantiationService(); + instantiationService = disposables.add(new TestInstantiationService()); instantiationService.stub(IContextMenuService, { showContextMenu(delegate: IContextMenuDelegate): void { } } as Partial); - capability = new TestCommandDetectionCapability(xterm, new NullLogService()); + capability = disposables.add(new TestCommandDetectionCapability(xterm, new NullLogService())); addEvents = []; capability.onCommandFinished(e => addEvents.push(e)); assertCommands([]); }); - teardown(() => { - instantiationService.dispose(); - }); + teardown(() => disposables.dispose()); + + ensureNoDisposablesAreLeakedInTestSuite(); test('should not add commands when no capability methods are triggered', async () => { await writeP(xterm, 'foo\r\nbar\r\n'); diff --git a/src/vs/workbench/contrib/terminal/test/browser/capabilities/partialCommandDetectionCapability.test.ts b/src/vs/workbench/contrib/terminal/test/browser/capabilities/partialCommandDetectionCapability.test.ts index 947b6745852..26bb55f5654 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/capabilities/partialCommandDetectionCapability.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/capabilities/partialCommandDetectionCapability.test.ts @@ -9,6 +9,7 @@ import type { IMarker, Terminal } from 'xterm'; import { IXtermCore } from 'vs/workbench/contrib/terminal/browser/xterm-private'; import { importAMDNodeModule } from 'vs/amdX'; import { writeP } from 'vs/workbench/contrib/terminal/browser/terminalTestHelpers'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; interface TestTerminal extends Terminal { _core: IXtermCore; @@ -33,6 +34,8 @@ suite('PartialCommandDetectionCapability', () => { capability.onCommandFinished(e => addEvents.push(e)); }); + ensureNoDisposablesAreLeakedInTestSuite(); + test('should not add commands when the cursor position is too close to the left side', async () => { assertCommands([]); xterm._core._onData.fire('\x0d'); diff --git a/src/vs/workbench/contrib/terminal/test/browser/capabilities/terminalCapabilityStore.test.ts b/src/vs/workbench/contrib/terminal/test/browser/capabilities/terminalCapabilityStore.test.ts index 54c1489cb99..07ec5631f32 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/capabilities/terminalCapabilityStore.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/capabilities/terminalCapabilityStore.test.ts @@ -4,6 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { deepStrictEqual } from 'assert'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; import { TerminalCapabilityStore, TerminalCapabilityStoreMultiplexer } from 'vs/platform/terminal/common/capabilities/terminalCapabilityStore'; @@ -53,6 +55,7 @@ suite('TerminalCapabilityStore', () => { }); suite('TerminalCapabilityStoreMultiplexer', () => { + let store: DisposableStore; let multiplexer: TerminalCapabilityStoreMultiplexer; let store1: TerminalCapabilityStore; let store2: TerminalCapabilityStore; @@ -60,16 +63,19 @@ suite('TerminalCapabilityStoreMultiplexer', () => { let removeEvents: TerminalCapability[]; setup(() => { - multiplexer = new TerminalCapabilityStoreMultiplexer(); + store = new DisposableStore(); + multiplexer = store.add(new TerminalCapabilityStoreMultiplexer()); multiplexer.onDidAddCapabilityType(e => addEvents.push(e)); multiplexer.onDidRemoveCapabilityType(e => removeEvents.push(e)); - store1 = new TerminalCapabilityStore(); - store2 = new TerminalCapabilityStore(); + store1 = store.add(new TerminalCapabilityStore()); + store2 = store.add(new TerminalCapabilityStore()); addEvents = []; removeEvents = []; }); - teardown(() => multiplexer.dispose()); + teardown(() => store.dispose()); + + ensureNoDisposablesAreLeakedInTestSuite(); test('should fire events when capabilities are enabled', async () => { assertEvents(addEvents, []); diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalConfigHelper.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalConfigHelper.test.ts index 799c2c74ba8..8764db8e257 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/terminalConfigHelper.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalConfigHelper.test.ts @@ -8,6 +8,8 @@ import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/term import { EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/editorOptions'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { LinuxDistro } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { DisposableStore } from 'vs/base/common/lifecycle'; class TestTerminalConfigHelper extends TerminalConfigHelper { set linuxDistro(distro: LinuxDistro) { @@ -16,6 +18,7 @@ class TestTerminalConfigHelper extends TerminalConfigHelper { } suite('Workbench - TerminalConfigHelper', function () { + let store: DisposableStore; let fixture: HTMLElement; // This suite has retries setup because the font-related tests flake only on GitHub actions, not @@ -24,15 +27,19 @@ suite('Workbench - TerminalConfigHelper', function () { this.retries(3); setup(() => { + store = new DisposableStore(); fixture = document.body; }); + teardown(() => store.dispose()); + + ensureNoDisposablesAreLeakedInTestSuite(); test('TerminalConfigHelper - getFont fontFamily', () => { const configurationService = new TestConfigurationService({ editor: { fontFamily: 'foo' }, terminal: { integrated: { fontFamily: 'bar' } } }); - const configHelper = new TestTerminalConfigHelper(configurationService, null!, null!, null!, null!); + const configHelper = store.add(new TestTerminalConfigHelper(configurationService, null!, null!, null!, null!)); configHelper.panelContainer = fixture; assert.strictEqual(configHelper.getFont().fontFamily, 'bar, monospace', 'terminal.integrated.fontFamily should be selected over editor.fontFamily'); }); @@ -42,7 +49,7 @@ suite('Workbench - TerminalConfigHelper', function () { editor: { fontFamily: 'foo' }, terminal: { integrated: { fontFamily: null } } }); - const configHelper = new TestTerminalConfigHelper(configurationService, null!, null!, null!, null!); + const configHelper = store.add(new TestTerminalConfigHelper(configurationService, null!, null!, null!, null!)); configHelper.linuxDistro = LinuxDistro.Fedora; configHelper.panelContainer = fixture; assert.strictEqual(configHelper.getFont().fontFamily, '\'DejaVu Sans Mono\', monospace', 'Fedora should have its font overridden when terminal.integrated.fontFamily not set'); @@ -53,7 +60,7 @@ suite('Workbench - TerminalConfigHelper', function () { editor: { fontFamily: 'foo' }, terminal: { integrated: { fontFamily: null } } }); - const configHelper = new TestTerminalConfigHelper(configurationService, null!, null!, null!, null!); + const configHelper = store.add(new TestTerminalConfigHelper(configurationService, null!, null!, null!, null!)); configHelper.linuxDistro = LinuxDistro.Ubuntu; configHelper.panelContainer = fixture; assert.strictEqual(configHelper.getFont().fontFamily, '\'Ubuntu Mono\', monospace', 'Ubuntu should have its font overridden when terminal.integrated.fontFamily not set'); @@ -64,7 +71,7 @@ suite('Workbench - TerminalConfigHelper', function () { editor: { fontFamily: 'foo' }, terminal: { integrated: { fontFamily: null } } }); - const configHelper = new TestTerminalConfigHelper(configurationService, null!, null!, null!, null!); + const configHelper = store.add(new TestTerminalConfigHelper(configurationService, null!, null!, null!, null!)); configHelper.panelContainer = fixture; assert.strictEqual(configHelper.getFont().fontFamily, 'foo, monospace', 'editor.fontFamily should be the fallback when terminal.integrated.fontFamily not set'); }); @@ -82,7 +89,7 @@ suite('Workbench - TerminalConfigHelper', function () { } } }); - const configHelper = new TestTerminalConfigHelper(configurationService, null!, null!, null!, null!); + const configHelper = store.add(new TestTerminalConfigHelper(configurationService, null!, null!, null!, null!)); configHelper.panelContainer = fixture; assert.strictEqual(configHelper.getFont().fontSize, 10, 'terminal.integrated.fontSize should be selected over editor.fontSize'); }); @@ -99,12 +106,12 @@ suite('Workbench - TerminalConfigHelper', function () { } } }); - let configHelper = new TestTerminalConfigHelper(configurationService, null!, null!, null!, null!); + let configHelper = store.add(new TestTerminalConfigHelper(configurationService, null!, null!, null!, null!)); configHelper.linuxDistro = LinuxDistro.Ubuntu; configHelper.panelContainer = fixture; assert.strictEqual(configHelper.getFont().fontSize, 8, 'The minimum terminal font size (with adjustment) should be used when terminal.integrated.fontSize less than it'); - configHelper = new TestTerminalConfigHelper(configurationService, null!, null!, null!, null!); + configHelper = store.add(new TestTerminalConfigHelper(configurationService, null!, null!, null!, null!)); configHelper.panelContainer = fixture; assert.strictEqual(configHelper.getFont().fontSize, 6, 'The minimum terminal font size should be used when terminal.integrated.fontSize less than it'); }); @@ -121,7 +128,7 @@ suite('Workbench - TerminalConfigHelper', function () { } } }); - const configHelper = new TestTerminalConfigHelper(configurationService, null!, null!, null!, null!); + const configHelper = store.add(new TestTerminalConfigHelper(configurationService, null!, null!, null!, null!)); configHelper.panelContainer = fixture; assert.strictEqual(configHelper.getFont().fontSize, 100, 'The maximum terminal font size should be used when terminal.integrated.fontSize more than it'); }); @@ -138,12 +145,12 @@ suite('Workbench - TerminalConfigHelper', function () { } } }); - let configHelper = new TestTerminalConfigHelper(configurationService, null!, null!, null!, null!); + let configHelper = store.add(new TestTerminalConfigHelper(configurationService, null!, null!, null!, null!)); configHelper.linuxDistro = LinuxDistro.Ubuntu; configHelper.panelContainer = fixture; assert.strictEqual(configHelper.getFont().fontSize, EDITOR_FONT_DEFAULTS.fontSize + 2, 'The default editor font size (with adjustment) should be used when terminal.integrated.fontSize is not set'); - configHelper = new TestTerminalConfigHelper(configurationService, null!, null!, null!, null!); + configHelper = store.add(new TestTerminalConfigHelper(configurationService, null!, null!, null!, null!)); configHelper.panelContainer = fixture; assert.strictEqual(configHelper.getFont().fontSize, EDITOR_FONT_DEFAULTS.fontSize, 'The default editor font size should be used when terminal.integrated.fontSize is not set'); }); @@ -161,7 +168,7 @@ suite('Workbench - TerminalConfigHelper', function () { } } }); - const configHelper = new TestTerminalConfigHelper(configurationService, null!, null!, null!, null!); + const configHelper = store.add(new TestTerminalConfigHelper(configurationService, null!, null!, null!, null!)); configHelper.panelContainer = fixture; assert.strictEqual(configHelper.getFont().lineHeight, 2, 'terminal.integrated.lineHeight should be selected over editor.lineHeight'); }); @@ -179,7 +186,7 @@ suite('Workbench - TerminalConfigHelper', function () { } } }); - const configHelper = new TestTerminalConfigHelper(configurationService, null!, null!, null!, null!); + const configHelper = store.add(new TestTerminalConfigHelper(configurationService, null!, null!, null!, null!)); configHelper.panelContainer = fixture; assert.strictEqual(configHelper.getFont().lineHeight, 1, 'editor.lineHeight should be 1 when terminal.integrated.lineHeight not set'); }); @@ -193,7 +200,7 @@ suite('Workbench - TerminalConfigHelper', function () { } }); - const configHelper = new TestTerminalConfigHelper(configurationService, null!, null!, null!, null!); + const configHelper = store.add(new TestTerminalConfigHelper(configurationService, null!, null!, null!, null!)); configHelper.panelContainer = fixture; assert.strictEqual(configHelper.configFontIsMonospace(), true, 'monospace is monospaced'); }); @@ -206,7 +213,7 @@ suite('Workbench - TerminalConfigHelper', function () { } } }); - const configHelper = new TestTerminalConfigHelper(configurationService, null!, null!, null!, null!); + const configHelper = store.add(new TestTerminalConfigHelper(configurationService, null!, null!, null!, null!)); configHelper.panelContainer = fixture; assert.strictEqual(configHelper.configFontIsMonospace(), false, 'sans-serif is not monospaced'); }); @@ -219,7 +226,7 @@ suite('Workbench - TerminalConfigHelper', function () { } } }); - const configHelper = new TestTerminalConfigHelper(configurationService, null!, null!, null!, null!); + const configHelper = store.add(new TestTerminalConfigHelper(configurationService, null!, null!, null!, null!)); configHelper.panelContainer = fixture; assert.strictEqual(configHelper.configFontIsMonospace(), false, 'serif is not monospaced'); }); @@ -236,7 +243,7 @@ suite('Workbench - TerminalConfigHelper', function () { } }); - const configHelper = new TestTerminalConfigHelper(configurationService, null!, null!, null!, null!); + const configHelper = store.add(new TestTerminalConfigHelper(configurationService, null!, null!, null!, null!)); configHelper.panelContainer = fixture; assert.strictEqual(configHelper.configFontIsMonospace(), true, 'monospace is monospaced'); }); @@ -253,7 +260,7 @@ suite('Workbench - TerminalConfigHelper', function () { } }); - const configHelper = new TestTerminalConfigHelper(configurationService, null!, null!, null!, null!); + const configHelper = store.add(new TestTerminalConfigHelper(configurationService, null!, null!, null!, null!)); configHelper.panelContainer = fixture; assert.strictEqual(configHelper.configFontIsMonospace(), false, 'sans-serif is not monospaced'); }); @@ -270,7 +277,7 @@ suite('Workbench - TerminalConfigHelper', function () { } }); - const configHelper = new TestTerminalConfigHelper(configurationService, null!, null!, null!, null!); + const configHelper = store.add(new TestTerminalConfigHelper(configurationService, null!, null!, null!, null!)); configHelper.panelContainer = fixture; assert.strictEqual(configHelper.configFontIsMonospace(), false, 'serif is not monospaced'); }); diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalInstance.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalInstance.test.ts index 295fb4bb119..bcbb193e128 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/terminalInstance.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalInstance.test.ts @@ -20,34 +20,19 @@ import { TerminalCapabilityStore } from 'vs/platform/terminal/common/capabilitie import { TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; import { Schemas } from 'vs/base/common/network'; import { TestFileService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { DisposableStore } from 'vs/base/common/lifecycle'; -export function createInstance(partial?: Partial): Pick { - const capabilities = new TerminalCapabilityStore(); - if (!isWindows) { - capabilities.add(TerminalCapability.NaiveCwdDetection, null!); - } - return { - shellLaunchConfig: {}, - cwd: 'cwd', - initialCwd: undefined, - processName: '', - sequence: undefined, - workspaceFolder: undefined, - staticTitle: undefined, - capabilities, - title: '', - description: '', - userHome: undefined, - ...partial - }; -} const root1 = '/foo/root1'; const ROOT_1 = fixPath(root1); const root2 = '/foo/root2'; const ROOT_2 = fixPath(root2); const emptyRoot = '/foo'; const ROOT_EMPTY = fixPath(emptyRoot); + suite('Workbench - TerminalInstance', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + suite('parseExitResult', () => { test('should return no message for exit code = undefined', () => { deepStrictEqual( @@ -155,6 +140,7 @@ suite('Workbench - TerminalInstance', () => { }); }); suite('TerminalLabelComputer', () => { + let store: DisposableStore; let configurationService: TestConfigurationService; let terminalLabelComputer: TerminalLabelComputer; let instantiationService: TestInstantiationService; @@ -166,10 +152,33 @@ suite('Workbench - TerminalInstance', () => { let emptyWorkspace: Workspace; let capabilities: TerminalCapabilityStore; let configHelper: TerminalConfigHelper; + + function createInstance(partial?: Partial): Pick { + const capabilities = store.add(new TerminalCapabilityStore()); + if (!isWindows) { + capabilities.add(TerminalCapability.NaiveCwdDetection, null!); + } + return { + shellLaunchConfig: {}, + cwd: 'cwd', + initialCwd: undefined, + processName: '', + sequence: undefined, + workspaceFolder: undefined, + staticTitle: undefined, + capabilities, + title: '', + description: '', + userHome: undefined, + ...partial + }; + } + setup(async () => { - instantiationService = new TestInstantiationService(); + store = new DisposableStore(); + instantiationService = store.add(new TestInstantiationService()); instantiationService.stub(IWorkspaceContextService, new TestContextService()); - capabilities = new TerminalCapabilityStore(); + capabilities = store.add(new TerminalCapabilityStore()); if (!isWindows) { capabilities.add(TerminalCapability.NaiveCwdDetection, null!); } @@ -190,14 +199,12 @@ suite('Workbench - TerminalInstance', () => { emptyContextService.setWorkspace(emptyWorkspace); }); - teardown(() => { - instantiationService.dispose(); - }); + teardown(() => store.dispose()); test('should resolve to "" when the template variables are empty', () => { configurationService = new TestConfigurationService({ terminal: { integrated: { tabs: { separator: ' - ', title: '', description: '' } } } }); - configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!); - terminalLabelComputer = new TerminalLabelComputer(configHelper, new TestFileService(), mockContextService); + configHelper = store.add(new TerminalConfigHelper(configurationService, null!, null!, null!, null!)); + terminalLabelComputer = store.add(new TerminalLabelComputer(configHelper, new TestFileService(), mockContextService)); terminalLabelComputer.refreshLabel(createInstance({ capabilities, processName: '' })); // TODO: // terminalLabelComputer.onLabelChanged(e => { @@ -209,80 +216,80 @@ suite('Workbench - TerminalInstance', () => { }); test('should resolve cwd', () => { configurationService = new TestConfigurationService({ terminal: { integrated: { tabs: { separator: ' - ', title: '${cwd}', description: '${cwd}' } } } }); - configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!); - terminalLabelComputer = new TerminalLabelComputer(configHelper, new TestFileService(), mockContextService); + configHelper = store.add(new TerminalConfigHelper(configurationService, null!, null!, null!, null!)); + terminalLabelComputer = store.add(new TerminalLabelComputer(configHelper, new TestFileService(), mockContextService)); terminalLabelComputer.refreshLabel(createInstance({ capabilities, cwd: ROOT_1 })); strictEqual(terminalLabelComputer.title, ROOT_1); strictEqual(terminalLabelComputer.description, ROOT_1); }); test('should resolve workspaceFolder', () => { configurationService = new TestConfigurationService({ terminal: { integrated: { tabs: { separator: ' - ', title: '${workspaceFolder}', description: '${workspaceFolder}' } } } }); - configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!); - terminalLabelComputer = new TerminalLabelComputer(configHelper, new TestFileService(), mockContextService); + configHelper = store.add(new TerminalConfigHelper(configurationService, null!, null!, null!, null!)); + terminalLabelComputer = store.add(new TerminalLabelComputer(configHelper, new TestFileService(), mockContextService)); terminalLabelComputer.refreshLabel(createInstance({ capabilities, processName: 'zsh', workspaceFolder: { uri: URI.from({ scheme: Schemas.file, path: 'folder' }) } as IWorkspaceFolder })); strictEqual(terminalLabelComputer.title, 'folder'); strictEqual(terminalLabelComputer.description, 'folder'); }); test('should resolve local', () => { configurationService = new TestConfigurationService({ terminal: { integrated: { tabs: { separator: ' - ', title: '${local}', description: '${local}' } } } }); - configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!); - terminalLabelComputer = new TerminalLabelComputer(configHelper, new TestFileService(), mockContextService); + configHelper = store.add(new TerminalConfigHelper(configurationService, null!, null!, null!, null!)); + terminalLabelComputer = store.add(new TerminalLabelComputer(configHelper, new TestFileService(), mockContextService)); terminalLabelComputer.refreshLabel(createInstance({ capabilities, processName: 'zsh', shellLaunchConfig: { type: 'Local' } })); strictEqual(terminalLabelComputer.title, 'Local'); strictEqual(terminalLabelComputer.description, 'Local'); }); test('should resolve process', () => { configurationService = new TestConfigurationService({ terminal: { integrated: { tabs: { separator: ' - ', title: '${process}', description: '${process}' } } } }); - configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!); - terminalLabelComputer = new TerminalLabelComputer(configHelper, new TestFileService(), mockContextService); + configHelper = store.add(new TerminalConfigHelper(configurationService, null!, null!, null!, null!)); + terminalLabelComputer = store.add(new TerminalLabelComputer(configHelper, new TestFileService(), mockContextService)); terminalLabelComputer.refreshLabel(createInstance({ capabilities, processName: 'zsh' })); strictEqual(terminalLabelComputer.title, 'zsh'); strictEqual(terminalLabelComputer.description, 'zsh'); }); test('should resolve sequence', () => { configurationService = new TestConfigurationService({ terminal: { integrated: { tabs: { separator: ' - ', title: '${sequence}', description: '${sequence}' } } } }); - configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!); - terminalLabelComputer = new TerminalLabelComputer(configHelper, new TestFileService(), mockContextService); + configHelper = store.add(new TerminalConfigHelper(configurationService, null!, null!, null!, null!)); + terminalLabelComputer = store.add(new TerminalLabelComputer(configHelper, new TestFileService(), mockContextService)); terminalLabelComputer.refreshLabel(createInstance({ capabilities, sequence: 'sequence' })); strictEqual(terminalLabelComputer.title, 'sequence'); strictEqual(terminalLabelComputer.description, 'sequence'); }); test('should resolve task', () => { configurationService = new TestConfigurationService({ terminal: { integrated: { tabs: { separator: ' ~ ', title: '${process}${separator}${task}', description: '${task}' } } } }); - configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!); - terminalLabelComputer = new TerminalLabelComputer(configHelper, new TestFileService(), mockContextService); + configHelper = store.add(new TerminalConfigHelper(configurationService, null!, null!, null!, null!)); + terminalLabelComputer = store.add(new TerminalLabelComputer(configHelper, new TestFileService(), mockContextService)); terminalLabelComputer.refreshLabel(createInstance({ capabilities, processName: 'zsh', shellLaunchConfig: { type: 'Task' } })); strictEqual(terminalLabelComputer.title, 'zsh ~ Task'); strictEqual(terminalLabelComputer.description, 'Task'); }); test('should resolve separator', () => { configurationService = new TestConfigurationService({ terminal: { integrated: { tabs: { separator: ' ~ ', title: '${separator}', description: '${separator}' } } } }); - configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!); - terminalLabelComputer = new TerminalLabelComputer(configHelper, new TestFileService(), mockContextService); + configHelper = store.add(new TerminalConfigHelper(configurationService, null!, null!, null!, null!)); + terminalLabelComputer = store.add(new TerminalLabelComputer(configHelper, new TestFileService(), mockContextService)); terminalLabelComputer.refreshLabel(createInstance({ capabilities, processName: 'zsh', shellLaunchConfig: { type: 'Task' } })); strictEqual(terminalLabelComputer.title, 'zsh'); strictEqual(terminalLabelComputer.description, ''); }); test('should always return static title when specified', () => { configurationService = new TestConfigurationService({ terminal: { integrated: { tabs: { separator: ' ~ ', title: '${process}', description: '${workspaceFolder}' } } } }); - configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!); - terminalLabelComputer = new TerminalLabelComputer(configHelper, new TestFileService(), mockContextService); + configHelper = store.add(new TerminalConfigHelper(configurationService, null!, null!, null!, null!)); + terminalLabelComputer = store.add(new TerminalLabelComputer(configHelper, new TestFileService(), mockContextService)); terminalLabelComputer.refreshLabel(createInstance({ capabilities, processName: 'process', workspaceFolder: { uri: URI.from({ scheme: Schemas.file, path: 'folder' }) } as IWorkspaceFolder, staticTitle: 'my-title' })); strictEqual(terminalLabelComputer.title, 'my-title'); strictEqual(terminalLabelComputer.description, 'folder'); }); test('should provide cwdFolder for all cwds only when in multi-root', () => { configurationService = new TestConfigurationService({ terminal: { integrated: { tabs: { separator: ' ~ ', title: '${process}${separator}${cwdFolder}', description: '${cwdFolder}' } } } }); - configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!); - terminalLabelComputer = new TerminalLabelComputer(configHelper, new TestFileService(), mockContextService); + configHelper = store.add(new TerminalConfigHelper(configurationService, null!, null!, null!, null!)); + terminalLabelComputer = store.add(new TerminalLabelComputer(configHelper, new TestFileService(), mockContextService)); terminalLabelComputer.refreshLabel(createInstance({ capabilities, processName: 'process', workspaceFolder: { uri: URI.from({ scheme: Schemas.file, path: ROOT_1 }) } as IWorkspaceFolder, cwd: ROOT_1 })); // single-root, cwd is same as root strictEqual(terminalLabelComputer.title, 'process'); strictEqual(terminalLabelComputer.description, ''); // multi-root configurationService = new TestConfigurationService({ terminal: { integrated: { tabs: { separator: ' ~ ', title: '${process}${separator}${cwdFolder}', description: '${cwdFolder}' } } } }); - configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!); - terminalLabelComputer = new TerminalLabelComputer(configHelper, new TestFileService(), mockMultiRootContextService); + configHelper = store.add(new TerminalConfigHelper(configurationService, null!, null!, null!, null!)); + terminalLabelComputer = store.add(new TerminalLabelComputer(configHelper, new TestFileService(), mockMultiRootContextService)); terminalLabelComputer.refreshLabel(createInstance({ capabilities, processName: 'process', workspaceFolder: { uri: URI.from({ scheme: Schemas.file, path: ROOT_1 }) } as IWorkspaceFolder, cwd: ROOT_2 })); if (isWindows) { strictEqual(terminalLabelComputer.title, 'process'); @@ -294,13 +301,13 @@ suite('Workbench - TerminalInstance', () => { }); test('should hide cwdFolder in single folder workspaces when cwd matches the workspace\'s default cwd even when slashes differ', async () => { configurationService = new TestConfigurationService({ terminal: { integrated: { tabs: { separator: ' ~ ', title: '${process}${separator}${cwdFolder}', description: '${cwdFolder}' } } } }); - configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!); - terminalLabelComputer = new TerminalLabelComputer(configHelper, new TestFileService(), mockContextService); + configHelper = store.add(new TerminalConfigHelper(configurationService, null!, null!, null!, null!)); + terminalLabelComputer = store.add(new TerminalLabelComputer(configHelper, new TestFileService(), mockContextService)); terminalLabelComputer.refreshLabel(createInstance({ capabilities, processName: 'process', workspaceFolder: { uri: URI.from({ scheme: Schemas.file, path: ROOT_1 }) } as IWorkspaceFolder, cwd: ROOT_1 })); strictEqual(terminalLabelComputer.title, 'process'); strictEqual(terminalLabelComputer.description, ''); if (!isWindows) { - terminalLabelComputer = new TerminalLabelComputer(configHelper, new TestFileService(), mockContextService); + terminalLabelComputer = store.add(new TerminalLabelComputer(configHelper, new TestFileService(), mockContextService)); terminalLabelComputer.refreshLabel(createInstance({ capabilities, processName: 'process', workspaceFolder: { uri: URI.from({ scheme: Schemas.file, path: ROOT_1 }) } as IWorkspaceFolder, cwd: ROOT_2 })); strictEqual(terminalLabelComputer.title, 'process ~ root2'); strictEqual(terminalLabelComputer.description, 'root2'); diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalInstanceService.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalInstanceService.test.ts index b36530cb82b..0b14bd97a30 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/terminalInstanceService.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalInstanceService.test.ts @@ -15,6 +15,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { TestEnvironmentService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('Workbench - TerminalInstanceService', () => { let instantiationService: TestInstantiationService; @@ -40,6 +41,8 @@ suite('Workbench - TerminalInstanceService', () => { instantiationService.dispose(); }); + ensureNoDisposablesAreLeakedInTestSuite(); + suite('convertProfileToShellLaunchConfig', () => { test('should return an empty shell launch config when undefined is provided', () => { deepStrictEqual(terminalInstanceService.convertProfileToShellLaunchConfig(), {}); diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalProcessManager.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalProcessManager.test.ts index b7291fc85f4..8f4c4a1410b 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/terminalProcessManager.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalProcessManager.test.ts @@ -14,12 +14,14 @@ import { IEnvironmentVariableService } from 'vs/workbench/contrib/terminal/commo import { EnvironmentVariableService } from 'vs/workbench/contrib/terminal/common/environmentVariableService'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; -import { ITerminalChildProcess } from 'vs/platform/terminal/common/terminal'; +import { ITerminalChildProcess, ITerminalLogService } from 'vs/platform/terminal/common/terminal'; import { ITerminalProfileResolverService } from 'vs/workbench/contrib/terminal/common/terminal'; import { ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { Event } from 'vs/base/common/event'; import { TestProductService } from 'vs/workbench/test/common/workbenchTestServices'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { NullLogService } from 'vs/platform/log/common/log'; class TestTerminalChildProcess implements ITerminalChildProcess { id: number = 0; @@ -73,19 +75,20 @@ class TestTerminalInstanceService implements Partial { env: any, windowsEnableConpty: boolean, shouldPersist: boolean - ) => new TestTerminalChildProcess(shouldPersist) + ) => new TestTerminalChildProcess(shouldPersist), + getLatency: () => Promise.resolve([]) } as any; } } suite('Workbench - TerminalProcessManager', () => { - let disposables: DisposableStore; + let store: DisposableStore; let instantiationService: ITestInstantiationService; let manager: TerminalProcessManager; setup(async () => { - disposables = new DisposableStore(); - instantiationService = workbenchInstantiationService(undefined, disposables); + store = new DisposableStore(); + instantiationService = workbenchInstantiationService(undefined, store); const configurationService = new TestConfigurationService(); await configurationService.setUserConfiguration('editor', { fontFamily: 'foo' }); await configurationService.setUserConfiguration('terminal', { @@ -99,17 +102,18 @@ suite('Workbench - TerminalProcessManager', () => { }); instantiationService.stub(IConfigurationService, configurationService); instantiationService.stub(IProductService, TestProductService); + instantiationService.stub(ITerminalLogService, new NullLogService()); instantiationService.stub(IEnvironmentVariableService, instantiationService.createInstance(EnvironmentVariableService)); instantiationService.stub(ITerminalProfileResolverService, TestTerminalProfileResolverService); instantiationService.stub(ITerminalInstanceService, new TestTerminalInstanceService()); - const configHelper = instantiationService.createInstance(TerminalConfigHelper); - manager = instantiationService.createInstance(TerminalProcessManager, 1, configHelper, undefined, undefined, undefined); + const configHelper = store.add(instantiationService.createInstance(TerminalConfigHelper)); + manager = store.add(instantiationService.createInstance(TerminalProcessManager, 1, configHelper, undefined, undefined, undefined)); }); - teardown(() => { - disposables.dispose(); - }); + teardown(() => store.dispose()); + + ensureNoDisposablesAreLeakedInTestSuite(); suite('process persistence', () => { suite('local', () => { diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalService.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalService.test.ts index 4ba34944a97..e2051ef04aa 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/terminalService.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalService.test.ts @@ -5,14 +5,14 @@ import { fail } from 'assert'; import { Emitter } from 'vs/base/common/event'; -import { TerminalLocation } from 'vs/platform/terminal/common/terminal'; +import { ITerminalLogService, TerminalLocation } from 'vs/platform/terminal/common/terminal'; import { TerminalService } from 'vs/workbench/contrib/terminal/browser/terminalService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ContextKeyService } from 'vs/platform/contextkey/browser/contextKeyService'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { TestEditorService, TestLifecycleService, TestRemoteAgentService, TestTerminalEditorService, TestTerminalGroupService, TestTerminalInstanceService, TestTerminalProfileService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestEditorService, TestEnvironmentService, TestLifecycleService, TestRemoteAgentService, TestTerminalEditorService, TestTerminalGroupService, TestTerminalInstanceService, TestTerminalProfileService } from 'vs/workbench/test/browser/workbenchTestServices'; import { ITerminalEditorService, ITerminalGroupService, ITerminalInstance, ITerminalInstanceService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -22,14 +22,20 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { NullLogService } from 'vs/platform/log/common/log'; suite('Workbench - TerminalService', () => { + let store: DisposableStore; let instantiationService: TestInstantiationService; let terminalService: TerminalService; let configurationService: TestConfigurationService; let dialogService: TestDialogService; setup(async () => { + store = new DisposableStore(); dialogService = new TestDialogService(); configurationService = new TestConfigurationService({ terminal: { @@ -39,106 +45,96 @@ suite('Workbench - TerminalService', () => { } }); - instantiationService = new TestInstantiationService(); + instantiationService = store.add(new TestInstantiationService()); instantiationService.stub(IConfigurationService, configurationService); instantiationService.stub(IContextKeyService, instantiationService.createInstance(ContextKeyService)); instantiationService.stub(ILifecycleService, new TestLifecycleService()); instantiationService.stub(IThemeService, new TestThemeService()); + instantiationService.stub(ITerminalLogService, new NullLogService()); instantiationService.stub(IEditorService, new TestEditorService()); + instantiationService.stub(IEnvironmentService, TestEnvironmentService); instantiationService.stub(ITerminalEditorService, new TestTerminalEditorService()); instantiationService.stub(ITerminalGroupService, new TestTerminalGroupService()); instantiationService.stub(ITerminalInstanceService, new TestTerminalInstanceService()); + instantiationService.stub(ITerminalInstanceService, 'getBackend', undefined); + instantiationService.stub(ITerminalInstanceService, 'getRegisteredBackends', []); instantiationService.stub(ITerminalProfileService, new TestTerminalProfileService()); instantiationService.stub(IRemoteAgentService, new TestRemoteAgentService()); instantiationService.stub(IRemoteAgentService, 'getConnection', null); instantiationService.stub(IDialogService, dialogService); - terminalService = instantiationService.createInstance(TerminalService); + terminalService = store.add(instantiationService.createInstance(TerminalService)); instantiationService.stub(ITerminalService, terminalService); }); - teardown(() => { - instantiationService.dispose(); - }); + teardown(() => store.dispose()); + + ensureNoDisposablesAreLeakedInTestSuite(); suite('safeDisposeTerminal', () => { let onExitEmitter: Emitter; setup(() => { - onExitEmitter = new Emitter(); + onExitEmitter = store.add(new Emitter()); }); test('should not show prompt when confirmOnKill is never', async () => { - setConfirmOnKill(configurationService, 'never'); - await new Promise(r => { - terminalService.safeDisposeTerminal({ - target: TerminalLocation.Editor, - hasChildProcesses: true, - onExit: onExitEmitter.event, - dispose: () => r() - } as Partial as any); - }); - await new Promise(r => { - terminalService.safeDisposeTerminal({ - target: TerminalLocation.Panel, - hasChildProcesses: true, - onExit: onExitEmitter.event, - dispose: () => r() - } as Partial as any); - }); + await setConfirmOnKill(configurationService, 'never'); + await terminalService.safeDisposeTerminal({ + target: TerminalLocation.Editor, + hasChildProcesses: true, + onExit: onExitEmitter.event, + dispose: () => onExitEmitter.fire(undefined) + } as Partial as any); + await terminalService.safeDisposeTerminal({ + target: TerminalLocation.Panel, + hasChildProcesses: true, + onExit: onExitEmitter.event, + dispose: () => onExitEmitter.fire(undefined) + } as Partial as any); }); test('should not show prompt when any terminal editor is closed (handled by editor itself)', async () => { - setConfirmOnKill(configurationService, 'editor'); - await new Promise(r => { - terminalService.safeDisposeTerminal({ - target: TerminalLocation.Editor, - hasChildProcesses: true, - onExit: onExitEmitter.event, - dispose: () => r() - } as Partial as any); - }); - setConfirmOnKill(configurationService, 'always'); - await new Promise(r => { - terminalService.safeDisposeTerminal({ - target: TerminalLocation.Editor, - hasChildProcesses: true, - onExit: onExitEmitter.event, - dispose: () => r() - } as Partial as any); - }); + await setConfirmOnKill(configurationService, 'editor'); + terminalService.safeDisposeTerminal({ + target: TerminalLocation.Editor, + hasChildProcesses: true, + onExit: onExitEmitter.event, + dispose: () => onExitEmitter.fire(undefined) + } as Partial as any); + await setConfirmOnKill(configurationService, 'always'); + terminalService.safeDisposeTerminal({ + target: TerminalLocation.Editor, + hasChildProcesses: true, + onExit: onExitEmitter.event, + dispose: () => onExitEmitter.fire(undefined) + } as Partial as any); }); test('should not show prompt when confirmOnKill is editor and panel terminal is closed', async () => { - setConfirmOnKill(configurationService, 'editor'); - await new Promise(r => { - terminalService.safeDisposeTerminal({ - target: TerminalLocation.Panel, - hasChildProcesses: true, - onExit: onExitEmitter.event, - dispose: () => r() - } as Partial as any); - }); + await setConfirmOnKill(configurationService, 'editor'); + terminalService.safeDisposeTerminal({ + target: TerminalLocation.Panel, + hasChildProcesses: true, + onExit: onExitEmitter.event, + dispose: () => onExitEmitter.fire(undefined) + } as Partial as any); }); test('should show prompt when confirmOnKill is panel and panel terminal is closed', async () => { - setConfirmOnKill(configurationService, 'panel'); + await setConfirmOnKill(configurationService, 'panel'); // No child process cases dialogService.setConfirmResult({ confirmed: false }); - await new Promise(r => { - terminalService.safeDisposeTerminal({ - target: TerminalLocation.Panel, - hasChildProcesses: false, - onExit: onExitEmitter.event, - dispose: () => r() - } as Partial as any); - }); + terminalService.safeDisposeTerminal({ + target: TerminalLocation.Panel, + hasChildProcesses: false, + onExit: onExitEmitter.event, + dispose: () => onExitEmitter.fire(undefined) + } as Partial as any); dialogService.setConfirmResult({ confirmed: true }); - await new Promise(r => { - terminalService.safeDisposeTerminal({ - target: TerminalLocation.Panel, - hasChildProcesses: false, - onExit: onExitEmitter.event, - dispose: () => r() - } as Partial as any); - }); + terminalService.safeDisposeTerminal({ + target: TerminalLocation.Panel, + hasChildProcesses: false, + onExit: onExitEmitter.event, + dispose: () => onExitEmitter.fire(undefined) + } as Partial as any); // Child process cases dialogService.setConfirmResult({ confirmed: false }); await terminalService.safeDisposeTerminal({ @@ -147,36 +143,30 @@ suite('Workbench - TerminalService', () => { dispose: () => fail() } as Partial as any); dialogService.setConfirmResult({ confirmed: true }); - await new Promise(r => { - terminalService.safeDisposeTerminal({ - target: TerminalLocation.Panel, - hasChildProcesses: true, - onExit: onExitEmitter.event, - dispose: () => r() - } as Partial as any); - }); + terminalService.safeDisposeTerminal({ + target: TerminalLocation.Panel, + hasChildProcesses: true, + onExit: onExitEmitter.event, + dispose: () => onExitEmitter.fire(undefined) + } as Partial as any); }); test('should show prompt when confirmOnKill is always and panel terminal is closed', async () => { - setConfirmOnKill(configurationService, 'always'); + await setConfirmOnKill(configurationService, 'always'); // No child process cases dialogService.setConfirmResult({ confirmed: false }); - await new Promise(r => { - terminalService.safeDisposeTerminal({ - target: TerminalLocation.Panel, - hasChildProcesses: false, - onExit: onExitEmitter.event, - dispose: () => r() - } as Partial as any); - }); + terminalService.safeDisposeTerminal({ + target: TerminalLocation.Panel, + hasChildProcesses: false, + onExit: onExitEmitter.event, + dispose: () => onExitEmitter.fire(undefined) + } as Partial as any); dialogService.setConfirmResult({ confirmed: true }); - await new Promise(r => { - terminalService.safeDisposeTerminal({ - target: TerminalLocation.Panel, - hasChildProcesses: false, - onExit: onExitEmitter.event, - dispose: () => r() - } as Partial as any); - }); + terminalService.safeDisposeTerminal({ + target: TerminalLocation.Panel, + hasChildProcesses: false, + onExit: onExitEmitter.event, + dispose: () => onExitEmitter.fire(undefined) + } as Partial as any); // Child process cases dialogService.setConfirmResult({ confirmed: false }); await terminalService.safeDisposeTerminal({ @@ -185,14 +175,12 @@ suite('Workbench - TerminalService', () => { dispose: () => fail() } as Partial as any); dialogService.setConfirmResult({ confirmed: true }); - await new Promise(r => { - terminalService.safeDisposeTerminal({ - target: TerminalLocation.Panel, - hasChildProcesses: true, - onExit: onExitEmitter.event, - dispose: () => r() - } as Partial as any); - }); + terminalService.safeDisposeTerminal({ + target: TerminalLocation.Panel, + hasChildProcesses: true, + onExit: onExitEmitter.event, + dispose: () => onExitEmitter.fire(undefined) + } as Partial as any); }); }); }); diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalStatusList.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalStatusList.test.ts index 21d76a53d16..af6a7c3fbe1 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/terminalStatusList.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalStatusList.test.ts @@ -11,23 +11,27 @@ import { spinningLoading } from 'vs/platform/theme/common/iconRegistry'; import { ThemeIcon } from 'vs/base/common/themables'; import { TerminalStatusList } from 'vs/workbench/contrib/terminal/browser/terminalStatusList'; import { ITerminalStatus } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { DisposableStore } from 'vs/base/common/lifecycle'; function statusesEqual(list: TerminalStatusList, expected: [string, Severity][]) { deepStrictEqual(list.statuses.map(e => [e.id, e.severity]), expected); } suite('Workbench - TerminalStatusList', () => { + let store: DisposableStore; let list: TerminalStatusList; let configService: TestConfigurationService; setup(() => { + store = new DisposableStore(); configService = new TestConfigurationService(); - list = new TerminalStatusList(configService); + list = store.add(new TerminalStatusList(configService)); }); - teardown(() => { - list.dispose(); - }); + teardown(() => store.dispose()); + + ensureNoDisposablesAreLeakedInTestSuite(); test('primary', () => { strictEqual(list.primary?.id, undefined); @@ -72,7 +76,7 @@ suite('Workbench - TerminalStatusList', () => { test('onDidAddStatus', async () => { const result = await new Promise(r => { - list.onDidAddStatus(r); + store.add(list.onDidAddStatus(r)); list.add({ id: 'test', severity: Severity.Info }); }); deepStrictEqual(result, { id: 'test', severity: Severity.Info }); @@ -80,7 +84,7 @@ suite('Workbench - TerminalStatusList', () => { test('onDidRemoveStatus', async () => { const result = await new Promise(r => { - list.onDidRemoveStatus(r); + store.add(list.onDidRemoveStatus(r)); list.add({ id: 'test', severity: Severity.Info }); list.remove('test'); }); @@ -89,7 +93,7 @@ suite('Workbench - TerminalStatusList', () => { test('onDidChangePrimaryStatus', async () => { const result = await new Promise(r => { - list.onDidRemoveStatus(r); + store.add(list.onDidRemoveStatus(r)); list.add({ id: 'test', severity: Severity.Info }); list.remove('test'); }); @@ -132,8 +136,8 @@ suite('Workbench - TerminalStatusList', () => { test('add should fire onDidRemoveStatus if same status id with a different object reference was added', () => { const eventCalls: string[] = []; - list.onDidAddStatus(() => eventCalls.push('add')); - list.onDidRemoveStatus(() => eventCalls.push('remove')); + store.add(list.onDidAddStatus(() => eventCalls.push('add'))); + store.add(list.onDidRemoveStatus(() => eventCalls.push('remove'))); list.add({ id: 'test', severity: Severity.Info }); list.add({ id: 'test', severity: Severity.Info }); deepStrictEqual(eventCalls, [ diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalUri.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalUri.test.ts index 1a5430116a2..2b01688244c 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/terminalUri.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalUri.test.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { deepStrictEqual, strictEqual } from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { getInstanceFromResource, getTerminalResourcesFromDragEvent, getTerminalUri, IPartialDragEvent } from 'vs/workbench/contrib/terminal/browser/terminalUri'; function fakeDragEvent(data: string): IPartialDragEvent { @@ -17,6 +18,8 @@ function fakeDragEvent(data: string): IPartialDragEvent { } suite('terminalUri', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + suite('getTerminalResourcesFromDragEvent', () => { test('should give undefined when no terminal resources is in event', () => { deepStrictEqual( diff --git a/src/vs/workbench/contrib/terminal/test/browser/xterm/decorationAddon.test.ts b/src/vs/workbench/contrib/terminal/test/browser/xterm/decorationAddon.test.ts index b9306c6c0a7..bfbfebf1d3d 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/xterm/decorationAddon.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/xterm/decorationAddon.test.ts @@ -20,6 +20,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { TestLifecycleService } from 'vs/workbench/test/browser/workbenchTestServices'; import { importAMDNodeModule } from 'vs/amdX'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('DecorationAddon', () => { let decorationAddon: DecorationAddon; @@ -71,6 +72,8 @@ suite('DecorationAddon', () => { instantiationService.dispose(); }); + ensureNoDisposablesAreLeakedInTestSuite(); + suite('registerDecoration', () => { test('should throw when command has no marker', async () => { throws(() => decorationAddon.registerCommandDecoration({ command: 'cd src', timestamp: Date.now(), hasOutput: () => false } as ITerminalCommand)); diff --git a/src/vs/workbench/contrib/terminal/test/browser/xterm/lineDataEventAddon.test.ts b/src/vs/workbench/contrib/terminal/test/browser/xterm/lineDataEventAddon.test.ts index 8953411f4a9..e7099524039 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/xterm/lineDataEventAddon.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/xterm/lineDataEventAddon.test.ts @@ -9,22 +9,29 @@ import { OperatingSystem } from 'vs/base/common/platform'; import { deepStrictEqual } from 'assert'; import { importAMDNodeModule } from 'vs/amdX'; import { writeP } from 'vs/workbench/contrib/terminal/browser/terminalTestHelpers'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { DisposableStore } from 'vs/base/common/lifecycle'; suite('LineDataEventAddon', () => { let xterm: Terminal; let lineDataEventAddon: LineDataEventAddon; + let store: DisposableStore; + setup(() => store = new DisposableStore()); + teardown(() => store.dispose()); + ensureNoDisposablesAreLeakedInTestSuite(); + suite('onLineData', () => { let events: string[]; setup(async () => { const TerminalCtor = (await importAMDNodeModule('xterm', 'lib/xterm.js')).Terminal; - xterm = new TerminalCtor({ allowProposedApi: true, cols: 4 }); - lineDataEventAddon = new LineDataEventAddon(); + xterm = store.add(new TerminalCtor({ allowProposedApi: true, cols: 4 })); + lineDataEventAddon = store.add(new LineDataEventAddon()); xterm.loadAddon(lineDataEventAddon); events = []; - lineDataEventAddon.onLineData(e => events.push(e)); + store.add(lineDataEventAddon.onLineData(e => events.push(e))); }); test('should fire when a non-wrapped line ends with a line feed', async () => { diff --git a/src/vs/workbench/contrib/terminal/test/browser/xterm/shellIntegrationAddon.test.ts b/src/vs/workbench/contrib/terminal/test/browser/xterm/shellIntegrationAddon.test.ts index 5cba5252508..1fcf433b59c 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/xterm/shellIntegrationAddon.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/xterm/shellIntegrationAddon.test.ts @@ -11,6 +11,8 @@ import { ITerminalCapabilityStore, TerminalCapability } from 'vs/platform/termin import { NullLogService } from 'vs/platform/log/common/log'; import { importAMDNodeModule } from 'vs/amdX'; import { writeP } from 'vs/workbench/contrib/terminal/browser/terminalTestHelpers'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; class TestShellIntegrationAddon extends ShellIntegrationAddon { getCommandDetectionMock(terminal: Terminal): sinon.SinonMock { @@ -26,15 +28,19 @@ class TestShellIntegrationAddon extends ShellIntegrationAddon { } suite('ShellIntegrationAddon', () => { + let store: DisposableStore; + setup(() => store = new DisposableStore()); + teardown(() => store.dispose()); + ensureNoDisposablesAreLeakedInTestSuite(); + let xterm: Terminal; let shellIntegrationAddon: TestShellIntegrationAddon; let capabilities: ITerminalCapabilityStore; setup(async () => { - const TerminalCtor = (await importAMDNodeModule('xterm', 'lib/xterm.js')).Terminal; - xterm = new TerminalCtor({ allowProposedApi: true, cols: 80, rows: 30 }); - shellIntegrationAddon = new TestShellIntegrationAddon('', true, undefined, new NullLogService()); + xterm = store.add(new TerminalCtor({ allowProposedApi: true, cols: 80, rows: 30 })); + shellIntegrationAddon = store.add(new TestShellIntegrationAddon('', true, undefined, new NullLogService())); xterm.loadAddon(shellIntegrationAddon); capabilities = shellIntegrationAddon.capabilities; }); @@ -256,59 +262,59 @@ suite('ShellIntegrationAddon', () => { }); }); }); -}); -suite('deserializeMessage', () => { - // A single literal backslash, in order to avoid confusion about whether we are escaping test data or testing escapes. - const Backslash = '\\' as const; - const Newline = '\n' as const; - const Semicolon = ';' as const; + suite('deserializeMessage', () => { + // A single literal backslash, in order to avoid confusion about whether we are escaping test data or testing escapes. + const Backslash = '\\' as const; + const Newline = '\n' as const; + const Semicolon = ';' as const; - type TestCase = [title: string, input: string, expected: string]; - const cases: TestCase[] = [ - ['empty', '', ''], - ['basic', 'value', 'value'], - ['space', 'some thing', 'some thing'], - ['escaped backslash', `${Backslash}${Backslash}`, Backslash], - ['non-initial escaped backslash', `foo${Backslash}${Backslash}`, `foo${Backslash}`], - ['two escaped backslashes', `${Backslash}${Backslash}${Backslash}${Backslash}`, `${Backslash}${Backslash}`], - ['escaped backslash amidst text', `Hello${Backslash}${Backslash}there`, `Hello${Backslash}there`], - ['backslash escaped literally and as hex', `${Backslash}${Backslash} is same as ${Backslash}x5c`, `${Backslash} is same as ${Backslash}`], - ['escaped semicolon', `${Backslash}x3b`, Semicolon], - ['non-initial escaped semicolon', `foo${Backslash}x3b`, `foo${Semicolon}`], - ['escaped semicolon (upper hex)', `${Backslash}x3B`, Semicolon], - ['escaped backslash followed by literal "x3b" is not a semicolon', `${Backslash}${Backslash}x3b`, `${Backslash}x3b`], - ['non-initial escaped backslash followed by literal "x3b" is not a semicolon', `foo${Backslash}${Backslash}x3b`, `foo${Backslash}x3b`], - ['escaped backslash followed by escaped semicolon', `${Backslash}${Backslash}${Backslash}x3b`, `${Backslash}${Semicolon}`], - ['escaped semicolon amidst text', `some${Backslash}x3bthing`, `some${Semicolon}thing`], - ['escaped newline', `${Backslash}x0a`, Newline], - ['non-initial escaped newline', `foo${Backslash}x0a`, `foo${Newline}`], - ['escaped newline (upper hex)', `${Backslash}x0A`, Newline], - ['escaped backslash followed by literal "x0a" is not a newline', `${Backslash}${Backslash}x0a`, `${Backslash}x0a`], - ['non-initial escaped backslash followed by literal "x0a" is not a newline', `foo${Backslash}${Backslash}x0a`, `foo${Backslash}x0a`], - ]; + type TestCase = [title: string, input: string, expected: string]; + const cases: TestCase[] = [ + ['empty', '', ''], + ['basic', 'value', 'value'], + ['space', 'some thing', 'some thing'], + ['escaped backslash', `${Backslash}${Backslash}`, Backslash], + ['non-initial escaped backslash', `foo${Backslash}${Backslash}`, `foo${Backslash}`], + ['two escaped backslashes', `${Backslash}${Backslash}${Backslash}${Backslash}`, `${Backslash}${Backslash}`], + ['escaped backslash amidst text', `Hello${Backslash}${Backslash}there`, `Hello${Backslash}there`], + ['backslash escaped literally and as hex', `${Backslash}${Backslash} is same as ${Backslash}x5c`, `${Backslash} is same as ${Backslash}`], + ['escaped semicolon', `${Backslash}x3b`, Semicolon], + ['non-initial escaped semicolon', `foo${Backslash}x3b`, `foo${Semicolon}`], + ['escaped semicolon (upper hex)', `${Backslash}x3B`, Semicolon], + ['escaped backslash followed by literal "x3b" is not a semicolon', `${Backslash}${Backslash}x3b`, `${Backslash}x3b`], + ['non-initial escaped backslash followed by literal "x3b" is not a semicolon', `foo${Backslash}${Backslash}x3b`, `foo${Backslash}x3b`], + ['escaped backslash followed by escaped semicolon', `${Backslash}${Backslash}${Backslash}x3b`, `${Backslash}${Semicolon}`], + ['escaped semicolon amidst text', `some${Backslash}x3bthing`, `some${Semicolon}thing`], + ['escaped newline', `${Backslash}x0a`, Newline], + ['non-initial escaped newline', `foo${Backslash}x0a`, `foo${Newline}`], + ['escaped newline (upper hex)', `${Backslash}x0A`, Newline], + ['escaped backslash followed by literal "x0a" is not a newline', `${Backslash}${Backslash}x0a`, `${Backslash}x0a`], + ['non-initial escaped backslash followed by literal "x0a" is not a newline', `foo${Backslash}${Backslash}x0a`, `foo${Backslash}x0a`], + ]; - cases.forEach(([title, input, expected]) => { - test(title, () => strictEqual(deserializeMessage(input), expected)); - }); -}); - -test('parseKeyValueAssignment', () => { - type TestCase = [title: string, input: string, expected: [key: string, value: string | undefined]]; - const cases: TestCase[] = [ - ['empty', '', ['', undefined]], - ['no "=" sign', 'some-text', ['some-text', undefined]], - ['empty value', 'key=', ['key', '']], - ['empty key', '=value', ['', 'value']], - ['normal', 'key=value', ['key', 'value']], - ['multiple "=" signs (1)', 'key==value', ['key', '=value']], - ['multiple "=" signs (2)', 'key=value===true', ['key', 'value===true']], - ['just a "="', '=', ['', '']], - ['just a "=="', '==', ['', '=']], - ]; - - cases.forEach(x => { - const [title, input, [key, value]] = x; - deepStrictEqual(parseKeyValueAssignment(input), { key, value }, title); + cases.forEach(([title, input, expected]) => { + test(title, () => strictEqual(deserializeMessage(input), expected)); + }); + }); + + test('parseKeyValueAssignment', () => { + type TestCase = [title: string, input: string, expected: [key: string, value: string | undefined]]; + const cases: TestCase[] = [ + ['empty', '', ['', undefined]], + ['no "=" sign', 'some-text', ['some-text', undefined]], + ['empty value', 'key=', ['key', '']], + ['empty key', '=value', ['', 'value']], + ['normal', 'key=value', ['key', 'value']], + ['multiple "=" signs (1)', 'key==value', ['key', '=value']], + ['multiple "=" signs (2)', 'key=value===true', ['key', 'value===true']], + ['just a "="', '=', ['', '']], + ['just a "=="', '==', ['', '=']], + ]; + + cases.forEach(x => { + const [title, input, [key, value]] = x; + deepStrictEqual(parseKeyValueAssignment(input), { key, value }, title); + }); }); }); diff --git a/src/vs/workbench/contrib/terminal/test/browser/xterm/xtermTerminal.test.ts b/src/vs/workbench/contrib/terminal/test/browser/xterm/xtermTerminal.test.ts index 204505dd1e6..c43605db7b4 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/xterm/xtermTerminal.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/xterm/xtermTerminal.test.ts @@ -33,6 +33,7 @@ import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKe import { Color, RGBA } from 'vs/base/common/color'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ITerminalLogService } from 'vs/platform/terminal/common/terminal'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; class TestWebglAddon implements WebglAddon { static shouldThrow = false; @@ -92,6 +93,8 @@ const defaultTerminalConfig: Partial = { }; suite('XtermTerminal', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + let instantiationService: TestInstantiationService; let configurationService: TestConfigurationService; let themeService: TestThemeService; @@ -113,29 +116,26 @@ suite('XtermTerminal', () => { themeService = new TestThemeService(); viewDescriptorService = new TestViewDescriptorService(); - instantiationService = new TestInstantiationService(); + instantiationService = store.add(new TestInstantiationService()); instantiationService.stub(IConfigurationService, configurationService); instantiationService.stub(ITerminalLogService, new NullLogService()); - instantiationService.stub(IStorageService, new TestStorageService()); + instantiationService.stub(IStorageService, store.add(new TestStorageService())); instantiationService.stub(IThemeService, themeService); instantiationService.stub(IViewDescriptorService, viewDescriptorService); - instantiationService.stub(IContextMenuService, instantiationService.createInstance(ContextMenuService)); - instantiationService.stub(ILifecycleService, new TestLifecycleService()); + instantiationService.stub(IContextMenuService, store.add(instantiationService.createInstance(ContextMenuService))); + instantiationService.stub(ILifecycleService, store.add(new TestLifecycleService())); instantiationService.stub(IContextKeyService, new MockContextKeyService()); - configHelper = instantiationService.createInstance(TerminalConfigHelper); + configHelper = store.add(instantiationService.createInstance(TerminalConfigHelper)); XTermBaseCtor = (await importAMDNodeModule('xterm', 'lib/xterm.js')).Terminal; - xterm = instantiationService.createInstance(TestXtermTerminal, XTermBaseCtor, configHelper, 80, 30, { getBackgroundColor: () => undefined }, new TerminalCapabilityStore(), '', new MockContextKeyService().createKey('', true)!, true); + const capabilityStore = store.add(new TerminalCapabilityStore()); + xterm = store.add(instantiationService.createInstance(TestXtermTerminal, XTermBaseCtor, configHelper, 80, 30, { getBackgroundColor: () => undefined }, capabilityStore, '', new MockContextKeyService().createKey('', true)!, true)); TestWebglAddon.shouldThrow = false; TestWebglAddon.isEnabled = false; }); - teardown(() => { - instantiationService.dispose(); - }); - test('should use fallback dimensions of 80x30', () => { strictEqual(xterm.raw.cols, 80); strictEqual(xterm.raw.rows, 30); @@ -147,7 +147,7 @@ suite('XtermTerminal', () => { [PANEL_BACKGROUND]: '#ff0000', [SIDE_BAR_BACKGROUND]: '#00ff00' })); - xterm = instantiationService.createInstance(XtermTerminal, XTermBaseCtor, configHelper, 80, 30, { getBackgroundColor: () => new Color(new RGBA(255, 0, 0)) }, new TerminalCapabilityStore(), '', new MockContextKeyService().createKey('', true)!, true); + xterm = store.add(instantiationService.createInstance(XtermTerminal, XTermBaseCtor, configHelper, 80, 30, { getBackgroundColor: () => new Color(new RGBA(255, 0, 0)) }, store.add(new TerminalCapabilityStore()), '', new MockContextKeyService().createKey('', true)!, true)); strictEqual(xterm.raw.options.theme?.background, '#ff0000'); }); test('should react to and apply theme changes', () => { @@ -176,7 +176,7 @@ suite('XtermTerminal', () => { 'terminal.ansiBrightCyan': '#150000', 'terminal.ansiBrightWhite': '#160000', })); - xterm = instantiationService.createInstance(XtermTerminal, XTermBaseCtor, configHelper, 80, 30, { getBackgroundColor: () => undefined }, new TerminalCapabilityStore(), '', new MockContextKeyService().createKey('', true)!, true); + xterm = store.add(instantiationService.createInstance(XtermTerminal, XTermBaseCtor, configHelper, 80, 30, { getBackgroundColor: () => undefined }, store.add(new TerminalCapabilityStore()), '', new MockContextKeyService().createKey('', true)!, true)); deepStrictEqual(xterm.raw.options.theme, { background: undefined, foreground: '#000200', diff --git a/src/vs/workbench/contrib/terminal/test/common/environmentVariableCollection.test.ts b/src/vs/workbench/contrib/terminal/test/common/environmentVariableCollection.test.ts index cb047ca042b..3367f87854f 100644 --- a/src/vs/workbench/contrib/terminal/test/common/environmentVariableCollection.test.ts +++ b/src/vs/workbench/contrib/terminal/test/common/environmentVariableCollection.test.ts @@ -9,8 +9,11 @@ import { IProcessEnvironment, isWindows } from 'vs/base/common/platform'; import { MergedEnvironmentVariableCollection } from 'vs/platform/terminal/common/environmentVariableCollection'; import { deserializeEnvironmentDescriptionMap, deserializeEnvironmentVariableCollection } from 'vs/platform/terminal/common/environmentVariableShared'; import { URI } from 'vs/base/common/uri'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + suite('ctor', () => { test('Should keep entries that come after a Prepend or Append type mutators', () => { const merged = new MergedEnvironmentVariableCollection(new Map([ diff --git a/src/vs/workbench/contrib/terminal/test/common/environmentVariableService.test.ts b/src/vs/workbench/contrib/terminal/test/common/environmentVariableService.test.ts index e338f9d55fd..631a0b7bc86 100644 --- a/src/vs/workbench/contrib/terminal/test/common/environmentVariableService.test.ts +++ b/src/vs/workbench/contrib/terminal/test/common/environmentVariableService.test.ts @@ -14,6 +14,7 @@ import { Emitter } from 'vs/base/common/event'; import { IProcessEnvironment } from 'vs/base/common/platform'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { URI } from 'vs/base/common/uri'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; class TestEnvironmentVariableService extends EnvironmentVariableService { persistCollections(): void { this._persistCollections(); } @@ -21,6 +22,8 @@ class TestEnvironmentVariableService extends EnvironmentVariableService { } suite('EnvironmentVariable - EnvironmentVariableService', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + let instantiationService: TestInstantiationService; let environmentVariableService: TestEnvironmentVariableService; let storageService: TestStorageService; @@ -28,27 +31,23 @@ suite('EnvironmentVariable - EnvironmentVariableService', () => { let changeExtensionsEvent: Emitter; setup(() => { - changeExtensionsEvent = new Emitter(); + changeExtensionsEvent = store.add(new Emitter()); - instantiationService = new TestInstantiationService(); + instantiationService = store.add(new TestInstantiationService()); instantiationService.stub(IExtensionService, TestExtensionService); - storageService = new TestStorageService(); + storageService = store.add(new TestStorageService()); historyService = new TestHistoryService(); instantiationService.stub(IStorageService, storageService); instantiationService.stub(IExtensionService, TestExtensionService); instantiationService.stub(IExtensionService, 'onDidChangeExtensions', changeExtensionsEvent.event); - instantiationService.stub(IExtensionService, 'getExtensions', [ + instantiationService.stub(IExtensionService, 'extensions', [ { identifier: { value: 'ext1' } }, { identifier: { value: 'ext2' } }, { identifier: { value: 'ext3' } } ]); instantiationService.stub(IHistoryService, historyService); - environmentVariableService = instantiationService.createInstance(TestEnvironmentVariableService); - }); - - teardown(() => { - instantiationService.dispose(); + environmentVariableService = store.add(instantiationService.createInstance(TestEnvironmentVariableService)); }); test('should persist collections to the storage service and be able to restore from them', () => { @@ -65,7 +64,7 @@ suite('EnvironmentVariable - EnvironmentVariableService', () => { // Persist with old service, create a new service with the same storage service to verify restore environmentVariableService.persistCollections(); - const service2: TestEnvironmentVariableService = instantiationService.createInstance(TestEnvironmentVariableService); + const service2: TestEnvironmentVariableService = store.add(instantiationService.createInstance(TestEnvironmentVariableService)); deepStrictEqual([...service2.mergedCollection.getVariableMap(undefined).entries()], [ ['A', [{ extensionIdentifier: 'ext1', type: EnvironmentVariableMutatorType.Replace, value: 'a', variable: 'A', options: undefined }]], ['B', [{ extensionIdentifier: 'ext1', type: EnvironmentVariableMutatorType.Append, value: 'b', variable: 'B', options: undefined }]], diff --git a/src/vs/workbench/contrib/terminal/test/common/environmentVariableShared.test.ts b/src/vs/workbench/contrib/terminal/test/common/environmentVariableShared.test.ts index d3fb2177556..ba72290399b 100644 --- a/src/vs/workbench/contrib/terminal/test/common/environmentVariableShared.test.ts +++ b/src/vs/workbench/contrib/terminal/test/common/environmentVariableShared.test.ts @@ -6,8 +6,11 @@ import { deepStrictEqual } from 'assert'; import { deserializeEnvironmentVariableCollection, serializeEnvironmentVariableCollection } from 'vs/platform/terminal/common/environmentVariableShared'; import { EnvironmentVariableMutatorType, IEnvironmentVariableMutator } from 'vs/platform/terminal/common/environmentVariable'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('EnvironmentVariable - deserializeEnvironmentVariableCollection', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + test('should construct correctly with 3 arguments', () => { const c = deserializeEnvironmentVariableCollection([ ['A', { value: 'a', type: EnvironmentVariableMutatorType.Replace, variable: 'A' }], @@ -23,6 +26,8 @@ suite('EnvironmentVariable - deserializeEnvironmentVariableCollection', () => { }); suite('EnvironmentVariable - serializeEnvironmentVariableCollection', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + test('should correctly serialize the object', () => { const collection = new Map(); deepStrictEqual(serializeEnvironmentVariableCollection(collection), []); diff --git a/src/vs/workbench/contrib/terminal/test/common/history.test.ts b/src/vs/workbench/contrib/terminal/test/common/history.test.ts index 428ec07e20f..db0777ddd08 100644 --- a/src/vs/workbench/contrib/terminal/test/common/history.test.ts +++ b/src/vs/workbench/contrib/terminal/test/common/history.test.ts @@ -10,6 +10,7 @@ import { join } from 'vs/base/common/path'; import { isWindows, OperatingSystem } from 'vs/base/common/platform'; import { env } from 'vs/base/common/process'; import { URI } from 'vs/base/common/uri'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IFileService } from 'vs/platform/files/common/files'; @@ -40,6 +41,8 @@ const expectedCommands = [ ]; suite('Terminal history', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + suite('TerminalPersistedHistory', () => { let history: ITerminalPersistedHistory; let instantiationService: TestInstantiationService; @@ -48,12 +51,12 @@ suite('Terminal history', () => { setup(() => { configurationService = new TestConfigurationService(getConfig(5)); - storageService = new TestStorageService(); - instantiationService = new TestInstantiationService(); + storageService = store.add(new TestStorageService()); + instantiationService = store.add(new TestInstantiationService()); instantiationService.set(IConfigurationService, configurationService); instantiationService.set(IStorageService, storageService); - history = instantiationService.createInstance(TerminalPersistedHistory, 'test'); + history = store.add(instantiationService.createInstance(TerminalPersistedHistory, 'test')); }); teardown(() => { @@ -116,7 +119,7 @@ suite('Terminal history', () => { history.add('2', 2); history.add('3', 3); strictEqual(Array.from(history.entries).length, 3); - const history2 = instantiationService.createInstance(TerminalPersistedHistory, 'test'); + const history2 = store.add(instantiationService.createInstance(TerminalPersistedHistory, 'test')); strictEqual(Array.from(history2.entries).length, 3); }); }); diff --git a/src/vs/workbench/contrib/terminal/test/common/terminalColorRegistry.test.ts b/src/vs/workbench/contrib/terminal/test/common/terminalColorRegistry.test.ts index 63c258c9b8e..4a9d9a78954 100644 --- a/src/vs/workbench/contrib/terminal/test/common/terminalColorRegistry.test.ts +++ b/src/vs/workbench/contrib/terminal/test/common/terminalColorRegistry.test.ts @@ -10,6 +10,7 @@ import { ansiColorIdentifiers, registerColors } from 'vs/workbench/contrib/termi import { IColorTheme } from 'vs/platform/theme/common/themeService'; import { Color } from 'vs/base/common/color'; import { ColorScheme } from 'vs/platform/theme/common/theme'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; registerColors(); @@ -29,6 +30,7 @@ function getMockTheme(type: ColorScheme): IColorTheme { } suite('Workbench - TerminalColorRegistry', () => { + ensureNoDisposablesAreLeakedInTestSuite(); test('hc colors', function () { const theme = getMockTheme(ColorScheme.HIGH_CONTRAST_DARK); diff --git a/src/vs/workbench/contrib/terminal/test/common/terminalDataBuffering.test.ts b/src/vs/workbench/contrib/terminal/test/common/terminalDataBuffering.test.ts index 01cec8ee267..dd74c586060 100644 --- a/src/vs/workbench/contrib/terminal/test/common/terminalDataBuffering.test.ts +++ b/src/vs/workbench/contrib/terminal/test/common/terminalDataBuffering.test.ts @@ -5,11 +5,14 @@ import * as assert from 'assert'; import { Emitter } from 'vs/base/common/event'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { TerminalDataBufferer } from 'vs/platform/terminal/common/terminalDataBuffering'; const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); suite('Workbench - TerminalDataBufferer', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + let bufferer: TerminalDataBufferer; let counter: { [id: number]: number }; let data: { [id: number]: string }; @@ -17,7 +20,7 @@ suite('Workbench - TerminalDataBufferer', () => { setup(async () => { counter = {}; data = {}; - bufferer = new TerminalDataBufferer((id, e) => { + bufferer = store.add(new TerminalDataBufferer((id, e) => { if (!(id in counter)) { counter[id] = 0; } @@ -26,13 +29,13 @@ suite('Workbench - TerminalDataBufferer', () => { data[id] = ''; } data[id] = e; - }); + })); }); test('start', async () => { const terminalOnData = new Emitter(); - bufferer.startBuffering(1, terminalOnData.event, 0); + store.add(bufferer.startBuffering(1, terminalOnData.event, 0)); terminalOnData.fire('1'); terminalOnData.fire('2'); @@ -55,8 +58,8 @@ suite('Workbench - TerminalDataBufferer', () => { const terminal1OnData = new Emitter(); const terminal2OnData = new Emitter(); - bufferer.startBuffering(1, terminal1OnData.event, 0); - bufferer.startBuffering(2, terminal2OnData.event, 0); + store.add(bufferer.startBuffering(1, terminal1OnData.event, 0)); + store.add(bufferer.startBuffering(2, terminal2OnData.event, 0)); terminal1OnData.fire('1'); terminal2OnData.fire('4'); @@ -100,7 +103,7 @@ suite('Workbench - TerminalDataBufferer', () => { const terminal2OnData = new Emitter(); bufferer.startBuffering(1, terminal1OnData.event, 0); - bufferer.startBuffering(2, terminal2OnData.event, 0); + store.add(bufferer.startBuffering(2, terminal2OnData.event, 0)); terminal1OnData.fire('1'); terminal2OnData.fire('4'); @@ -128,8 +131,8 @@ suite('Workbench - TerminalDataBufferer', () => { const terminal1OnData = new Emitter(); const terminal2OnData = new Emitter(); - bufferer.startBuffering(1, terminal1OnData.event, 0); - bufferer.startBuffering(2, terminal2OnData.event, 0); + store.add(bufferer.startBuffering(1, terminal1OnData.event, 0)); + store.add(bufferer.startBuffering(2, terminal2OnData.event, 0)); terminal1OnData.fire('1'); terminal2OnData.fire('4'); diff --git a/src/vs/workbench/contrib/terminal/test/common/terminalEnvironment.test.ts b/src/vs/workbench/contrib/terminal/test/common/terminalEnvironment.test.ts index 2fcf2ecf33c..6c7ae16402a 100644 --- a/src/vs/workbench/contrib/terminal/test/common/terminalEnvironment.test.ts +++ b/src/vs/workbench/contrib/terminal/test/common/terminalEnvironment.test.ts @@ -9,8 +9,11 @@ import { isWindows, OperatingSystem } from 'vs/base/common/platform'; import { URI as Uri } from 'vs/base/common/uri'; import { addTerminalEnvironmentKeys, createTerminalEnvironment, getCwd, getLangEnvVariable, mergeEnvironments, preparePathForShell, shouldSetLangEnvVariable } from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; import { PosixShellType, WindowsShellType } from 'vs/platform/terminal/common/terminal'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('Workbench - TerminalEnvironment', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + suite('addTerminalEnvironmentKeys', () => { test('should set expected variables', () => { const env: { [key: string]: any } = {}; diff --git a/src/vs/workbench/contrib/terminal/test/node/terminalProfiles.test.ts b/src/vs/workbench/contrib/terminal/test/node/terminalProfiles.test.ts index 5e6f6e31c3f..d86818ff870 100644 --- a/src/vs/workbench/contrib/terminal/test/node/terminalProfiles.test.ts +++ b/src/vs/workbench/contrib/terminal/test/node/terminalProfiles.test.ts @@ -9,6 +9,7 @@ import { ITerminalProfile, ProfileSource } from 'vs/platform/terminal/common/ter import { ITerminalConfiguration, ITerminalProfiles } from 'vs/workbench/contrib/terminal/common/terminal'; import { detectAvailableProfiles, IFsProvider } from 'vs/platform/terminal/node/terminalProfiles'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; /** * Assets that two profiles objects are equal, this will treat explicit undefined and unset @@ -28,6 +29,8 @@ function profilesEqual(actualProfiles: ITerminalProfile[], expectedProfiles: ITe } suite('Workbench - TerminalProfiles', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + suite('detectAvailableProfiles', () => { if (isWindows) { test('should detect Git Bash and provide login args', async () => { diff --git a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/bufferContentTracker.ts b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/bufferContentTracker.ts index 53bc70995a9..14678778467 100644 --- a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/bufferContentTracker.ts +++ b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/bufferContentTracker.ts @@ -3,12 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Disposable } from 'vs/base/common/lifecycle'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITerminalLogService, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; import { IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal'; import type { IMarker, Terminal } from 'xterm'; -export class BufferContentTracker { +export class BufferContentTracker extends Disposable { /** * Marks the last part of the buffer that was cached */ @@ -27,6 +28,7 @@ export class BufferContentTracker { private readonly _xterm: Pick & { raw: Terminal }, @ITerminalLogService private readonly _logService: ITerminalLogService, @IConfigurationService private readonly _configurationService: IConfigurationService) { + super(); } reset(): void { @@ -44,7 +46,7 @@ export class BufferContentTracker { this._removeViewportContent(); this._updateCachedContent(); this._updateViewportContent(); - this._lastCachedMarker = this._xterm.raw.registerMarker(); + this._lastCachedMarker = this._register(this._xterm.raw.registerMarker()); this._logService.debug('Buffer content tracker: set ', this._lines.length, ' lines'); } diff --git a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts index b0596b2d2b0..29783e7170b 100644 --- a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts @@ -7,25 +7,27 @@ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from 'vs/platform/accessibility/common/accessibility'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; -import { TerminalSettingId, terminalTabFocusModeContextKey } from 'vs/platform/terminal/common/terminal'; -import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; -import { AccessibilityHelpAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; +import { ITerminalCommand, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; +import { ICurrentPartialCommand } from 'vs/platform/terminal/common/capabilities/commandDetectionCapability'; +import { AccessibleViewProviderId, accessibleViewCurrentProviderId, accessibleViewIsShown } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { IAccessibleViewService, NavigationType } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; +import { AccessibilityHelpAction, AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; import { ITerminalContribution, ITerminalInstance, ITerminalService, IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal'; import { registerTerminalAction } from 'vs/workbench/contrib/terminal/browser/terminalActions'; import { registerTerminalContribution } from 'vs/workbench/contrib/terminal/browser/terminalExtensions'; import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager'; import { ITerminalProcessManager, TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; +import { BufferContentTracker } from 'vs/workbench/contrib/terminalContrib/accessibility/browser/bufferContentTracker'; import { TerminalAccessibleContentProvider } from 'vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibilityHelp'; -import { AccessibleBufferWidget, NavigationType } from 'vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleBuffer'; import { TextAreaSyncAddon } from 'vs/workbench/contrib/terminalContrib/accessibility/browser/textAreaSyncAddon'; import type { Terminal } from 'xterm'; - +import { Position } from 'vs/editor/common/core/position'; +import { ICommandWithEditorLine, TerminalAccessibleBufferProvider } from 'vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleBufferProvider'; class TextAreaSyncContribution extends DisposableStore implements ITerminalContribution { static readonly ID = 'terminal.textAreaSync'; @@ -48,56 +50,113 @@ class TextAreaSyncContribution extends DisposableStore implements ITerminalContr } registerTerminalContribution(TextAreaSyncContribution.ID, TextAreaSyncContribution); -class AccessibleBufferContribution extends DisposableStore implements ITerminalContribution { - static readonly ID = 'terminal.accessible-buffer'; - private _xterm: IXtermTerminal & { raw: Terminal } | undefined; - static get(instance: ITerminalInstance): AccessibleBufferContribution | null { - return instance.getContribution(AccessibleBufferContribution.ID); - } - private _accessibleBufferWidget: AccessibleBufferWidget | undefined; +export class TerminalAccessibleViewContribution extends Disposable implements ITerminalContribution { + static readonly ID = 'terminal.accessibleBufferProvider'; + static get(instance: ITerminalInstance): TerminalAccessibleViewContribution | null { + return instance.getContribution(TerminalAccessibleViewContribution.ID); + } + private _bufferTracker: BufferContentTracker | undefined; + private _xterm: Pick & { raw: Terminal } | undefined; constructor( private readonly _instance: ITerminalInstance, processManager: ITerminalProcessManager, widgetManager: TerminalWidgetManager, + @IAccessibleViewService private readonly _accessibleViewService: IAccessibleViewService, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IConfigurationService configurationService: IConfigurationService - ) { + @ITerminalService private readonly _terminalService: ITerminalService) { super(); - this.add(_instance.onDidRunText(() => { - const focusAfterRun = configurationService.getValue(TerminalSettingId.FocusAfterRun); - if (focusAfterRun === 'terminal') { - _instance.focus(true); - } else if (focusAfterRun === 'accessible-buffer') { - this.show(); + this._register(AccessibleViewAction.addImplementation(90, 'terminal', () => { + if (this._terminalService.activeInstance !== this._instance) { + return false; } - })); + this.show(); + return true; + }, TerminalContextKeys.focus)); } - layout(xterm: IXtermTerminal & { raw: Terminal }): void { + xtermReady(xterm: IXtermTerminal & { raw: Terminal }): void { + const addon = this._instantiationService.createInstance(TextAreaSyncAddon, this._instance.capabilities); + xterm.raw.loadAddon(addon); + addon.activate(xterm.raw); this._xterm = xterm; } - async show(): Promise { + show(): void { if (!this._xterm) { return; } - if (!this._accessibleBufferWidget) { - this._accessibleBufferWidget = this.add(this._instantiationService.createInstance(AccessibleBufferWidget, this._instance, this._xterm)); + if (!this._bufferTracker) { + this._bufferTracker = this._register(this._instantiationService.createInstance(BufferContentTracker, this._xterm)); } - await this._accessibleBufferWidget.show(); + this._accessibleViewService.show(this._instantiationService.createInstance(TerminalAccessibleBufferProvider, this._instance, this._bufferTracker)); + // wait for the render to happen so that the line count is correct and + // the cursor is at the bottom of the buffer + setTimeout(() => { + const lastPosition = this._accessibleViewService.getLastPosition(); + if (lastPosition) { + this._accessibleViewService.setPosition(lastPosition, true); + } + }, 50); } - - async createCommandQuickPick(): Promise | undefined> { - return this._accessibleBufferWidget?.createQuickPick(); - } - navigateToCommand(type: NavigationType): void { - return this._accessibleBufferWidget?.navigateToCommand(type); + const currentLine = this._accessibleViewService.getPosition()?.lineNumber || this._accessibleViewService.getLastPosition()?.lineNumber; + const commands = this._getCommandsWithEditorLine(); + if (!commands?.length || !currentLine) { + return; + } + + const filteredCommands = type === NavigationType.Previous ? commands.filter(c => c.lineNumber < currentLine).sort((a, b) => b.lineNumber - a.lineNumber) : commands.filter(c => c.lineNumber > currentLine).sort((a, b) => a.lineNumber - b.lineNumber); + if (!filteredCommands.length) { + return; + } + this._accessibleViewService.setPosition(new Position(filteredCommands[0].lineNumber, 1), true); } - hide(): void { - this._accessibleBufferWidget?.hide(); + + private _getCommandsWithEditorLine(): ICommandWithEditorLine[] | undefined { + const capability = this._instance.capabilities.get(TerminalCapability.CommandDetection); + const commands = capability?.commands; + const currentCommand = capability?.currentCommand; + if (!commands?.length) { + return; + } + const result: ICommandWithEditorLine[] = []; + for (const command of commands) { + const lineNumber = this._getEditorLineForCommand(command); + if (!lineNumber) { + continue; + } + result.push({ command, lineNumber }); + } + if (currentCommand) { + const lineNumber = this._getEditorLineForCommand(currentCommand); + if (!!lineNumber) { + result.push({ command: currentCommand, lineNumber }); + } + } + return result; } + + private _getEditorLineForCommand(command: ITerminalCommand | ICurrentPartialCommand): number | undefined { + if (!this._bufferTracker) { + return; + } + let line: number | undefined; + if ('marker' in command) { + line = command.marker?.line; + } else if ('commandStartMarker' in command) { + line = command.commandStartMarker?.line; + } + if (line === undefined || line < 0) { + return; + } + line = this._bufferTracker.bufferToEditorLineMapping.get(line); + if (line === undefined) { + return; + } + return line + 1; + } + } -registerTerminalContribution(AccessibleBufferContribution.ID, AccessibleBufferContribution); +registerTerminalContribution(TerminalAccessibleViewContribution.ID, TerminalAccessibleViewContribution); export class TerminalAccessibilityHelpContribution extends Disposable { static ID: 'terminalAccessibilityHelpContribution'; @@ -115,72 +174,51 @@ export class TerminalAccessibilityHelpContribution extends Disposable { return; } accessibleViewService.show(instantiationService.createInstance(TerminalAccessibleContentProvider, instance, terminal)); - }, ContextKeyExpr.or(TerminalContextKeys.focus, TerminalContextKeys.accessibleBufferFocus))); + }, ContextKeyExpr.or(TerminalContextKeys.focus, ContextKeyExpr.and(accessibleViewIsShown, ContextKeyExpr.equals(accessibleViewCurrentProviderId.key, AccessibleViewProviderId.Terminal))))); } } registerTerminalContribution(TerminalAccessibilityHelpContribution.ID, TerminalAccessibilityHelpContribution); -registerTerminalAction({ - id: TerminalCommandId.FocusAccessibleBuffer, - title: { value: localize('workbench.action.terminal.focusAccessibleBuffer', 'Focus Accessible Buffer'), original: 'Focus Accessible Buffer' }, - precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), - keybinding: [ - { - primary: KeyMod.Alt | KeyCode.F2, - secondary: [KeyMod.CtrlCmd | KeyCode.UpArrow], - linux: { - primary: KeyMod.Alt | KeyCode.F2 | KeyMod.Shift, - secondary: [KeyMod.CtrlCmd | KeyCode.UpArrow] - }, - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(CONTEXT_ACCESSIBILITY_MODE_ENABLED, TerminalContextKeys.focus, ContextKeyExpr.or(terminalTabFocusModeContextKey, TerminalContextKeys.accessibleBufferFocus.negate())) - } - ], - run: async (c) => { - const instance = await c.service.getActiveOrCreateInstance(); - await c.service.revealActiveTerminal(); - if (!instance) { - return; - } - await AccessibleBufferContribution.get(instance)?.show(); - } -}); -registerTerminalAction({ - id: TerminalCommandId.NavigateAccessibleBuffer, - title: { value: localize('workbench.action.terminal.navigateAccessibleBuffer', 'Navigate Accessible Buffer'), original: 'Navigate Accessible Buffer' }, - precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), - keybinding: [ - { - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyG, - weight: KeybindingWeight.WorkbenchContrib + 2, - when: TerminalContextKeys.accessibleBufferFocus - } - ], - run: async (c) => { - const instance = await c.service.getActiveOrCreateInstance(); - await c.service.revealActiveTerminal(); - if (!instance) { +class FocusAccessibleBufferAction extends Action2 { + constructor() { + super({ + id: TerminalCommandId.FocusAccessibleBuffer, + title: { value: localize('workbench.action.terminal.focusAccessibleBuffer', 'Focus Accessible Buffer'), original: 'Focus Accessible Buffer' }, + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), + keybinding: [ + { + primary: KeyMod.Alt | KeyCode.F2, + secondary: [KeyMod.CtrlCmd | KeyCode.UpArrow], + linux: { + primary: KeyMod.Alt | KeyCode.F2 | KeyMod.Shift, + secondary: [KeyMod.CtrlCmd | KeyCode.UpArrow] + }, + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(CONTEXT_ACCESSIBILITY_MODE_ENABLED, TerminalContextKeys.focus) + } + ] + }); + } + override async run(accessor: ServicesAccessor, ...args: any[]): Promise { + const terminalService = accessor.get(ITerminalService); + const terminal = await terminalService.getActiveOrCreateInstance(); + if (!terminal?.xterm) { return; } - const quickPick = await AccessibleBufferContribution.get(instance)?.createCommandQuickPick(); - quickPick?.show(); + TerminalAccessibleViewContribution.get(terminal)?.show(); } -}); +} +registerAction2(FocusAccessibleBufferAction); registerTerminalAction({ id: TerminalCommandId.AccessibleBufferGoToNextCommand, title: { value: localize('workbench.action.terminal.accessibleBufferGoToNextCommand', 'Accessible Buffer Go to Next Command'), original: 'Accessible Buffer Go to Next Command' }, - precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated, TerminalContextKeys.accessibleBufferFocus), + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated, ContextKeyExpr.and(accessibleViewIsShown, ContextKeyExpr.equals(accessibleViewCurrentProviderId.key, AccessibleViewProviderId.Terminal))), keybinding: [ - { - primary: KeyMod.CtrlCmd | KeyCode.DownArrow, - when: ContextKeyExpr.and(TerminalContextKeys.accessibleBufferFocus, CONTEXT_ACCESSIBILITY_MODE_ENABLED.negate()), - weight: KeybindingWeight.WorkbenchContrib + 2 - }, { primary: KeyMod.Alt | KeyCode.DownArrow, - when: ContextKeyExpr.and(TerminalContextKeys.accessibleBufferFocus, CONTEXT_ACCESSIBILITY_MODE_ENABLED), + when: ContextKeyExpr.and(ContextKeyExpr.and(accessibleViewIsShown, ContextKeyExpr.equals(accessibleViewCurrentProviderId.key, AccessibleViewProviderId.Terminal))), weight: KeybindingWeight.WorkbenchContrib + 2 } ], @@ -190,7 +228,7 @@ registerTerminalAction({ if (!instance) { return; } - await AccessibleBufferContribution.get(instance)?.navigateToCommand(NavigationType.Next); + await TerminalAccessibleViewContribution.get(instance)?.navigateToCommand(NavigationType.Next); } }); @@ -198,16 +236,11 @@ registerTerminalAction({ registerTerminalAction({ id: TerminalCommandId.AccessibleBufferGoToPreviousCommand, title: { value: localize('workbench.action.terminal.accessibleBufferGoToPreviousCommand', 'Accessible Buffer Go to Previous Command'), original: 'Accessible Buffer Go to Previous Command' }, - precondition: ContextKeyExpr.and(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.accessibleBufferFocus), + precondition: ContextKeyExpr.and(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), ContextKeyExpr.and(accessibleViewIsShown, ContextKeyExpr.equals(accessibleViewCurrentProviderId.key, AccessibleViewProviderId.Terminal))), keybinding: [ - { - primary: KeyMod.CtrlCmd | KeyCode.UpArrow, - when: ContextKeyExpr.and(TerminalContextKeys.accessibleBufferFocus, CONTEXT_ACCESSIBILITY_MODE_ENABLED.negate()), - weight: KeybindingWeight.WorkbenchContrib + 2 - }, { primary: KeyMod.Alt | KeyCode.UpArrow, - when: ContextKeyExpr.and(TerminalContextKeys.accessibleBufferFocus, CONTEXT_ACCESSIBILITY_MODE_ENABLED), + when: ContextKeyExpr.and(ContextKeyExpr.and(accessibleViewIsShown, ContextKeyExpr.equals(accessibleViewCurrentProviderId.key, AccessibleViewProviderId.Terminal))), weight: KeybindingWeight.WorkbenchContrib + 2 } ], @@ -217,6 +250,6 @@ registerTerminalAction({ if (!instance) { return; } - await AccessibleBufferContribution.get(instance)?.navigateToCommand(NavigationType.Previous); + await TerminalAccessibleViewContribution.get(instance)?.navigateToCommand(NavigationType.Previous); } }); diff --git a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibilityHelp.ts b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibilityHelp.ts index 340427fd528..ec61041471b 100644 --- a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibilityHelp.ts @@ -8,15 +8,15 @@ import { format } from 'vs/base/common/strings'; import { localize } from 'vs/nls'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ShellIntegrationStatus, TerminalSettingId, WindowsShellType } from 'vs/platform/terminal/common/terminal'; -import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { AccessibilityVerbositySettingId, AccessibleViewProviderId, accessibleViewCurrentProviderId, accessibleViewIsShown } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { AccessibleViewType, IAccessibleContentProvider, IAccessibleViewOptions } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; +import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; import { ITerminalInstance, IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; -import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; import type { Terminal } from 'xterm'; export const enum ClassName { @@ -29,7 +29,8 @@ export class TerminalAccessibleContentProvider extends Disposable implements IAc private readonly _hasShellIntegration: boolean = false; onClose() { - if (this._contextKeyService.getContextKeyValue(TerminalContextKeys.accessibleBufferFocus.key) === true) { + const expr = ContextKeyExpr.and(accessibleViewIsShown, ContextKeyExpr.equals(accessibleViewCurrentProviderId.key, AccessibleViewProviderId.Terminal)); + if (expr?.evaluate(this._contextKeyService.getContext(null))) { this._commandService.executeCommand(TerminalCommandId.FocusAccessibleBuffer); } else { this._instance.focus(); @@ -84,7 +85,7 @@ export class TerminalAccessibleContentProvider extends Disposable implements IAc shellIntegrationCommandList.push(localize('shellIntegration', "The terminal has a feature called shell integration that offers an enhanced experience and provides useful commands for screen readers such as:")); shellIntegrationCommandList.push('- ' + this._descriptionForCommand(TerminalCommandId.AccessibleBufferGoToNextCommand, localize('goToNextCommand', 'Go to Next Command ({0})'), localize('goToNextCommandNoKb', 'Go to Next Command is currently not triggerable by a keybinding.'))); shellIntegrationCommandList.push('- ' + this._descriptionForCommand(TerminalCommandId.AccessibleBufferGoToPreviousCommand, localize('goToPreviousCommand', 'Go to Previous Command ({0})'), localize('goToPreviousCommandNoKb', 'Go to Previous Command is currently not triggerable by a keybinding.'))); - shellIntegrationCommandList.push('- ' + this._descriptionForCommand(TerminalCommandId.NavigateAccessibleBuffer, localize('navigateAccessibleBuffer', 'Navigate Accessible Buffer ({0})'), localize('navigateAccessibleBufferNoKb', 'Navigate Accessible Buffer is currently not triggerable by a keybinding.'))); + shellIntegrationCommandList.push('- ' + this._descriptionForCommand(AccessibilityCommandId.GoToSymbol, localize('goToSymbol', 'Go to Symbol ({0})'), localize('goToSymbolNoKb', 'Go to symbol is currently not triggerable by a keybinding.'))); shellIntegrationCommandList.push('- ' + this._descriptionForCommand(TerminalCommandId.RunRecentCommand, localize('runRecentCommand', 'Run Recent Command ({0})'), localize('runRecentCommandNoKb', 'Run Recent Command is currently not triggerable by a keybinding.'))); shellIntegrationCommandList.push('- ' + this._descriptionForCommand(TerminalCommandId.GoToRecentDirectory, localize('goToRecentDirectory', 'Go to Recent Directory ({0})'), localize('goToRecentDirectoryNoKb', 'Go to Recent Directory is currently not triggerable by a keybinding.'))); content.push(shellIntegrationCommandList.join('\n')); diff --git a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleBuffer.ts b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleBuffer.ts deleted file mode 100644 index 8e6c44210bb..00000000000 --- a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleBuffer.ts +++ /dev/null @@ -1,268 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Event } from 'vs/base/common/event'; -import { IEditorViewState } from 'vs/editor/common/editorCommon'; -import { ITextModel } from 'vs/editor/common/model'; -import { IModelService } from 'vs/editor/common/services/model'; -import { localize } from 'vs/nls'; -import { AudioCue, IAudioCueService } from 'vs/platform/audioCues/browser/audioCueService'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IQuickInputService, IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; -import { ITerminalCommand, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; -import { ICurrentPartialCommand } from 'vs/platform/terminal/common/capabilities/commandDetectionCapability'; -import { ITerminalLogService } from 'vs/platform/terminal/common/terminal'; -import { ITerminalInstance, ITerminalService, IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal'; -import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; -import { BufferContentTracker } from 'vs/workbench/contrib/terminalContrib/accessibility/browser/bufferContentTracker'; -import { TerminalAccessibleWidget } from 'vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleWidget'; -import type { Terminal } from 'xterm'; - -export const enum NavigationType { - Next = 'next', - Previous = 'previous' -} - -interface IAccessibleBufferQuickPickItem extends IQuickPickItem { - lineNumber: number; - exitCode?: number; -} - -export const enum ClassName { - AccessibleBuffer = 'accessible-buffer', - Active = 'active' -} - -export class AccessibleBufferWidget extends TerminalAccessibleWidget { - private _isUpdating: boolean = false; - private _pendingUpdates = 0; - - private _bufferTracker: BufferContentTracker; - - private _cursorPosition: { lineNumber: number; column: number } | undefined; - - constructor( - _instance: Pick, - _xterm: Pick & { raw: Terminal }, - @IInstantiationService _instantiationService: IInstantiationService, - @IModelService _modelService: IModelService, - @IConfigurationService _configurationService: IConfigurationService, - @IQuickInputService private readonly _quickInputService: IQuickInputService, - @IAudioCueService private readonly _audioCueService: IAudioCueService, - @IContextKeyService _contextKeyService: IContextKeyService, - @ITerminalLogService private readonly _logService: ITerminalLogService, - @ITerminalService _terminalService: ITerminalService - ) { - super(ClassName.AccessibleBuffer, _instance, _xterm, TerminalContextKeys.accessibleBufferFocus, TerminalContextKeys.accessibleBufferOnLastLine, _instantiationService, _modelService, _configurationService, _contextKeyService, _terminalService); - this._bufferTracker = _instantiationService.createInstance(BufferContentTracker, _xterm); - this.element.ariaRoleDescription = localize('terminal.integrated.accessibleBuffer', 'Terminal buffer'); - _instance.onDidRequestFocus(() => this.hide(true)); - this.updateEditor(); - // xterm's initial layout call has already happened - this.layout(); - } - - navigateToCommand(type: NavigationType): void { - const currentLine = this.editorWidget.getPosition()?.lineNumber || this._getDefaultCursorPosition()?.lineNumber; - const commands = this._getCommandsWithEditorLine(); - if (!commands?.length || !currentLine) { - return; - } - - const filteredCommands = type === NavigationType.Previous ? commands.filter(c => c.lineNumber < currentLine).sort((a, b) => b.lineNumber - a.lineNumber) : commands.filter(c => c.lineNumber > currentLine).sort((a, b) => a.lineNumber - b.lineNumber); - if (!filteredCommands.length) { - return; - } - this._cursorPosition = { lineNumber: filteredCommands[0].lineNumber, column: 1 }; - this._resetPosition(); - } - - private _getEditorLineForCommand(command: ITerminalCommand | ICurrentPartialCommand): number | undefined { - let line: number | undefined; - if ('marker' in command) { - line = command.marker?.line; - } else if ('commandStartMarker' in command) { - line = command.commandStartMarker?.line; - } - if (line === undefined || line < 0) { - return; - } - line = this._bufferTracker.bufferToEditorLineMapping.get(line); - if (line === undefined) { - return; - } - return line + 1; - } - - private _getCommandsWithEditorLine(): ICommandWithEditorLine[] | undefined { - const capability = this._instance.capabilities.get(TerminalCapability.CommandDetection); - const commands = capability?.commands; - const currentCommand = capability?.currentCommand; - if (!commands?.length) { - return; - } - const result: ICommandWithEditorLine[] = []; - for (const command of commands) { - const lineNumber = this._getEditorLineForCommand(command); - if (!lineNumber) { - continue; - } - result.push({ command, lineNumber }); - } - if (currentCommand) { - const lineNumber = this._getEditorLineForCommand(currentCommand); - if (!!lineNumber) { - result.push({ command: currentCommand, lineNumber }); - } - } - return result; - } - - async createQuickPick(): Promise | undefined> { - this._cursorPosition = this.editorWidget.getPosition() ?? undefined; - const commands = this._getCommandsWithEditorLine(); - if (!commands) { - return; - } - const quickPickItems: IAccessibleBufferQuickPickItem[] = []; - for (const { command, lineNumber } of commands) { - const line = this._getEditorLineForCommand(command); - if (!line) { - continue; - } - quickPickItems.push( - { - label: localize('terminal.integrated.symbolQuickPick.labelNoExitCode', '{0}', command.command), - lineNumber, - exitCode: 'exitCode' in command ? command.exitCode : undefined - }); - } - const quickPick = this._quickInputService.createQuickPick(); - quickPick.canSelectMany = false; - quickPick.onDidChangeActive(() => { - const activeItem = quickPick.activeItems[0]; - if (!activeItem) { - return; - } - if (activeItem.exitCode) { - this._audioCueService.playAudioCue(AudioCue.error, { allowManyInParallel: true, source: 'accessibleBufferWidget' }); - } - this.editorWidget.revealLine(activeItem.lineNumber, 0); - }); - quickPick.onDidHide(() => { - this._resetPosition(); - quickPick.dispose(); - }); - quickPick.onDidAccept(() => { - const item = quickPick.activeItems[0]; - const model = this.editorWidget.getModel(); - if (!model) { - return; - } - if (!item && this._cursorPosition) { - this._resetPosition(); - } else { - this._cursorPosition = { lineNumber: item.lineNumber, column: 1 }; - } - quickPick.dispose(); - this.editorWidget.focus(); - return; - }); - quickPick.items = quickPickItems.reverse(); - return quickPick; - } - - private _resetPosition(): void { - this._cursorPosition = this._cursorPosition ?? this._getDefaultCursorPosition(); - if (!this._cursorPosition) { - return; - } - this.editorWidget.setPosition(this._cursorPosition); - this.editorWidget.setScrollPosition({ scrollTop: this.editorWidget.getTopForLineNumber(this._cursorPosition.lineNumber) }); - } - - override layout(): void { - if (this._bufferTracker) { - this._bufferTracker.reset(); - } - super.layout(); - } - - async updateEditor(dataChanged?: boolean): Promise { - if (this._isUpdating) { - this._pendingUpdates++; - return; - } - this._isUpdating = true; - const model = await this._updateModel(dataChanged); - if (!model) { - return; - } - this._isUpdating = false; - if (this._pendingUpdates) { - this._logService.debug('TerminalAccessibleBuffer._updateEditor: pending updates', this._pendingUpdates); - this._pendingUpdates--; - await this.updateEditor(dataChanged); - } - } - - override registerListeners(): void { - super.registerListeners(); - this._xterm.raw.onWriteParsed(async () => { - if (this._xterm.raw.buffer.active.baseY === 0) { - await this.updateEditor(true); - } - }); - const onRequestUpdateEditor = Event.latch(this._xterm.raw.onScroll); - this._listeners.push(onRequestUpdateEditor(async () => await this.updateEditor(true))); - } - - private _getDefaultCursorPosition(): { lineNumber: number; column: number } | undefined { - const modelLineCount = this.editorWidget.getModel()?.getLineCount(); - return modelLineCount ? { lineNumber: modelLineCount, column: 1 } : undefined; - } - - private async _updateModel(dataChanged?: boolean): Promise { - const linesBefore = this._bufferTracker.lines.length; - this._bufferTracker.update(); - const linesAfter = this._bufferTracker.lines.length; - const modelChanged = linesBefore !== linesAfter; - - // Save the view state before the update if it was set by the user - let savedViewState: IEditorViewState | undefined; - if (dataChanged) { - savedViewState = this.editorWidget.saveViewState() ?? undefined; - } - - let model = this.editorWidget.getModel(); - const text = this._bufferTracker.lines.join('\n'); - if (model) { - model.setValue(text); - } else { - model = await this.getTextModel(this._instance.resource.with({ fragment: `${ClassName.AccessibleBuffer}-${text}` })); - } - this.editorWidget.setModel(model); - - // If the model changed due to new data, restore the view state - // If the model changed due to a refresh or the cursor is a the top, set to the bottom of the buffer - // Otherwise, don't change the position - const positionTopOfBuffer = this.editorWidget.getPosition()?.lineNumber === 1 && this.editorWidget.getPosition()?.column === 1; - if (savedViewState) { - this.editorWidget.restoreViewState(savedViewState); - } else if (modelChanged || positionTopOfBuffer) { - const defaultPosition = this._getDefaultCursorPosition(); - if (defaultPosition) { - this.editorWidget.setPosition(defaultPosition); - this.editorWidget.setScrollPosition({ scrollTop: this.editorWidget.getTopForLineNumber(defaultPosition.lineNumber) }); - } - } - return model!; - } -} - -interface ICommandWithEditorLine { command: ITerminalCommand | ICurrentPartialCommand; lineNumber: number } - diff --git a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleBufferProvider.ts b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleBufferProvider.ts new file mode 100644 index 00000000000..c4ba54d12a6 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleBufferProvider.ts @@ -0,0 +1,123 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { IModelService } from 'vs/editor/common/services/model'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { TerminalCapability, ITerminalCommand } from 'vs/platform/terminal/common/capabilities/capabilities'; +import { ICurrentPartialCommand } from 'vs/platform/terminal/common/capabilities/commandDetectionCapability'; +import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { AccessibleViewType, IAccessibleContentProvider, IAccessibleViewOptions, IAccessibleViewService, IAccessibleViewSymbol } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; +import { IXtermTerminal, ITerminalInstance, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { BufferContentTracker } from 'vs/workbench/contrib/terminalContrib/accessibility/browser/bufferContentTracker'; +import type { Terminal } from 'xterm'; +import { Event } from 'vs/base/common/event'; + +export class TerminalAccessibleBufferProvider extends DisposableStore implements IAccessibleContentProvider { + options: IAccessibleViewOptions = { type: AccessibleViewType.View, language: 'terminal' }; + verbositySettingKey = AccessibilityVerbositySettingId.Terminal; + private _xterm: IXtermTerminal & { raw: Terminal } | undefined; + constructor( + private readonly _instance: Pick, + private _bufferTracker: BufferContentTracker, + @IModelService _modelService: IModelService, + @IConfigurationService _configurationService: IConfigurationService, + @IContextKeyService _contextKeyService: IContextKeyService, + @ITerminalService _terminalService: ITerminalService, + @IConfigurationService configurationService: IConfigurationService, + @IAccessibleViewService private readonly _accessibleViewService: IAccessibleViewService + ) { + super(); + this.add(_instance.onDidRunText(() => { + const focusAfterRun = configurationService.getValue(TerminalSettingId.FocusAfterRun); + if (focusAfterRun === 'terminal') { + _instance.focus(true); + } else if (focusAfterRun === 'accessible-buffer') { + _accessibleViewService.show(this); + } + })); + this.registerListeners(); + } + + onClose() { + this._instance.focus(); + } + registerListeners(): void { + if (!this._xterm) { + return; + } + this._xterm.raw.onWriteParsed(async () => { + if (this._xterm!.raw.buffer.active.baseY === 0) { + this._bufferTracker.update(); + this._accessibleViewService.show(this); + } + }); + const onRequestUpdateEditor = Event.latch(this._xterm.raw.onScroll); + this.add(onRequestUpdateEditor(() => this._accessibleViewService.show(this))); + } + + provideContent(): string { + this._bufferTracker.update(); + return this._bufferTracker.lines.join('\n'); + } + + getSymbols(): IAccessibleViewSymbol[] { + const commands = this._getCommandsWithEditorLine() ?? []; + const symbols: IAccessibleViewSymbol[] = []; + for (const command of commands) { + const label = command.command.command; + if (label) { + symbols.push({ + label, + lineNumber: command.lineNumber + }); + } + } + return symbols; + } + + private _getCommandsWithEditorLine(): ICommandWithEditorLine[] | undefined { + const capability = this._instance.capabilities.get(TerminalCapability.CommandDetection); + const commands = capability?.commands; + const currentCommand = capability?.currentCommand; + if (!commands?.length) { + return; + } + const result: ICommandWithEditorLine[] = []; + for (const command of commands) { + const lineNumber = this._getEditorLineForCommand(command); + if (lineNumber === undefined) { + continue; + } + result.push({ command, lineNumber }); + } + if (currentCommand) { + const lineNumber = this._getEditorLineForCommand(currentCommand); + if (lineNumber !== undefined) { + result.push({ command: currentCommand, lineNumber }); + } + } + return result; + } + private _getEditorLineForCommand(command: ITerminalCommand | ICurrentPartialCommand): number | undefined { + let line: number | undefined; + if ('marker' in command) { + line = command.marker?.line; + } else if ('commandStartMarker' in command) { + line = command.commandStartMarker?.line; + } + if (line === undefined || line < 0) { + return; + } + line = this._bufferTracker.bufferToEditorLineMapping.get(line); + if (line === undefined) { + return; + } + return line + 1; + } +} +export interface ICommandWithEditorLine { command: ITerminalCommand | ICurrentPartialCommand; lineNumber: number } diff --git a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleWidget.ts b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleWidget.ts deleted file mode 100644 index 448cd3da0e4..00000000000 --- a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleWidget.ts +++ /dev/null @@ -1,177 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { KeyCode } from 'vs/base/common/keyCodes'; -import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; -import { URI } from 'vs/base/common/uri'; -import * as dom from 'vs/base/browser/dom'; -import { Event } from 'vs/base/common/event'; -import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; -import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; -import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget'; -import { ITextModel } from 'vs/editor/common/model'; -import { IModelService } from 'vs/editor/common/services/model'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; -import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; -import { ITerminalInstance, ITerminalService, IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal'; -import type { Terminal } from 'xterm'; -import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { CodeActionController } from 'vs/editor/contrib/codeAction/browser/codeActionController'; -import { localize } from 'vs/nls'; - -const enum ClassName { - Active = 'active', - Hide = 'hide', - Widget = 'terminal-accessible-widget' -} - -export abstract class TerminalAccessibleWidget extends DisposableStore { - - private _element: HTMLElement; - get element(): HTMLElement { return this._element; } - private _editorWidget: CodeEditorWidget; - protected get editorWidget(): CodeEditorWidget { return this._editorWidget; } - private _editorContainer: HTMLElement; - private _xtermElement: HTMLElement; - - protected _listeners: IDisposable[] = []; - - private readonly _focusedContextKey: IContextKey; - private readonly _focusedLastLineContextKey: IContextKey; - private readonly _focusTracker?: dom.IFocusTracker; - - constructor( - private readonly _className: string, - protected readonly _instance: Pick, - protected readonly _xterm: Pick & { raw: Terminal }, - rawFocusContextKey: RawContextKey, - rawFocusLastLineContextKey: RawContextKey, - @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IModelService private readonly _modelService: IModelService, - @IConfigurationService private readonly _configurationService: IConfigurationService, - @IContextKeyService protected readonly _contextKeyService: IContextKeyService, - @ITerminalService private readonly _terminalService: ITerminalService - ) { - super(); - this._xtermElement = _xterm.raw.element!; - this._element = document.createElement('div'); - this._element.setAttribute('role', 'document'); - this._element.classList.add(_className); - this._element.classList.add(ClassName.Widget); - this._editorContainer = document.createElement('div'); - const codeEditorWidgetOptions: ICodeEditorWidgetOptions = { - contributions: EditorExtensionsRegistry.getEditorContributions().filter(c => c.id !== CodeActionController.ID) - }; - const font = _xterm.getFont(); - const editorOptions: IEditorConstructionOptions = { - ...getSimpleEditorOptions(this._configurationService), - lineDecorationsWidth: 6, - dragAndDrop: true, - cursorWidth: 1, - fontSize: font.fontSize, - lineHeight: font.charHeight ? font.charHeight * font.lineHeight : 1, - letterSpacing: font.letterSpacing, - fontFamily: font.fontFamily, - wrappingStrategy: 'advanced', - wrappingIndent: 'none', - padding: { top: 2, bottom: 2 }, - quickSuggestions: false, - renderWhitespace: 'none', - dropIntoEditor: { enabled: true }, - readOnly: true, - ariaLabel: localize('terminalAccessibleBuffer', "Terminal Buffer") - }; - this._editorWidget = this.add(this._instantiationService.createInstance(CodeEditorWidget, this._editorContainer, editorOptions, codeEditorWidgetOptions)); - this._element.replaceChildren(this._editorContainer); - this._xtermElement.insertAdjacentElement('beforebegin', this._element); - - this._focusTracker = this.add(dom.trackFocus(this._editorContainer)); - this._focusedContextKey = rawFocusContextKey.bindTo(this._contextKeyService); - this._focusedLastLineContextKey = rawFocusLastLineContextKey.bindTo(this._contextKeyService); - this.add(this._focusTracker.onDidFocus(() => { - this._focusedContextKey?.set(true); - this._focusedLastLineContextKey?.set(this._editorWidget.getSelection()?.positionLineNumber === this._editorWidget.getModel()?.getLineCount()); - })); - this.add(this._focusTracker.onDidBlur(() => { - this._focusedContextKey?.reset(); - this._focusedLastLineContextKey?.reset(); - })); - this._editorWidget.onDidChangeCursorPosition(() => { - console.log(this._editorWidget.getSelection()?.positionLineNumber === this._editorWidget.getModel()?.getLineCount()); - this._focusedLastLineContextKey?.set(this._editorWidget.getSelection()?.positionLineNumber === this._editorWidget.getModel()?.getLineCount()); - }); - - this.add(Event.runAndSubscribe(this._xterm.raw.onResize, () => this.layout())); - this.add(this._configurationService.onDidChangeConfiguration(e => { - if (e.affectedKeys.has(TerminalSettingId.FontFamily) || e.affectedKeys.has(TerminalSettingId.FontSize) || e.affectedKeys.has(TerminalSettingId.LineHeight) || e.affectedKeys.has(TerminalSettingId.LetterSpacing)) { - const font = this._xterm.getFont(); - this._editorWidget.updateOptions({ fontFamily: font.fontFamily, fontSize: font.fontSize, lineHeight: font.charHeight ? font.charHeight * font.lineHeight : 1, letterSpacing: font.letterSpacing }); - } - })); - this.add(this._editorWidget.onKeyDown((e) => { - switch (e.keyCode) { - case KeyCode.Escape: - // On escape, hide the accessible buffer and force focus onto the terminal - this.hide(true); - break; - } - })); - this.add(this._editorWidget.onDidFocusEditorText(async () => { - this._terminalService.setActiveInstance(this._instance as ITerminalInstance); - this._xtermElement.classList.add(ClassName.Hide); - })); - this.add(this._editorWidget.onDidBlurEditorText(async () => this.hide())); - } - - registerListeners(): void { - this._listeners.push(this._instance.onDidRequestFocus(() => this.editorWidget.focus())); - } - - layout(): void { - this._editorWidget.layout({ width: this._xtermElement.clientWidth, height: this._xtermElement.clientHeight }); - } - - abstract updateEditor(): Promise; - - async show(): Promise { - this.registerListeners(); - await this.updateEditor(); - this.element.tabIndex = -1; - this.layout(); - this.element.classList.add(ClassName.Active); - this._xtermElement.classList.add(ClassName.Hide); - this.editorWidget.focus(); - } - - override dispose(): void { - this._disposeListeners(); - super.dispose(); - } - - private _disposeListeners(): void { - for (const listener of this._listeners) { - listener.dispose(); - } - } - - hide(focusXterm?: boolean): void { - this._disposeListeners(); - this.element.classList.remove(ClassName.Active); - this._xtermElement.classList.remove(ClassName.Hide); - if (focusXterm) { - this._xterm.raw.focus(); - } - } - - async getTextModel(resource: URI): Promise { - const existing = this._modelService.getModel(resource); - if (existing && !existing.isDisposed()) { - return existing; - } - return this._modelService.createModel(`${this._className}-${resource.fragment}`, null, resource, false); - } -} diff --git a/src/vs/workbench/contrib/terminalContrib/accessibility/test/browser/bufferContentTracker.test.ts b/src/vs/workbench/contrib/terminalContrib/accessibility/test/browser/bufferContentTracker.test.ts index 240c15c4c21..5f2a3678c61 100644 --- a/src/vs/workbench/contrib/terminalContrib/accessibility/test/browser/bufferContentTracker.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/accessibility/test/browser/bufferContentTracker.test.ts @@ -6,6 +6,7 @@ import * as assert from 'assert'; import { importAMDNodeModule } from 'vs/amdX'; import { isWindows } from 'vs/base/common/platform'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -41,6 +42,8 @@ const defaultTerminalConfig: Partial = { }; suite('Buffer Content Tracker', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + let instantiationService: TestInstantiationService; let configurationService: TestConfigurationService; let themeService: TestThemeService; @@ -50,33 +53,31 @@ suite('Buffer Content Tracker', () => { let bufferTracker: BufferContentTracker; const prompt = 'vscode-git:(prompt/more-tests)'; const promptPlusData = 'vscode-git:(prompt/more-tests) ' + 'some data'; + setup(async () => { configurationService = new TestConfigurationService({ terminal: { integrated: defaultTerminalConfig } }); - instantiationService = new TestInstantiationService(); + instantiationService = store.add(new TestInstantiationService()); themeService = new TestThemeService(); instantiationService.stub(IConfigurationService, configurationService); instantiationService.stub(IThemeService, themeService); instantiationService.stub(ITerminalLogService, new NullLogService()); - instantiationService.stub(ILoggerService, new TestLoggerService()); - instantiationService.stub(IContextMenuService, instantiationService.createInstance(ContextMenuService)); - instantiationService.stub(ILifecycleService, new TestLifecycleService()); - instantiationService.stub(IContextKeyService, new MockContextKeyService()); - configHelper = instantiationService.createInstance(TerminalConfigHelper); - capabilities = new TerminalCapabilityStore(); + instantiationService.stub(ILoggerService, store.add(new TestLoggerService())); + instantiationService.stub(IContextMenuService, store.add(instantiationService.createInstance(ContextMenuService))); + instantiationService.stub(ILifecycleService, store.add(new TestLifecycleService())); + instantiationService.stub(IContextKeyService, store.add(new MockContextKeyService())); + configHelper = store.add(instantiationService.createInstance(TerminalConfigHelper)); + capabilities = store.add(new TerminalCapabilityStore()); if (!isWindows) { capabilities.add(TerminalCapability.NaiveCwdDetection, null!); } const TerminalCtor = (await importAMDNodeModule('xterm', 'lib/xterm.js')).Terminal; - xterm = instantiationService.createInstance(XtermTerminal, TerminalCtor, configHelper, 80, 30, { getBackgroundColor: () => undefined }, capabilities, '', new MockContextKeyService().createKey('', true)!, true); + xterm = store.add(instantiationService.createInstance(XtermTerminal, TerminalCtor, configHelper, 80, 30, { getBackgroundColor: () => undefined }, capabilities, '', new MockContextKeyService().createKey('', true)!, true)); const container = document.createElement('div'); xterm.raw.open(container); configurationService = new TestConfigurationService({ terminal: { integrated: { tabs: { separator: ' - ', title: '${cwd}', description: '${cwd}' } } } }); - configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!); - bufferTracker = instantiationService.createInstance(BufferContentTracker, xterm); - }); - teardown(() => { - instantiationService.dispose(); + bufferTracker = store.add(instantiationService.createInstance(BufferContentTracker, xterm)); }); + test('should not clear the prompt line', async () => { assert.strictEqual(bufferTracker.lines.length, 0); await writeP(xterm.raw, prompt); diff --git a/src/vs/workbench/contrib/terminalContrib/links/browser/terminal.links.contribution.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/terminal.links.contribution.ts index 5d144353e9f..51bd07850d8 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/terminal.links.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/browser/terminal.links.contribution.ts @@ -10,6 +10,7 @@ import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { AccessibleViewProviderId, accessibleViewCurrentProviderId, accessibleViewIsShown } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { IDetachedTerminalInstance, ITerminalContribution, ITerminalInstance, IXtermTerminal, isDetachedTerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import { registerActiveInstanceAction } from 'vs/workbench/contrib/terminal/browser/terminalActions'; import { registerTerminalContribution } from 'vs/workbench/contrib/terminal/browser/terminalExtensions'; @@ -113,11 +114,16 @@ registerActiveInstanceAction({ f1: true, category, precondition: TerminalContextKeys.terminalHasBeenCreated, - keybinding: { + keybinding: [{ primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyO, weight: KeybindingWeight.WorkbenchContrib + 1, - when: ContextKeyExpr.or(TerminalContextKeys.focus, TerminalContextKeys.accessibleBufferFocus) + when: TerminalContextKeys.focus + }, { + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyG, + weight: KeybindingWeight.WorkbenchContrib + 1, + when: ContextKeyExpr.and(accessibleViewIsShown, ContextKeyExpr.equals(accessibleViewCurrentProviderId.key, AccessibleViewProviderId.Terminal)) }, + ], run: (activeInstance) => TerminalLinkContribution.get(activeInstance)?.showLinkQuickpick() }); registerActiveInstanceAction({ diff --git a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager.ts index 93105d6e3c0..bdf2d680d99 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager.ts @@ -132,8 +132,8 @@ export class TerminalLinkManager extends DisposableStore { } private _setupLinkDetector(id: string, detector: ITerminalLinkDetector, isExternal: boolean = false): ILinkProvider { - const detectorAdapter = this._instantiationService.createInstance(TerminalLinkDetectorAdapter, detector); - detectorAdapter.onDidActivateLink(e => { + const detectorAdapter = this.add(this._instantiationService.createInstance(TerminalLinkDetectorAdapter, detector)); + this.add(detectorAdapter.onDidActivateLink(e => { // Prevent default electron link handling so Alt+Click mode works normally e.event?.preventDefault(); // Require correct modifier on click unless event is coming from linkQuickPick selection @@ -147,8 +147,8 @@ export class TerminalLinkManager extends DisposableStore { } else { this._openLink(e.link); } - }); - detectorAdapter.onDidShowHover(e => this._tooltipCallback(e.link, e.viewportRange, e.modifierDownCallback, e.modifierUpCallback)); + })); + this.add(detectorAdapter.onDidShowHover(e => this._tooltipCallback(e.link, e.viewportRange, e.modifierDownCallback, e.modifierUpCallback))); if (!isExternal) { this._standardLinkProviders.set(id, detectorAdapter); } diff --git a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkHelpers.test.ts b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkHelpers.test.ts index 32bc485b448..bf12083f0fe 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkHelpers.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkHelpers.test.ts @@ -6,8 +6,11 @@ import * as assert from 'assert'; import type { IBufferLine, IBufferCell } from 'xterm'; import { convertLinkRangeToBuffer } from 'vs/workbench/contrib/terminalContrib/links/browser/terminalLinkHelpers'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('Workbench - Terminal Link Helpers', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + suite('convertLinkRangeToBuffer', () => { test('should convert ranges for ascii characters', () => { const lines = createBufferLineArray([ diff --git a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkManager.test.ts b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkManager.test.ts index 884214a69e5..d40bbb8a085 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkManager.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkManager.test.ts @@ -24,6 +24,7 @@ import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServic import type { ILink, Terminal } from 'xterm'; import { TerminalLinkResolver } from 'vs/workbench/contrib/terminalContrib/links/browser/terminalLinkResolver'; import { importAMDNodeModule } from 'vs/amdX'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; const defaultTerminalConfig: Partial = { fontFamily: 'monospace', @@ -55,6 +56,8 @@ class TestLinkManager extends TerminalLinkManager { } suite('TerminalLinkManager', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + let instantiationService: TestInstantiationService; let configurationService: TestConfigurationService; let themeService: TestThemeService; @@ -75,17 +78,17 @@ suite('TerminalLinkManager', () => { themeService = new TestThemeService(); viewDescriptorService = new TestViewDescriptorService(); - instantiationService = new TestInstantiationService(); - instantiationService.stub(IContextMenuService, instantiationService.createInstance(ContextMenuService)); + instantiationService = store.add(new TestInstantiationService()); + instantiationService.stub(IContextMenuService, store.add(instantiationService.createInstance(ContextMenuService))); instantiationService.stub(IConfigurationService, configurationService); instantiationService.stub(ILogService, new NullLogService()); - instantiationService.stub(IStorageService, new TestStorageService()); + instantiationService.stub(IStorageService, store.add(new TestStorageService())); instantiationService.stub(IThemeService, themeService); instantiationService.stub(IViewDescriptorService, viewDescriptorService); const TerminalCtor = (await importAMDNodeModule('xterm', 'lib/xterm.js')).Terminal; - xterm = new TerminalCtor({ allowProposedApi: true, cols: 80, rows: 30 }); - linkManager = instantiationService.createInstance(TestLinkManager, xterm, upcastPartial({ + xterm = store.add(new TerminalCtor({ allowProposedApi: true, cols: 80, rows: 30 })); + linkManager = store.add(instantiationService.createInstance(TestLinkManager, xterm, upcastPartial({ get initialCwd() { return ''; } @@ -93,11 +96,7 @@ suite('TerminalLinkManager', () => { get(capability: T): ITerminalCapabilityImplMap[T] | undefined { return undefined; } - } as Partial as any, instantiationService.createInstance(TerminalLinkResolver)); - }); - - teardown(() => { - instantiationService.dispose(); + } as Partial as any, instantiationService.createInstance(TerminalLinkResolver))); }); suite('getLinks and open recent link', () => { diff --git a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkOpeners.test.ts b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkOpeners.test.ts index e9193758069..c55b7caff8b 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkOpeners.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkOpeners.test.ts @@ -27,6 +27,7 @@ import { IFileQuery, ISearchComplete, ISearchService } from 'vs/workbench/servic import { SearchService } from 'vs/workbench/services/search/common/searchService'; import { ITerminalLogService, ITerminalOutputMatcher } from 'vs/platform/terminal/common/terminal'; import { importAMDNodeModule } from 'vs/amdX'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; interface ITerminalLinkActivationResult { source: 'editor' | 'search'; @@ -70,6 +71,8 @@ class TestTerminalSearchLinkOpener extends TerminalSearchLinkOpener { } suite('Workbench - TerminalLinkOpeners', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + let instantiationService: TestInstantiationService; let fileService: TestFileService; let searchService: TestSearchService; @@ -77,9 +80,9 @@ suite('Workbench - TerminalLinkOpeners', () => { let xterm: Terminal; setup(async () => { - instantiationService = new TestInstantiationService(); - fileService = new TestFileService(new NullLogService()); - searchService = new TestSearchService(null!, null!, null!, null!, null!, null!, null!); + instantiationService = store.add(new TestInstantiationService()); + fileService = store.add(new TestFileService(new NullLogService())); + searchService = store.add(new TestSearchService(null!, null!, null!, null!, null!, null!, null!)); instantiationService.set(IFileService, fileService); instantiationService.set(ILogService, new NullLogService()); instantiationService.set(ISearchService, searchService); @@ -110,11 +113,7 @@ suite('Workbench - TerminalLinkOpeners', () => { } } as Partial); const TerminalCtor = (await importAMDNodeModule('xterm', 'lib/xterm.js')).Terminal; - xterm = new TerminalCtor({ allowProposedApi: true }); - }); - - teardown(() => { - instantiationService.dispose(); + xterm = store.add(new TerminalCtor({ allowProposedApi: true })); }); suite('TerminalSearchLinkOpener', () => { @@ -124,8 +123,8 @@ suite('Workbench - TerminalLinkOpeners', () => { let localFileOpener: TerminalLocalFileLinkOpener; setup(() => { - capabilities = new TerminalCapabilityStore(); - commandDetection = instantiationService.createInstance(TestCommandDetectionCapability, xterm); + capabilities = store.add(new TerminalCapabilityStore()); + commandDetection = store.add(instantiationService.createInstance(TestCommandDetectionCapability, xterm)); capabilities.add(TerminalCapability.CommandDetection, commandDetection); }); diff --git a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkParsing.test.ts b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkParsing.test.ts index 453b1f95ec4..54694c34f01 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkParsing.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkParsing.test.ts @@ -5,6 +5,7 @@ import { deepStrictEqual, ok, strictEqual } from 'assert'; import { OperatingSystem } from 'vs/base/common/platform'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { detectLinks, detectLinkSuffixes, getLinkSuffix, IParsedLink, removeLinkQueryString, removeLinkSuffix } from 'vs/workbench/contrib/terminalContrib/links/browser/terminalLinkParsing'; interface ITestLink { @@ -142,6 +143,8 @@ const testLinks: ITestLink[] = [ const testLinksWithSuffix = testLinks.filter(e => !!e.suffix); suite('TerminalLinkParsing', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + suite('removeLinkSuffix', () => { for (const testLink of testLinks) { test('`' + testLink.link + '`', () => { diff --git a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLocalLinkDetector.test.ts b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLocalLinkDetector.test.ts index ba2e367c310..4e7cfa28cf6 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLocalLinkDetector.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLocalLinkDetector.test.ts @@ -22,6 +22,7 @@ import { URI } from 'vs/base/common/uri'; import { NullLogService } from 'vs/platform/log/common/log'; import { ITerminalLogService } from 'vs/platform/terminal/common/terminal'; import { importAMDNodeModule } from 'vs/amdX'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; const unixLinks: (string | { link: string; resource: URI })[] = [ // Absolute @@ -145,6 +146,8 @@ const supportedFallbackLinkFormats: LinkFormatInfo[] = [ ]; suite('Workbench - TerminalLocalLinkDetector', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + let instantiationService: TestInstantiationService; let configurationService: TestConfigurationService; let detector: TerminalLocalLinkDetector; @@ -157,11 +160,13 @@ suite('Workbench - TerminalLocalLinkDetector', () => { text: string, expected: ({ uri: URI; range: [number, number][] })[] ) { + let to; const race = await Promise.race([ assertLinkHelper(text, expected, detector, type).then(() => 'success'), - timeout(2).then(() => 'timeout') + (to = timeout(2)).then(() => 'timeout') ]); strictEqual(race, 'success', `Awaiting link assertion for "${text}" timed out`); + to.cancel(); } async function assertLinksWithWrapped(link: string, resource?: URI) { @@ -173,7 +178,7 @@ suite('Workbench - TerminalLocalLinkDetector', () => { } setup(async () => { - instantiationService = new TestInstantiationService(); + instantiationService = store.add(new TestInstantiationService()); configurationService = new TestConfigurationService(); instantiationService.stub(IConfigurationService, configurationService); instantiationService.stub(IFileService, { @@ -192,13 +197,9 @@ suite('Workbench - TerminalLocalLinkDetector', () => { xterm = new TerminalCtor({ allowProposedApi: true, cols: 80, rows: 30 }); }); - teardown(() => { - instantiationService.dispose(); - }); - suite('platform independent', () => { setup(() => { - detector = instantiationService.createInstance(TerminalLocalLinkDetector, xterm, new TerminalCapabilityStore(), { + detector = instantiationService.createInstance(TerminalLocalLinkDetector, xterm, store.add(new TerminalCapabilityStore()), { initialCwd: '/parent/cwd', os: OperatingSystem.Linux, remoteAuthority: undefined, @@ -235,7 +236,7 @@ suite('Workbench - TerminalLocalLinkDetector', () => { suite('macOS/Linux', () => { setup(() => { - detector = instantiationService.createInstance(TerminalLocalLinkDetector, xterm, new TerminalCapabilityStore(), { + detector = instantiationService.createInstance(TerminalLocalLinkDetector, xterm, store.add(new TerminalCapabilityStore()), { initialCwd: '/parent/cwd', os: OperatingSystem.Linux, remoteAuthority: undefined, @@ -277,7 +278,7 @@ suite('Workbench - TerminalLocalLinkDetector', () => { const wslUnixToWindowsPathMap: Map = new Map(); setup(() => { - detector = instantiationService.createInstance(TerminalLocalLinkDetector, xterm, new TerminalCapabilityStore(), { + detector = instantiationService.createInstance(TerminalLocalLinkDetector, xterm, store.add(new TerminalCapabilityStore()), { initialCwd: 'C:\\Parent\\Cwd', os: OperatingSystem.Windows, remoteAuthority: undefined, diff --git a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalMultiLineLinkDetector.test.ts b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalMultiLineLinkDetector.test.ts index a0c84d878b2..e7725906b21 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalMultiLineLinkDetector.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalMultiLineLinkDetector.test.ts @@ -21,6 +21,7 @@ import { NullLogService } from 'vs/platform/log/common/log'; import { ITerminalLogService } from 'vs/platform/terminal/common/terminal'; import { TerminalMultiLineLinkDetector } from 'vs/workbench/contrib/terminalContrib/links/browser/terminalMultiLineLinkDetector'; import { importAMDNodeModule } from 'vs/amdX'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; const unixLinks: (string | { link: string; resource: URI })[] = [ // Absolute @@ -100,6 +101,8 @@ const supportedLinkFormats: LinkFormatInfo[] = [ ]; suite('Workbench - TerminalMultiLineLinkDetector', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + let instantiationService: TestInstantiationService; let configurationService: TestConfigurationService; let detector: TerminalMultiLineLinkDetector; @@ -112,11 +115,13 @@ suite('Workbench - TerminalMultiLineLinkDetector', () => { text: string, expected: ({ uri: URI; range: [number, number][] })[] ) { + let to; const race = await Promise.race([ assertLinkHelper(text, expected, detector, type).then(() => 'success'), - timeout(2).then(() => 'timeout') + (to = timeout(2)).then(() => 'timeout') ]); strictEqual(race, 'success', `Awaiting link assertion for "${text}" timed out`); + to.cancel(); } async function assertLinksMain(link: string, resource?: URI) { @@ -132,7 +137,7 @@ suite('Workbench - TerminalMultiLineLinkDetector', () => { } setup(async () => { - instantiationService = new TestInstantiationService(); + instantiationService = store.add(new TestInstantiationService()); configurationService = new TestConfigurationService(); instantiationService.stub(IConfigurationService, configurationService); instantiationService.stub(IFileService, { @@ -151,10 +156,6 @@ suite('Workbench - TerminalMultiLineLinkDetector', () => { xterm = new TerminalCtor({ allowProposedApi: true, cols: 80, rows: 30 }); }); - teardown(() => { - instantiationService.dispose(); - }); - suite('macOS/Linux', () => { setup(() => { detector = instantiationService.createInstance(TerminalMultiLineLinkDetector, xterm, { diff --git a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalUriLinkDetector.test.ts b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalUriLinkDetector.test.ts index f42e8619756..d4adc4f38f0 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalUriLinkDetector.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalUriLinkDetector.test.ts @@ -16,8 +16,11 @@ import { URI } from 'vs/base/common/uri'; import type { Terminal } from 'xterm'; import { OperatingSystem } from 'vs/base/common/platform'; import { importAMDNodeModule } from 'vs/amdX'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('Workbench - TerminalUriLinkDetector', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + let configurationService: TestConfigurationService; let detector: TerminalUriLinkDetector; let xterm: Terminal; @@ -25,7 +28,7 @@ suite('Workbench - TerminalUriLinkDetector', () => { let instantiationService: TestInstantiationService; setup(async () => { - instantiationService = new TestInstantiationService(); + instantiationService = store.add(new TestInstantiationService()); configurationService = new TestConfigurationService(); instantiationService.stub(IConfigurationService, configurationService); instantiationService.stub(IFileService, { diff --git a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalWordLinkDetector.test.ts b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalWordLinkDetector.test.ts index bdb6dc8ac97..c30d174f9ff 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalWordLinkDetector.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalWordLinkDetector.test.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { importAMDNodeModule } from 'vs/amdX'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; @@ -15,13 +16,15 @@ import { TestProductService } from 'vs/workbench/test/common/workbenchTestServic import type { Terminal } from 'xterm'; suite('Workbench - TerminalWordLinkDetector', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + let configurationService: TestConfigurationService; let detector: TerminalWordLinkDetector; let xterm: Terminal; let instantiationService: TestInstantiationService; setup(async () => { - instantiationService = new TestInstantiationService(); + instantiationService = store.add(new TestInstantiationService()); configurationService = new TestConfigurationService(); await configurationService.setUserConfiguration('terminal', { integrated: { wordSeparators: '' } }); @@ -29,12 +32,8 @@ suite('Workbench - TerminalWordLinkDetector', () => { instantiationService.set(IProductService, TestProductService); const TerminalCtor = (await importAMDNodeModule('xterm', 'lib/xterm.js')).Terminal; - xterm = new TerminalCtor({ allowProposedApi: true, cols: 80, rows: 30 }); - detector = instantiationService.createInstance(TerminalWordLinkDetector, xterm); - }); - - teardown(() => { - instantiationService.dispose(); + xterm = store.add(new TerminalCtor({ allowProposedApi: true, cols: 80, rows: 30 })); + detector = store.add(instantiationService.createInstance(TerminalWordLinkDetector, xterm)); }); async function assertLink( diff --git a/src/vs/workbench/contrib/terminalContrib/quickFix/test/browser/quickFixAddon.test.ts b/src/vs/workbench/contrib/terminalContrib/quickFix/test/browser/quickFixAddon.test.ts index 74d2ae5cd88..44f01d96083 100644 --- a/src/vs/workbench/contrib/terminalContrib/quickFix/test/browser/quickFixAddon.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/quickFix/test/browser/quickFixAddon.test.ts @@ -19,7 +19,7 @@ import { gitSimilar, freePort, FreePortOutputRegex, gitCreatePr, GitCreatePrOutp import { TerminalQuickFixAddon, getQuickFixesForCommand } from 'vs/workbench/contrib/terminalContrib/quickFix/browser/quickFixAddon'; import { URI } from 'vs/base/common/uri'; import type { Terminal } from 'xterm'; -import { Emitter } from 'vs/base/common/event'; +import { Event } from 'vs/base/common/event'; import { LabelService } from 'vs/workbench/services/label/common/labelService'; import { ILabelService } from 'vs/platform/label/common/label'; import { OpenerService } from 'vs/editor/browser/services/openerService'; @@ -30,8 +30,11 @@ import { ITerminalQuickFixService } from 'vs/workbench/contrib/terminalContrib/q import { ITerminalOutputMatcher } from 'vs/platform/terminal/common/terminal'; import { importAMDNodeModule } from 'vs/amdX'; import { TestCommandService } from 'vs/editor/test/browser/editorTestServices'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('QuickFixAddon', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + let quickFixAddon: TerminalQuickFixAddon; let commandDetection: CommandDetectionCapability; let commandService: TestCommandService; @@ -39,37 +42,36 @@ suite('QuickFixAddon', () => { let labelService: LabelService; let terminal: Terminal; let instantiationService: TestInstantiationService; + setup(async () => { - instantiationService = new TestInstantiationService(); + instantiationService = store.add(new TestInstantiationService()); const TerminalCtor = (await importAMDNodeModule('xterm', 'lib/xterm.js')).Terminal; - terminal = new TerminalCtor({ + terminal = store.add(new TerminalCtor({ allowProposedApi: true, cols: 80, rows: 30 - }); - instantiationService.stub(IStorageService, new TestStorageService()); + })); + instantiationService.stub(IStorageService, store.add(new TestStorageService())); instantiationService.stub(ITerminalQuickFixService, { - onDidRegisterProvider: new Emitter().event, - onDidUnregisterProvider: new Emitter().event, - onDidRegisterCommandSelector: new Emitter().event, + onDidRegisterProvider: Event.None, + onDidUnregisterProvider: Event.None, + onDidRegisterCommandSelector: Event.None, extensionQuickFixes: Promise.resolve([]) } as Partial); instantiationService.stub(IConfigurationService, new TestConfigurationService()); instantiationService.stub(ILabelService, {} as Partial); - const capabilities = new TerminalCapabilityStore(); + const capabilities = store.add(new TerminalCapabilityStore()); instantiationService.stub(ILogService, new NullLogService()); - commandDetection = instantiationService.createInstance(CommandDetectionCapability, terminal); + commandDetection = store.add(instantiationService.createInstance(CommandDetectionCapability, terminal)); capabilities.add(TerminalCapability.CommandDetection, commandDetection); - instantiationService.stub(IContextMenuService, instantiationService.createInstance(ContextMenuService)); + instantiationService.stub(IContextMenuService, store.add(instantiationService.createInstance(ContextMenuService))); instantiationService.stub(IOpenerService, {} as Partial); commandService = new TestCommandService(instantiationService); quickFixAddon = instantiationService.createInstance(TerminalQuickFixAddon, [], capabilities); terminal.loadAddon(quickFixAddon); }); - teardown(() => { - instantiationService.dispose(); - }); + suite('registerCommandFinishedListener & getMatchActions', () => { suite('gitSimilarCommand', () => { const expectedMap = new Map(); diff --git a/src/vs/workbench/contrib/testing/browser/media/testing.css b/src/vs/workbench/contrib/testing/browser/media/testing.css index 77b8a2fd3c1..01d1919eecf 100644 --- a/src/vs/workbench/contrib/testing/browser/media/testing.css +++ b/src/vs/workbench/contrib/testing/browser/media/testing.css @@ -66,6 +66,7 @@ .test-output-peek-tree { color: var(--vscode-editor-foreground); + border-left: 1px solid var(--vscode-panelSection-border); } .test-output-peek-tree .monaco-list-row .monaco-action-bar, diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts index b0ad29a3597..5b3a558fba2 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts @@ -431,7 +431,7 @@ export class TestingExplorerView extends ViewPane { this.dimensions.height = height; this.dimensions.width = width; this.container.style.height = `${height}px`; - this.viewModel.layout(height - this.treeHeader.clientHeight, width); + this.viewModel?.layout(height - this.treeHeader.clientHeight, width); this.filter.value?.layout(width); } } diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index 79454864a6b..ade1e1091e3 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts @@ -68,6 +68,7 @@ import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storag import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; import { TerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/terminalCapabilityStore'; +import { formatMessageForTerminal } from 'vs/platform/terminal/common/terminalStrings'; import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; @@ -1492,6 +1493,7 @@ class TerminalMessagePeek extends Disposable implements IPeekOutputRenderer { terminal.xterm.write(chunk.buffer, () => pendingWrites.value--); } } else { + didWriteData = true; this.writeNotice(terminal, localize('runNoOutputForPast', 'Test output is only available for new test runs.')); } @@ -1527,7 +1529,7 @@ class TerminalMessagePeek extends Disposable implements IPeekOutputRenderer { } private writeNotice(terminal: IDetachedTerminalInstance, str: string) { - terminal.xterm.write(`\x1b[2m${str}\x1b[0m`); + terminal.xterm.write(formatMessageForTerminal(str)); } private attachTerminalToDom(terminal: IDetachedTerminalInstance) { @@ -2245,14 +2247,6 @@ class TreeActionsProvider { if (element instanceof TestCaseElement) { const extId = element.test.item.extId; - primary.push(new Action( - 'testing.outputPeek.goToFile', - localize('testing.goToFile', "Go to Source"), - ThemeIcon.asClassName(Codicon.goToFile), - undefined, - () => this.commandService.executeCommand('vscode.revealTest', extId), - )); - if (element.test.tasks[element.taskIndex].messages.some(m => m.type === TestMessageType.Output)) { primary.push(new Action( 'testing.outputPeek.showResultOutput', @@ -2290,6 +2284,14 @@ class TreeActionsProvider { () => this.commandService.executeCommand('vscode.runTestsById', TestRunProfileBitset.Debug, extId), )); } + + primary.push(new Action( + 'testing.outputPeek.goToFile', + localize('testing.goToFile', "Go to Source"), + ThemeIcon.asClassName(Codicon.goToFile), + undefined, + () => this.commandService.executeCommand('vscode.revealTest', extId), + )); } if (element instanceof TestMessageElement) { diff --git a/src/vs/workbench/contrib/themes/browser/themes.contribution.ts b/src/vs/workbench/contrib/themes/browser/themes.contribution.ts index f245ba5a9bc..2dcb37944d7 100644 --- a/src/vs/workbench/contrib/themes/browser/themes.contribution.ts +++ b/src/vs/workbench/contrib/themes/browser/themes.contribution.ts @@ -328,6 +328,7 @@ class InstalledThemesPicker { quickpick.placeholder = this.placeholderMessage; quickpick.activeItems = [picks[autoFocusIndex] as ThemeItem]; quickpick.canSelectMany = false; + quickpick.matchOnDescription = true; quickpick.onDidAccept(async _ => { isCompleted = true; const theme = quickpick.selectedItems[0]; @@ -545,7 +546,13 @@ function isItem(i: QuickPickInput): i is ThemeItem { } function toEntry(theme: IWorkbenchTheme): ThemeItem { - const item: ThemeItem = { id: theme.id, theme: theme, label: theme.label, description: theme.description }; + const settingId = theme.settingsId ?? undefined; + const item: ThemeItem = { + id: theme.id, + theme: theme, + label: theme.label, + description: theme.description || (theme.label === settingId ? undefined : settingId), + }; if (theme.extensionData) { item.buttons = [configureButton]; } diff --git a/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/src/vs/workbench/contrib/webview/browser/webviewElement.ts index 535da4b66c8..a860270eca9 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -29,7 +29,7 @@ import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remot import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ITunnelService } from 'vs/platform/tunnel/common/tunnel'; import { WebviewPortMappingManager } from 'vs/platform/webview/common/webviewPortMapping'; -import { parentOriginHash } from 'vs/workbench/browser/iframe'; +import { parentOriginHash } from 'vs/base/browser/iframe'; import { loadLocalResource, WebviewResourceResponse } from 'vs/workbench/contrib/webview/browser/resourceLoading'; import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/browser/themeing'; import { areWebviewContentOptionsEqual, IWebview, WebviewContentOptions, WebviewExtensionDescription, WebviewInitInfo, WebviewMessageReceivedEvent, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview'; diff --git a/src/vs/workbench/electron-sandbox/desktop.contribution.ts b/src/vs/workbench/electron-sandbox/desktop.contribution.ts index 7ae54948297..fda561a69e5 100644 --- a/src/vs/workbench/electron-sandbox/desktop.contribution.ts +++ b/src/vs/workbench/electron-sandbox/desktop.contribution.ts @@ -222,12 +222,6 @@ import { applicationConfigurationNodeBase } from 'vs/workbench/common/configurat 'scope': ConfigurationScope.APPLICATION, 'description': localize('titleBarStyle', "Adjust the appearance of the window title bar. On Linux and Windows, this setting also affects the application and context menu appearances. Changes require a full restart to apply.") }, - 'window.experimental.nativeContextMenuLocation': { // TODO@bpasero remove me eventually - 'type': 'boolean', - 'default': true, - 'scope': ConfigurationScope.APPLICATION, - 'description': localize('nativeContextMenuLocation', "Let the OS handle positioning of the context menu in cases where it should appear under the mouse.") - }, 'window.dialogStyle': { 'type': 'string', 'enum': ['native', 'custom'], diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index 1efcf6ba71f..21512355061 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -725,7 +725,7 @@ export class NativeWindow extends Disposable { // Windows 32-bit warning if (isWindows && this.environmentService.os.arch === 'ia32') { - const message = localize('windows32eolmessage', "{0} on Windows 32-bit will soon stop receiving updates. Consider upgrading to the 64-bit build.", this.productService.nameLong); + const message = localize('windows32eolmessage', "You are running {0} 32-bit, which will soon stop receiving updates on Windows. Consider upgrading to the 64-bit build.", this.productService.nameLong); const actions = [{ label: localize('windowseolBannerLearnMore', "Learn More"), href: 'https://aka.ms/vscode-faq-old-windows' diff --git a/src/vs/workbench/services/configuration/test/browser/configuration.test.ts b/src/vs/workbench/services/configuration/test/browser/configuration.test.ts index a13b9e59902..1b9393eb4c8 100644 --- a/src/vs/workbench/services/configuration/test/browser/configuration.test.ts +++ b/src/vs/workbench/services/configuration/test/browser/configuration.test.ts @@ -7,6 +7,7 @@ import * as assert from 'assert'; import { Event } from 'vs/base/common/event'; import { joinPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; import { DefaultConfiguration } from 'vs/workbench/services/configuration/browser/configuration'; @@ -25,6 +26,7 @@ class ConfigurationCache implements IConfigurationCache { suite('DefaultConfiguration', () => { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); const configurationRegistry = Registry.as(Extensions.Configuration); const cacheKey: ConfigurationKey = { type: 'defaults', key: 'configurationDefaultsOverrides' }; let configurationCache: ConfigurationCache; @@ -51,7 +53,7 @@ suite('DefaultConfiguration', () => { test('configuration default overrides are read from environment', async () => { const environmentService = new BrowserWorkbenchEnvironmentService('', joinPath(URI.file('tests').with({ scheme: 'vscode-tests' }), 'logs'), { configurationDefaults: { 'test.configurationDefaultsOverride': 'envOverrideValue' } }, TestProductService); - const testObject = new DefaultConfiguration(configurationCache, environmentService); + const testObject = disposables.add(new DefaultConfiguration(configurationCache, environmentService)); await testObject.initialize(); assert.deepStrictEqual(testObject.configurationModel.getValue('test.configurationDefaultsOverride'), 'envOverrideValue'); }); @@ -59,7 +61,7 @@ suite('DefaultConfiguration', () => { test('configuration default overrides are read from cache', async () => { window.localStorage.setItem(DefaultConfiguration.DEFAULT_OVERRIDES_CACHE_EXISTS_KEY, 'yes'); await configurationCache.write(cacheKey, JSON.stringify({ 'test.configurationDefaultsOverride': 'overrideValue' })); - const testObject = new DefaultConfiguration(configurationCache, TestEnvironmentService); + const testObject = disposables.add(new DefaultConfiguration(configurationCache, TestEnvironmentService)); const actual = await testObject.initialize(); @@ -70,7 +72,7 @@ suite('DefaultConfiguration', () => { test('configuration default overrides are not read from cache when model is read before initialize', async () => { window.localStorage.setItem(DefaultConfiguration.DEFAULT_OVERRIDES_CACHE_EXISTS_KEY, 'yes'); await configurationCache.write(cacheKey, JSON.stringify({ 'test.configurationDefaultsOverride': 'overrideValue' })); - const testObject = new DefaultConfiguration(configurationCache, TestEnvironmentService); + const testObject = disposables.add(new DefaultConfiguration(configurationCache, TestEnvironmentService)); assert.deepStrictEqual(testObject.configurationModel.getValue('test.configurationDefaultsOverride'), undefined); }); @@ -78,7 +80,7 @@ suite('DefaultConfiguration', () => { const environmentService = new BrowserWorkbenchEnvironmentService('', joinPath(URI.file('tests').with({ scheme: 'vscode-tests' }), 'logs'), { configurationDefaults: { 'test.configurationDefaultsOverride': 'envOverrideValue' } }, TestProductService); window.localStorage.setItem(DefaultConfiguration.DEFAULT_OVERRIDES_CACHE_EXISTS_KEY, 'yes'); await configurationCache.write(cacheKey, JSON.stringify({ 'test.configurationDefaultsOverride': 'overrideValue' })); - const testObject = new DefaultConfiguration(configurationCache, environmentService); + const testObject = disposables.add(new DefaultConfiguration(configurationCache, environmentService)); const actual = await testObject.initialize(); @@ -88,7 +90,7 @@ suite('DefaultConfiguration', () => { test('configuration default overrides are read from cache when default configuration changed', async () => { window.localStorage.setItem(DefaultConfiguration.DEFAULT_OVERRIDES_CACHE_EXISTS_KEY, 'yes'); await configurationCache.write(cacheKey, JSON.stringify({ 'test.configurationDefaultsOverride': 'overrideValue' })); - const testObject = new DefaultConfiguration(configurationCache, TestEnvironmentService); + const testObject = disposables.add(new DefaultConfiguration(configurationCache, TestEnvironmentService)); await testObject.initialize(); const promise = Event.toPromise(testObject.onDidChangeConfiguration); @@ -110,7 +112,7 @@ suite('DefaultConfiguration', () => { test('configuration default overrides are not read from cache after reload', async () => { window.localStorage.setItem(DefaultConfiguration.DEFAULT_OVERRIDES_CACHE_EXISTS_KEY, 'yes'); await configurationCache.write(cacheKey, JSON.stringify({ 'test.configurationDefaultsOverride': 'overrideValue' })); - const testObject = new DefaultConfiguration(configurationCache, TestEnvironmentService); + const testObject = disposables.add(new DefaultConfiguration(configurationCache, TestEnvironmentService)); await testObject.initialize(); const actual = testObject.reload(); @@ -121,7 +123,7 @@ suite('DefaultConfiguration', () => { test('cache is reset after reload', async () => { window.localStorage.setItem(DefaultConfiguration.DEFAULT_OVERRIDES_CACHE_EXISTS_KEY, 'yes'); await configurationCache.write(cacheKey, JSON.stringify({ 'test.configurationDefaultsOverride': 'overrideValue' })); - const testObject = new DefaultConfiguration(configurationCache, TestEnvironmentService); + const testObject = disposables.add(new DefaultConfiguration(configurationCache, TestEnvironmentService)); await testObject.initialize(); testObject.reload(); @@ -130,7 +132,7 @@ suite('DefaultConfiguration', () => { }); test('configuration default overrides are written in cache', async () => { - const testObject = new DefaultConfiguration(configurationCache, TestEnvironmentService); + const testObject = disposables.add(new DefaultConfiguration(configurationCache, TestEnvironmentService)); await testObject.initialize(); testObject.reload(); const promise = Event.toPromise(testObject.onDidChangeConfiguration); @@ -142,7 +144,7 @@ suite('DefaultConfiguration', () => { }); test('configuration default overrides are removed from cache if there are no overrides', async () => { - const testObject = new DefaultConfiguration(configurationCache, TestEnvironmentService); + const testObject = disposables.add(new DefaultConfiguration(configurationCache, TestEnvironmentService)); await testObject.initialize(); const promise = Event.toPromise(testObject.onDidChangeConfiguration); configurationRegistry.registerConfiguration({ diff --git a/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts b/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts index 66b9fb03b19..e05e7d2519c 100644 --- a/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts +++ b/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts @@ -54,7 +54,7 @@ export class ContextMenuService implements IContextMenuService { // Native context menu: otherwise else { - this.impl = new NativeContextMenuService(notificationService, telemetryService, keybindingService, menuService, contextKeyService, configurationService); + this.impl = new NativeContextMenuService(notificationService, telemetryService, keybindingService, menuService, contextKeyService); } } @@ -77,28 +77,14 @@ class NativeContextMenuService extends Disposable implements IContextMenuService private readonly _onDidHideContextMenu = this._store.add(new Emitter()); readonly onDidHideContextMenu = this._onDidHideContextMenu.event; - private useNativeContextMenuLocation = false; - constructor( @INotificationService private readonly notificationService: INotificationService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IKeybindingService private readonly keybindingService: IKeybindingService, @IMenuService private readonly menuService: IMenuService, - @IContextKeyService private readonly contextKeyService: IContextKeyService, - @IConfigurationService private readonly configurationService: IConfigurationService + @IContextKeyService private readonly contextKeyService: IContextKeyService ) { super(); - - this.updateUseNativeContextMenuLocation(); - this._register(this.configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('window.experimental.nativeContextMenuLocation')) { - this.updateUseNativeContextMenuLocation(); - } - })); - } - - private updateUseNativeContextMenuLocation(): void { - this.useNativeContextMenuLocation = this.configurationService.getValue('window.experimental.nativeContextMenuLocation') === true; } showContextMenu(delegate: IContextMenuDelegate | IContextMenuMenuDelegate): void { @@ -173,13 +159,8 @@ class NativeContextMenuService extends Disposable implements IContextMenuService x = anchor.x; y = anchor.y; } else { - if (this.useNativeContextMenuLocation) { - // We leave x/y undefined in this case which will result in - // Electron taking care of opening the menu at the cursor position. - } else { - x = anchor.posx + 1; // prevent first item from being selected automatically under mouse - y = anchor.posy; - } + // We leave x/y undefined in this case which will result in + // Electron taking care of opening the menu at the cursor position. } if (typeof x === 'number') { diff --git a/src/vs/workbench/services/decorations/browser/decorationsService.ts b/src/vs/workbench/services/decorations/browser/decorationsService.ts index 44aaa12b056..e725eccdd1b 100644 --- a/src/vs/workbench/services/decorations/browser/decorationsService.ts +++ b/src/vs/workbench/services/decorations/browser/decorationsService.ts @@ -268,6 +268,7 @@ export class DecorationsService implements IDecorationsService { dispose(): void { this._onDidChangeDecorations.dispose(); this._onDidChangeDecorationsDelayed.dispose(); + this._data.clear(); } registerDecorationsProvider(provider: IDecorationsProvider): IDisposable { @@ -374,15 +375,16 @@ export class DecorationsService implements IDecorationsService { map.delete(provider); } - const source = new CancellationTokenSource(); - const dataOrThenable = provider.provideDecorations(uri, source.token); + const cts = new CancellationTokenSource(); + const dataOrThenable = provider.provideDecorations(uri, cts.token); if (!isThenable | undefined>(dataOrThenable)) { // sync -> we have a result now + cts.dispose(); return this._keepItem(map, provider, uri, dataOrThenable); } else { // async -> we have a result soon - const request = new DecorationDataRequest(source, Promise.resolve(dataOrThenable).then(data => { + const request = new DecorationDataRequest(cts, Promise.resolve(dataOrThenable).then(data => { if (map.get(provider) === request) { this._keepItem(map, provider, uri, data); } @@ -390,6 +392,8 @@ export class DecorationsService implements IDecorationsService { if (!isCancellationError(err) && map.get(provider) === request) { map.delete(provider); } + }).finally(() => { + cts.dispose(); })); map.set(provider, request); diff --git a/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts b/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts index c1a086d323f..ae94c92179d 100644 --- a/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts +++ b/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts @@ -14,13 +14,13 @@ import { mock } from 'vs/base/test/common/mock'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('DecorationsService', function () { let service: DecorationsService; setup(function () { - service?.dispose(); service = new DecorationsService( new class extends mock() { override extUri = resources.extUri; @@ -29,6 +29,13 @@ suite('DecorationsService', function () { ); }); + teardown(function () { + service.dispose(); + }); + + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + test('Async provider, async/evented result', function () { return runWithFakedTimers({}, async function () { @@ -36,7 +43,7 @@ suite('DecorationsService', function () { const uri = URI.parse('foo:bar'); let callCounter = 0; - service.registerDecorationsProvider(new class implements IDecorationsProvider { + const reg = service.registerDecorationsProvider(new class implements IDecorationsProvider { readonly label: string = 'Test'; readonly onDidChange: Event = Event.None; provideDecorations(uri: URI) { @@ -62,6 +69,8 @@ suite('DecorationsService', function () { assert.deepStrictEqual(service.getDecoration(uri, false)!.tooltip, 'T'); assert.deepStrictEqual(service.getDecoration(uri, false)!.strikethrough, true); assert.strictEqual(callCounter, 1); + + reg.dispose(); }); }); @@ -70,7 +79,7 @@ suite('DecorationsService', function () { const uri = URI.parse('foo:bar'); let callCounter = 0; - service.registerDecorationsProvider(new class implements IDecorationsProvider { + const reg = service.registerDecorationsProvider(new class implements IDecorationsProvider { readonly label: string = 'Test'; readonly onDidChange: Event = Event.None; provideDecorations(uri: URI) { @@ -83,6 +92,8 @@ suite('DecorationsService', function () { assert.deepStrictEqual(service.getDecoration(uri, false)!.tooltip, 'Z'); assert.deepStrictEqual(service.getDecoration(uri, false)!.strikethrough, false); assert.strictEqual(callCounter, 1); + + reg.dispose(); }); test('Clear decorations on provider dispose', async function () { @@ -107,11 +118,12 @@ suite('DecorationsService', function () { // un-register -> ensure good event let didSeeEvent = false; const p = new Promise(resolve => { - service.onDidChangeDecorations(e => { + const l = service.onDidChangeDecorations(e => { assert.strictEqual(e.affectsResource(uri), true); assert.deepStrictEqual(service.getDecoration(uri, false), undefined); assert.strictEqual(callCounter, 1); didSeeEvent = true; + l.dispose(); resolve(); }); }); @@ -159,12 +171,12 @@ suite('DecorationsService', function () { deco = service.getDecoration(childUri.with({ path: 'some/path/' }), true)!; assert.strictEqual(typeof deco.tooltip, 'string'); + reg.dispose(); }); test('Decorations not showing up for second root folder #48502', async function () { let cancelCount = 0; - const winjsCancelCount = 0; let callCount = 0; const provider = new class implements IDecorationsProvider { @@ -176,9 +188,9 @@ suite('DecorationsService', function () { provideDecorations(uri: URI, token: CancellationToken): Promise { - token.onCancellationRequested(() => { + store.add(token.onCancellationRequested(() => { cancelCount += 1; - }); + })); return new Promise(resolve => { callCount += 1; @@ -192,15 +204,16 @@ suite('DecorationsService', function () { const reg = service.registerDecorationsProvider(provider); const uri = URI.parse('foo://bar'); - service.getDecoration(uri, false); + const d1 = service.getDecoration(uri, false); provider._onDidChange.fire([uri]); - service.getDecoration(uri, false); + const d2 = service.getDecoration(uri, false); assert.strictEqual(cancelCount, 1); - assert.strictEqual(winjsCancelCount, 0); assert.strictEqual(callCount, 2); + d1?.dispose(); + d2?.dispose(); reg.dispose(); }); @@ -314,23 +327,23 @@ suite('DecorationsService', function () { const invokeOrder: string[] = []; - service.registerDecorationsProvider(new class { + store.add(service.registerDecorationsProvider(new class { label = 'Provider-1'; onDidChange = Event.None; provideDecorations() { invokeOrder.push(this.label); return undefined; } - }); + })); - service.registerDecorationsProvider(new class { + store.add(service.registerDecorationsProvider(new class { label = 'Provider-2'; onDidChange = Event.None; provideDecorations() { invokeOrder.push(this.label); return undefined; } - }); + })); service.getDecoration(URI.parse('test://me/path'), false); diff --git a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts index e05cee676fc..e2881cf20cc 100644 --- a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { workbenchInstantiationService, registerTestEditor, TestFileEditorInput, TestEditorPart, TestServiceAccessor, createEditorPart } from 'vs/workbench/test/browser/workbenchTestServices'; +import { workbenchInstantiationService, registerTestEditor, TestFileEditorInput, TestEditorPart, TestServiceAccessor, createEditorPart, ITestInstantiationService, workbenchTeardown } from 'vs/workbench/test/browser/workbenchTestServices'; import { GroupDirection, GroupsOrder, MergeGroupMode, GroupOrientation, GroupLocation, isEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { CloseDirection, IEditorPartOptions, EditorsOrder, EditorInputCapabilities, GroupModelChangeKind, SideBySideEditor } from 'vs/workbench/common/editor'; import { URI } from 'vs/base/common/uri'; @@ -17,6 +17,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { IGroupModelChangeEvent, IGroupEditorMoveEvent, IGroupEditorOpenEvent } from 'vs/workbench/common/editor/editorGroupModel'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('EditorGroupsService', () => { @@ -25,11 +26,18 @@ suite('EditorGroupsService', () => { const disposables = new DisposableStore(); + let testLocalInstantiationService: ITestInstantiationService | undefined = undefined; + setup(() => { disposables.add(registerTestEditor(TEST_EDITOR_ID, [new SyncDescriptor(TestFileEditorInput), new SyncDescriptor(SideBySideEditorInput)], TEST_EDITOR_INPUT_ID)); }); - teardown(() => { + teardown(async () => { + if (testLocalInstantiationService) { + await workbenchTeardown(testLocalInstantiationService); + testLocalInstantiationService = undefined; + } + disposables.clear(); }); @@ -37,9 +45,15 @@ suite('EditorGroupsService', () => { const part = await createEditorPart(instantiationService, disposables); instantiationService.stub(IEditorGroupsService, part); + testLocalInstantiationService = instantiationService; + return [part, instantiationService]; } + function createTestFileEditorInput(resource: URI, typeId: string): TestFileEditorInput { + return disposables.add(new TestFileEditorInput(resource, typeId)); + } + test('groups basics', async function () { const instantiationService = workbenchInstantiationService({ contextKeyService: instantiationService => instantiationService.createInstance(MockScopableContextKeyService) }, disposables); const [part] = await createPart(instantiationService); @@ -123,9 +137,9 @@ suite('EditorGroupsService', () => { const downGroup = part.addGroup(rightGroup, GroupDirection.DOWN); let didDispose = false; - downGroup.onWillDispose(() => { + disposables.add(downGroup.onWillDispose(() => { didDispose = true; - }); + })); assert.strictEqual(groupAddedCounter, 2); assert.strictEqual(part.groups.length, 3); assert.ok(part.activeGroup === rightGroup); @@ -201,9 +215,9 @@ suite('EditorGroupsService', () => { const rootGroup = part.activeGroup; - const input1 = new TestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); - const input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); - const input3 = new TestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); + const input1 = createTestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); + const input2 = createTestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); + const input3 = createTestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); await rootGroup.openEditor(input1, { pinned: true }); await part.sideGroup.openEditor(input2, { pinned: true }); @@ -221,10 +235,10 @@ suite('EditorGroupsService', () => { const rightGroup = part.addGroup(rootGroup, GroupDirection.RIGHT); const downGroup = part.addGroup(rightGroup, GroupDirection.DOWN); - const rootGroupInput = new TestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); + const rootGroupInput = createTestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); await rootGroup.openEditor(rootGroupInput, { pinned: true }); - const rightGroupInput = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); + const rightGroupInput = createTestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); await rightGroup.openEditor(rightGroupInput, { pinned: true }); assert.strictEqual(part.groups.length, 3); @@ -317,7 +331,7 @@ suite('EditorGroupsService', () => { rootGroupDisposed = true; }); - const input = new TestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); + const input = createTestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); await rootGroup.openEditor(input, { pinned: true }); const rightGroup = part.addGroup(rootGroup, GroupDirection.RIGHT); @@ -346,9 +360,9 @@ suite('EditorGroupsService', () => { const rootGroup = part.groups[0]; - const input1 = new TestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); - const input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); - const input3 = new TestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); + const input1 = createTestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); + const input2 = createTestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); + const input3 = createTestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); await rootGroup.openEditor(input1, { pinned: true }); @@ -382,15 +396,15 @@ suite('EditorGroupsService', () => { let oldOptions!: IEditorPartOptions; let newOptions!: IEditorPartOptions; - part.onDidChangeEditorPartOptions(event => { + disposables.add(part.onDidChangeEditorPartOptions(event => { oldOptions = event.oldPartOptions; newOptions = event.newPartOptions; - }); + })); const currentOptions = part.partOptions; assert.ok(currentOptions); - part.enforcePartOptions({ showTabs: false }); + disposables.add(part.enforcePartOptions({ showTabs: false })); assert.strictEqual(part.partOptions.showTabs, false); assert.strictEqual(newOptions.showTabs, false); assert.strictEqual(oldOptions, currentOptions); @@ -449,8 +463,8 @@ suite('EditorGroupsService', () => { editorDidCloseCounter++; }); - const input = new TestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); - const inputInactive = new TestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID); + const input = createTestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); + const inputInactive = createTestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID); await group.openEditor(input, { pinned: true }); await group.openEditor(inputInactive, { inactive: true }); @@ -536,8 +550,8 @@ suite('EditorGroupsService', () => { const group = part.activeGroup; assert.strictEqual(group.isEmpty, true); - const input = new TestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); - const inputInactive = new TestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID); + const input = createTestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); + const inputInactive = createTestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID); await group.openEditors([ { editor: input, options: { pinned: true } }, @@ -564,7 +578,7 @@ suite('EditorGroupsService', () => { const group = part.activeGroup; - const input = new TestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); + const input = createTestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); input.dirty = true; await group.openEditor(input); @@ -589,8 +603,8 @@ suite('EditorGroupsService', () => { const rightGroup = part.addGroup(group, GroupDirection.RIGHT); - const input = new TestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); - const inputInactive = new TestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID); + const input = createTestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); + const inputInactive = createTestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID); await group.openEditors([{ editor: input, options: { pinned: true } }, { editor: inputInactive }]); await rightGroup.openEditors([{ editor: input, options: { pinned: true } }, { editor: inputInactive }]); @@ -615,10 +629,10 @@ suite('EditorGroupsService', () => { const group = part.activeGroup; - const input1 = new TestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); + const input1 = createTestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); input1.dirty = true; - const input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); + const input2 = createTestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); await group.openEditor(input1); await group.openEditor(input2); @@ -643,9 +657,9 @@ suite('EditorGroupsService', () => { const group = part.activeGroup; assert.strictEqual(group.isEmpty, true); - const input1 = new TestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); - const input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); - const input3 = new TestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); + const input1 = createTestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); + const input2 = createTestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); + const input3 = createTestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); await group.openEditors([ { editor: input1, options: { pinned: true } }, @@ -668,9 +682,9 @@ suite('EditorGroupsService', () => { const group = part.activeGroup; assert.strictEqual(group.isEmpty, true); - const input1 = new TestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); - const input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); - const input3 = new TestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); + const input1 = createTestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); + const input2 = createTestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); + const input3 = createTestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); await group.openEditors([ { editor: input1, options: { pinned: true, sticky: true } }, @@ -703,9 +717,9 @@ suite('EditorGroupsService', () => { const group = part.activeGroup; assert.strictEqual(group.isEmpty, true); - const input1 = new TestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); - const input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); - const input3 = new TestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); + const input1 = createTestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); + const input2 = createTestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); + const input3 = createTestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); await group.openEditors([ { editor: input1, options: { pinned: true } }, @@ -727,9 +741,9 @@ suite('EditorGroupsService', () => { const group = part.activeGroup; assert.strictEqual(group.isEmpty, true); - const input1 = new TestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); - const input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); - const input3 = new TestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); + const input1 = createTestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); + const input2 = createTestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); + const input3 = createTestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); await group.openEditors([ { editor: input1, options: { pinned: true, sticky: true } }, @@ -758,9 +772,9 @@ suite('EditorGroupsService', () => { const group = part.activeGroup; assert.strictEqual(group.isEmpty, true); - const input1 = new TestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); - const input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); - const input3 = new TestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); + const input1 = createTestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); + const input2 = createTestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); + const input3 = createTestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); await group.openEditors([ { editor: input1, options: { pinned: true } }, @@ -784,9 +798,9 @@ suite('EditorGroupsService', () => { const group = part.activeGroup; assert.strictEqual(group.isEmpty, true); - const input1 = new TestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); - const input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); - const input3 = new TestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); + const input1 = createTestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); + const input2 = createTestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); + const input3 = createTestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); await group.openEditors([ { editor: input1, options: { pinned: true, sticky: true } }, @@ -817,9 +831,9 @@ suite('EditorGroupsService', () => { const group = part.activeGroup; assert.strictEqual(group.isEmpty, true); - const input1 = new TestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); - const input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); - const input3 = new TestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); + const input1 = createTestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); + const input2 = createTestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); + const input3 = createTestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); await group.openEditors([ { editor: input1, options: { pinned: true } }, @@ -843,9 +857,9 @@ suite('EditorGroupsService', () => { const group = part.activeGroup; assert.strictEqual(group.isEmpty, true); - const input1 = new TestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); - const input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); - const input3 = new TestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); + const input1 = createTestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); + const input2 = createTestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); + const input3 = createTestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); await group.openEditors([ { editor: input1, options: { pinned: true, sticky: true } }, @@ -877,8 +891,8 @@ suite('EditorGroupsService', () => { const group = part.activeGroup; assert.strictEqual(group.isEmpty, true); - const input = new TestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); - const inputInactive = new TestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID); + const input = createTestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); + const inputInactive = createTestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID); await group.openEditors([ { editor: input, options: { pinned: true } }, @@ -902,10 +916,10 @@ suite('EditorGroupsService', () => { const group = part.activeGroup; - const input1 = new TestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); + const input1 = createTestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); input1.dirty = true; - const input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); + const input2 = createTestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); await group.openEditor(input1); await group.openEditor(input2); @@ -930,8 +944,8 @@ suite('EditorGroupsService', () => { const group = part.activeGroup; assert.strictEqual(group.isEmpty, true); - const input = new TestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); - const inputInactive = new TestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID); + const input = createTestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); + const inputInactive = createTestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID); await group.openEditors([ { editor: input, options: { pinned: true, sticky: true } }, @@ -957,8 +971,8 @@ suite('EditorGroupsService', () => { const group = part.activeGroup; assert.strictEqual(group.isEmpty, true); - const input = new TestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); - const inputInactive = new TestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID); + const input = createTestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); + const inputInactive = createTestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID); const moveEvents: IGroupModelChangeEvent[] = []; const editorGroupModelChangeListener = group.onDidModelChange(e => { @@ -998,8 +1012,8 @@ suite('EditorGroupsService', () => { const rightGroup = part.addGroup(group, GroupDirection.RIGHT); - const input = new TestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); - const inputInactive = new TestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID); + const input = createTestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); + const inputInactive = createTestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID); await group.openEditors([{ editor: input, options: { pinned: true } }, { editor: inputInactive }]); assert.strictEqual(group.count, 2); @@ -1019,9 +1033,9 @@ suite('EditorGroupsService', () => { const rightGroup = part.addGroup(group, GroupDirection.RIGHT); - const input1 = new TestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); - const input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); - const input3 = new TestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); + const input1 = createTestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); + const input2 = createTestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); + const input3 = createTestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); await group.openEditors([{ editor: input1, options: { pinned: true } }, { editor: input2, options: { pinned: true } }, { editor: input3, options: { pinned: true } }]); assert.strictEqual(group.getEditorByIndex(0), input1); @@ -1042,8 +1056,8 @@ suite('EditorGroupsService', () => { const rightGroup = part.addGroup(group, GroupDirection.RIGHT); - const input = new TestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); - const inputInactive = new TestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID); + const input = createTestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); + const inputInactive = createTestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID); await group.openEditors([{ editor: input, options: { pinned: true } }, { editor: inputInactive }]); assert.strictEqual(group.count, 2); @@ -1064,9 +1078,9 @@ suite('EditorGroupsService', () => { const rightGroup = part.addGroup(group, GroupDirection.RIGHT); - const input1 = new TestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); - const input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); - const input3 = new TestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); + const input1 = createTestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); + const input2 = createTestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); + const input3 = createTestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); await group.openEditors([{ editor: input1, options: { pinned: true } }, { editor: input2, options: { pinned: true } }, { editor: input3, options: { pinned: true } }]); assert.strictEqual(group.getEditorByIndex(0), input1); @@ -1085,8 +1099,8 @@ suite('EditorGroupsService', () => { const group = part.activeGroup; assert.strictEqual(group.isEmpty, true); - const input = new TestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); - const inputInactive = new TestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID); + const input = createTestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); + const inputInactive = createTestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID); await group.openEditor(input); assert.strictEqual(group.count, 1); @@ -1105,10 +1119,10 @@ suite('EditorGroupsService', () => { const group = part.activeGroup; - const input1 = new TestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); + const input1 = createTestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); input1.dirty = true; - const input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); + const input2 = createTestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); await group.openEditor(input1); assert.strictEqual(group.activeEditor, input1); @@ -1134,10 +1148,10 @@ suite('EditorGroupsService', () => { const group = part.activeGroup; - const input1 = new TestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); + const input1 = createTestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); input1.dirty = true; - const input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); + const input2 = createTestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); await group.openEditor(input1); assert.strictEqual(group.activeEditor, input1); @@ -1158,15 +1172,15 @@ suite('EditorGroupsService', () => { const group = part.activeGroup; assert.strictEqual(group.isEmpty, true); - const input1 = new TestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); - const input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); - const input3 = new TestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); - const input4 = new TestFileEditorInput(URI.file('foo/bar4'), TEST_EDITOR_INPUT_ID); - const input5 = new TestFileEditorInput(URI.file('foo/bar5'), TEST_EDITOR_INPUT_ID); + const input1 = createTestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); + const input2 = createTestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); + const input3 = createTestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); + const input4 = createTestFileEditorInput(URI.file('foo/bar4'), TEST_EDITOR_INPUT_ID); + const input5 = createTestFileEditorInput(URI.file('foo/bar5'), TEST_EDITOR_INPUT_ID); - const input6 = new TestFileEditorInput(URI.file('foo/bar6'), TEST_EDITOR_INPUT_ID); - const input7 = new TestFileEditorInput(URI.file('foo/bar7'), TEST_EDITOR_INPUT_ID); - const input8 = new TestFileEditorInput(URI.file('foo/bar8'), TEST_EDITOR_INPUT_ID); + const input6 = createTestFileEditorInput(URI.file('foo/bar6'), TEST_EDITOR_INPUT_ID); + const input7 = createTestFileEditorInput(URI.file('foo/bar7'), TEST_EDITOR_INPUT_ID); + const input8 = createTestFileEditorInput(URI.file('foo/bar8'), TEST_EDITOR_INPUT_ID); await group.openEditor(input1, { pinned: true }); await group.openEditor(input2, { pinned: true }); @@ -1192,7 +1206,7 @@ suite('EditorGroupsService', () => { const group = part.activeGroup; assert.strictEqual(group.isEmpty, true); - const input = new TestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); + const input = createTestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); const sideBySideInput = instantiationService.createInstance(SideBySideEditorInput, undefined, undefined, input, input); await group.openEditor(input); @@ -1214,11 +1228,11 @@ suite('EditorGroupsService', () => { const group2 = part.addGroup(group, GroupDirection.RIGHT); assert.strictEqual(group.isEmpty, true); - const input1 = new TestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); - const input2 = new TestFileEditorInput(URI.file('foo/bar1'), `${TEST_EDITOR_INPUT_ID}-1`); - const input3 = new TestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); - const input4 = new TestFileEditorInput(URI.file('foo/bar4'), TEST_EDITOR_INPUT_ID); - const input5 = new TestFileEditorInput(URI.file('foo/bar4'), `${TEST_EDITOR_INPUT_ID}-1`); + const input1 = createTestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); + const input2 = createTestFileEditorInput(URI.file('foo/bar1'), `${TEST_EDITOR_INPUT_ID}-1`); + const input3 = createTestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); + const input4 = createTestFileEditorInput(URI.file('foo/bar4'), TEST_EDITOR_INPUT_ID); + const input5 = createTestFileEditorInput(URI.file('foo/bar4'), `${TEST_EDITOR_INPUT_ID}-1`); await group.openEditor(input1, { pinned: true }); await group.openEditor(input2, { pinned: true }); @@ -1240,8 +1254,8 @@ suite('EditorGroupsService', () => { const group = part.activeGroup; assert.strictEqual(group.isEmpty, true); - const secondaryInput = new TestFileEditorInput(URI.file('foo/bar-secondary'), TEST_EDITOR_INPUT_ID); - const primaryInput = new TestFileEditorInput(URI.file('foo/bar-primary'), `${TEST_EDITOR_INPUT_ID}-1`); + const secondaryInput = createTestFileEditorInput(URI.file('foo/bar-secondary'), TEST_EDITOR_INPUT_ID); + const primaryInput = createTestFileEditorInput(URI.file('foo/bar-primary'), `${TEST_EDITOR_INPUT_ID}-1`); const sideBySideEditor = new SideBySideEditorInput(undefined, undefined, secondaryInput, primaryInput, accessor.editorService); await group.openEditor(sideBySideEditor, { pinned: true }); @@ -1351,8 +1365,8 @@ suite('EditorGroupsService', () => { assert.strictEqual(group.getEditors(EditorsOrder.SEQUENTIAL, { excludeSticky: true }).length, 0); assert.strictEqual(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE, { excludeSticky: true }).length, 0); - const input = new TestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); - const inputInactive = new TestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID); + const input = createTestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); + const inputInactive = createTestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID); await group.openEditor(input, { pinned: true }); await group.openEditor(inputInactive, { inactive: true }); @@ -1414,7 +1428,7 @@ suite('EditorGroupsService', () => { assert.strictEqual(group.getEditors(EditorsOrder.SEQUENTIAL, { excludeSticky: true }).length, 1); assert.strictEqual(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE, { excludeSticky: true }).length, 1); - const inputSticky = new TestFileEditorInput(URI.file('foo/bar/sticky'), TEST_EDITOR_INPUT_ID); + const inputSticky = createTestFileEditorInput(URI.file('foo/bar/sticky'), TEST_EDITOR_INPUT_ID); await group.openEditor(inputSticky, { sticky: true }); @@ -1448,9 +1462,9 @@ suite('EditorGroupsService', () => { const rightGroup = part.addGroup(group, GroupDirection.RIGHT); - const input = new TestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); - const inputInactive = new TestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID); - const thirdInput = new TestFileEditorInput(URI.file('foo/bar/third'), TEST_EDITOR_INPUT_ID); + const input = createTestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); + const inputInactive = createTestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID); + const thirdInput = createTestFileEditorInput(URI.file('foo/bar/third'), TEST_EDITOR_INPUT_ID); let leftFiredCount = 0; const leftGroupListener = group.onWillMoveEditor(() => { @@ -1489,9 +1503,9 @@ suite('EditorGroupsService', () => { const rightGroup = part.addGroup(group, GroupDirection.RIGHT); - const input = new TestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); - const secondInput = new TestFileEditorInput(URI.file('foo/bar/second'), TEST_EDITOR_INPUT_ID); - const thirdInput = new TestFileEditorInput(URI.file('foo/bar/third'), TEST_EDITOR_INPUT_ID); + const input = createTestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); + const secondInput = createTestFileEditorInput(URI.file('foo/bar/second'), TEST_EDITOR_INPUT_ID); + const thirdInput = createTestFileEditorInput(URI.file('foo/bar/third'), TEST_EDITOR_INPUT_ID); let leftFiredCount = 0; const leftGroupListener = group.onWillOpenEditor(() => { @@ -1532,8 +1546,8 @@ suite('EditorGroupsService', () => { const moveListener = group.onWillMoveEditor(() => firedCount++); const rightGroup = part.addGroup(group, GroupDirection.RIGHT); - const input = new TestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); - const inputInactive = new TestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID); + const input = createTestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); + const inputInactive = createTestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID); await group.openEditors([{ editor: input, options: { pinned: true } }, { editor: inputInactive }]); assert.strictEqual(firedCount, 0); @@ -1648,8 +1662,8 @@ suite('EditorGroupsService', () => { const rootGroup = part.activeGroup; let rightGroup = part.addGroup(rootGroup, GroupDirection.RIGHT); - let input1 = new TestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); - let input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); + let input1 = createTestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); + let input2 = createTestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); // First editor opens in right group: Locked=true await rightGroup.openEditor(input1, { pinned: true }); @@ -1665,8 +1679,8 @@ suite('EditorGroupsService', () => { part.removeGroup(rightGroup); await rootGroup.closeAllEditors(); - input1 = new TestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); - input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); + input1 = createTestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); + input2 = createTestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); await rootGroup.openEditor(input1, { pinned: true }); assert.strictEqual(rootGroup.isLocked, false); @@ -1677,4 +1691,6 @@ suite('EditorGroupsService', () => { part.removeGroup(leftGroup); assert.strictEqual(rootGroup.isLocked, false); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/services/editor/test/browser/editorService.test.ts b/src/vs/workbench/services/editor/test/browser/editorService.test.ts index ae5ee65a0b7..14e96886f5d 100644 --- a/src/vs/workbench/services/editor/test/browser/editorService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorService.test.ts @@ -8,7 +8,7 @@ import { EditorActivation, IResourceEditorInput } from 'vs/platform/editor/commo import { URI } from 'vs/base/common/uri'; import { Event } from 'vs/base/common/event'; import { DEFAULT_EDITOR_ASSOCIATION, EditorCloseContext, EditorsOrder, IEditorCloseEvent, EditorInputWithOptions, IEditorPane, IResourceDiffEditorInput, isEditorInputWithOptions, IUntitledTextResourceEditorInput, IUntypedEditorInput, SideBySideEditor, isEditorInput, EditorInputCapabilities } from 'vs/workbench/common/editor'; -import { workbenchInstantiationService, TestServiceAccessor, registerTestEditor, TestFileEditorInput, ITestInstantiationService, registerTestResourceEditor, registerTestSideBySideEditor, createEditorPart, registerTestFileEditor, TestTextFileEditor, TestSingletonFileEditorInput } from 'vs/workbench/test/browser/workbenchTestServices'; +import { workbenchInstantiationService, TestServiceAccessor, registerTestEditor, TestFileEditorInput, ITestInstantiationService, registerTestResourceEditor, registerTestSideBySideEditor, createEditorPart, registerTestFileEditor, TestTextFileEditor, TestSingletonFileEditorInput, workbenchTeardown } from 'vs/workbench/test/browser/workbenchTestServices'; import { EditorService } from 'vs/workbench/services/editor/browser/editorService'; import { IEditorGroup, IEditorGroupsService, GroupDirection, GroupsArrangement } from 'vs/workbench/services/editor/common/editorGroupsService'; import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; @@ -20,14 +20,14 @@ import { FileOperationEvent, FileOperation } from 'vs/platform/files/common/file import { DisposableStore } from 'vs/base/common/lifecycle'; import { MockScopableContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; -import { IWorkspaceTrustRequestService, WorkspaceTrustUriResponse } from 'vs/platform/workspace/common/workspaceTrust'; -import { TestWorkspaceTrustRequestService } from 'vs/workbench/services/workspaces/test/common/testWorkspaceTrustService'; +import { WorkspaceTrustUriResponse } from 'vs/platform/workspace/common/workspaceTrust'; import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { ErrorPlaceholderEditor } from 'vs/workbench/browser/parts/editor/editorPlaceholder'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('EditorService', () => { @@ -36,13 +36,20 @@ suite('EditorService', () => { const disposables = new DisposableStore(); + let testLocalInstantiationService: ITestInstantiationService | undefined = undefined; + setup(() => { disposables.add(registerTestEditor(TEST_EDITOR_ID, [new SyncDescriptor(TestFileEditorInput), new SyncDescriptor(TestSingletonFileEditorInput)], TEST_EDITOR_INPUT_ID)); disposables.add(registerTestResourceEditor()); disposables.add(registerTestSideBySideEditor()); }); - teardown(() => { + teardown(async () => { + if (testLocalInstantiationService) { + await workbenchTeardown(testLocalInstantiationService); + testLocalInstantiationService = undefined; + } + disposables.clear(); }); @@ -50,19 +57,23 @@ suite('EditorService', () => { const part = await createEditorPart(instantiationService, disposables); instantiationService.stub(IEditorGroupsService, part); - instantiationService.stub(IWorkspaceTrustRequestService, new TestWorkspaceTrustRequestService(false)); - - const editorService = instantiationService.createInstance(EditorService); + const editorService = disposables.add(instantiationService.createInstance(EditorService)); instantiationService.stub(IEditorService, editorService); + testLocalInstantiationService = instantiationService; + return [part, editorService, instantiationService.createInstance(TestServiceAccessor)]; } + function createTestFileEditorInput(resource: URI, typeId: string): TestFileEditorInput { + return disposables.add(new TestFileEditorInput(resource, typeId)); + } + test('openEditor() - basics', async () => { const [, service] = await createEditorService(); - let input = new TestFileEditorInput(URI.parse('my://resource-basics'), TEST_EDITOR_INPUT_ID); - let otherInput = new TestFileEditorInput(URI.parse('my://resource2-basics'), TEST_EDITOR_INPUT_ID); + let input = createTestFileEditorInput(URI.parse('my://resource-basics'), TEST_EDITOR_INPUT_ID); + let otherInput = createTestFileEditorInput(URI.parse('my://resource2-basics'), TEST_EDITOR_INPUT_ID); let activeEditorChangeEventCounter = 0; const activeEditorChangeListener = service.onDidActiveEditorChange(() => { @@ -119,8 +130,8 @@ suite('EditorService', () => { assert.strictEqual(0, service.count); // Open again 2 inputs (recreate because disposed) - input = new TestFileEditorInput(URI.parse('my://resource-basics'), TEST_EDITOR_INPUT_ID); - otherInput = new TestFileEditorInput(URI.parse('my://resource2-basics'), TEST_EDITOR_INPUT_ID); + input = createTestFileEditorInput(URI.parse('my://resource-basics'), TEST_EDITOR_INPUT_ID); + otherInput = createTestFileEditorInput(URI.parse('my://resource2-basics'), TEST_EDITOR_INPUT_ID); await service.openEditor(input, { pinned: true }); editor = await service.openEditor(otherInput, { pinned: true }); @@ -139,7 +150,7 @@ suite('EditorService', () => { assert.strictEqual(activeEditorChangeEventCounter, 4); assert.strictEqual(visibleEditorChangeEventCounter, 4); - const stickyInput = new TestFileEditorInput(URI.parse('my://resource3-basics'), TEST_EDITOR_INPUT_ID); + const stickyInput = createTestFileEditorInput(URI.parse('my://resource3-basics'), TEST_EDITOR_INPUT_ID); await service.openEditor(stickyInput, { sticky: true }); assert.strictEqual(3, service.count); @@ -168,8 +179,8 @@ suite('EditorService', () => { test('openEditor() - multiple calls are cancelled and indicated as such', async () => { const [, service] = await createEditorService(); - const input = new TestFileEditorInput(URI.parse('my://resource-basics'), TEST_EDITOR_INPUT_ID); - const otherInput = new TestFileEditorInput(URI.parse('my://resource2-basics'), TEST_EDITOR_INPUT_ID); + const input = createTestFileEditorInput(URI.parse('my://resource-basics'), TEST_EDITOR_INPUT_ID); + const otherInput = createTestFileEditorInput(URI.parse('my://resource2-basics'), TEST_EDITOR_INPUT_ID); let activeEditorChangeEventCounter = 0; const activeEditorChangeListener = service.onDidActiveEditorChange(() => { @@ -200,7 +211,7 @@ suite('EditorService', () => { test('openEditor() - same input does not cancel previous one - https://github.com/microsoft/vscode/issues/136684', async () => { const [, service] = await createEditorService(); - let input = new TestFileEditorInput(URI.parse('my://resource-basics'), TEST_EDITOR_INPUT_ID); + let input = createTestFileEditorInput(URI.parse('my://resource-basics'), TEST_EDITOR_INPUT_ID); let editorP1 = service.openEditor(input, { pinned: true }); let editorP2 = service.openEditor(input, { pinned: true }); @@ -214,8 +225,8 @@ suite('EditorService', () => { assert.ok(editor2.group); await editor2.group.closeAllEditors(); - input = new TestFileEditorInput(URI.parse('my://resource-basics'), TEST_EDITOR_INPUT_ID); - const inputSame = new TestFileEditorInput(URI.parse('my://resource-basics'), TEST_EDITOR_INPUT_ID); + input = createTestFileEditorInput(URI.parse('my://resource-basics'), TEST_EDITOR_INPUT_ID); + const inputSame = createTestFileEditorInput(URI.parse('my://resource-basics'), TEST_EDITOR_INPUT_ID); editorP1 = service.openEditor(input, { pinned: true }); editorP2 = service.openEditor(inputSame, { pinned: true }); @@ -230,8 +241,8 @@ suite('EditorService', () => { test('openEditor() - singleton typed editors reveal instead of split', async () => { const [part, service] = await createEditorService(); - const input1 = new TestSingletonFileEditorInput(URI.parse('my://resource-basics1'), TEST_EDITOR_INPUT_ID); - const input2 = new TestSingletonFileEditorInput(URI.parse('my://resource-basics2'), TEST_EDITOR_INPUT_ID); + const input1 = disposables.add(new TestSingletonFileEditorInput(URI.parse('my://resource-basics1'), TEST_EDITOR_INPUT_ID)); + const input2 = disposables.add(new TestSingletonFileEditorInput(URI.parse('my://resource-basics2'), TEST_EDITOR_INPUT_ID)); const input1Group = (await service.openEditor(input1, { pinned: true }))?.group; const input2Group = (await service.openEditor(input2, { pinned: true }, SIDE_GROUP))?.group; @@ -253,7 +264,7 @@ suite('EditorService', () => { { id: TEST_EDITOR_INPUT_ID, label: 'Label', priority: RegisteredEditorPriority.exclusive }, {}, { - createEditorInput: editor => ({ editor: new TestFileEditorInput(editor.resource, TEST_EDITOR_INPUT_ID) }) + createEditorInput: editor => ({ editor: createTestFileEditorInput(editor.resource, TEST_EDITOR_INPUT_ID) }) } )); @@ -391,7 +402,7 @@ suite('EditorService', () => { { id: TEST_EDITOR_INPUT_ID, label: 'Label', priority: RegisteredEditorPriority.exclusive }, {}, { - createEditorInput: editor => ({ editor: new TestFileEditorInput(editor.resource, TEST_EDITOR_INPUT_ID) }) + createEditorInput: editor => ({ editor: createTestFileEditorInput(editor.resource, TEST_EDITOR_INPUT_ID) }) } )); @@ -441,7 +452,7 @@ suite('EditorService', () => { { id: TEST_EDITOR_INPUT_ID, label: 'Label', priority: RegisteredEditorPriority.exclusive }, {}, { - createEditorInput: editor => ({ editor: new TestFileEditorInput(editor.resource, TEST_EDITOR_INPUT_ID) }) + createEditorInput: editor => ({ editor: createTestFileEditorInput(editor.resource, TEST_EDITOR_INPUT_ID) }) } )); @@ -491,7 +502,7 @@ suite('EditorService', () => { { id: TEST_EDITOR_INPUT_ID, label: 'Label', priority: RegisteredEditorPriority.exclusive }, {}, { - createEditorInput: editor => ({ editor: new TestFileEditorInput(editor.resource, TEST_EDITOR_INPUT_ID) }) + createEditorInput: editor => ({ editor: createTestFileEditorInput(editor.resource, TEST_EDITOR_INPUT_ID) }) } )); @@ -563,19 +574,19 @@ suite('EditorService', () => { editorFactoryCalled++; lastEditorFactoryEditor = editor; - return { editor: new TestFileEditorInput(editor.resource, TEST_EDITOR_INPUT_ID) }; + return { editor: createTestFileEditorInput(editor.resource, TEST_EDITOR_INPUT_ID) }; }, createUntitledEditorInput: untitledEditor => { untitledEditorFactoryCalled++; lastUntitledEditorFactoryEditor = untitledEditor; - return { editor: new TestFileEditorInput(untitledEditor.resource ?? URI.parse(`untitled://my-untitled-editor-${untitledEditorFactoryCalled}`), TEST_EDITOR_INPUT_ID) }; + return { editor: createTestFileEditorInput(untitledEditor.resource ?? URI.parse(`untitled://my-untitled-editor-${untitledEditorFactoryCalled}`), TEST_EDITOR_INPUT_ID) }; }, createDiffEditorInput: diffEditor => { diffEditorFactoryCalled++; lastDiffEditorFactoryEditor = diffEditor; - return { editor: new TestFileEditorInput(URI.file(`diff-editor-${diffEditorFactoryCalled}`), TEST_EDITOR_INPUT_ID) }; + return { editor: createTestFileEditorInput(URI.file(`diff-editor-${diffEditorFactoryCalled}`), TEST_EDITOR_INPUT_ID) }; } } )); @@ -589,13 +600,7 @@ suite('EditorService', () => { lastUntitledEditorFactoryEditor = undefined; lastDiffEditorFactoryEditor = undefined; - for (const group of part.groups) { - await group.closeAllEditors(); - } - - for (const group of part.groups) { - accessor.editorGroupService.removeGroup(group); - } + await workbenchTeardown(accessor.instantiationService); rootGroup = part.activeGroup; } @@ -848,7 +853,7 @@ suite('EditorService', () => { { // typed editor, no options, no group { - const typedEditor = new TestFileEditorInput(URI.file('file.editor-service-override-tests'), TEST_EDITOR_INPUT_ID); + const typedEditor = createTestFileEditorInput(URI.file('file.editor-service-override-tests'), TEST_EDITOR_INPUT_ID); const pane = await openEditor({ editor: typedEditor }); let typedInput = pane?.input; @@ -871,7 +876,7 @@ suite('EditorService', () => { assert.strictEqual(pane?.group.activeEditor, typedInput); // replaceEditors should work too - const typedEditorReplacement = new TestFileEditorInput(URI.file('file-replaced.editor-service-override-tests'), TEST_EDITOR_INPUT_ID); + const typedEditorReplacement = createTestFileEditorInput(URI.file('file-replaced.editor-service-override-tests'), TEST_EDITOR_INPUT_ID); await service.replaceEditors([{ editor: typedEditor, replacement: typedEditorReplacement @@ -894,7 +899,7 @@ suite('EditorService', () => { // typed editor, no options, no group { - const typedEditor = new TestFileEditorInput(URI.file('file.editor-service-override-tests'), TEST_EDITOR_INPUT_ID); + const typedEditor = createTestFileEditorInput(URI.file('file.editor-service-override-tests'), TEST_EDITOR_INPUT_ID); const pane = await openEditor({ editor: typedEditor }); const typedInput = pane?.input; @@ -920,7 +925,7 @@ suite('EditorService', () => { // typed editor, options (no override, sticky: true, preserveFocus: true), no group { - const typedEditor = new TestFileEditorInput(URI.file('file.editor-service-override-tests'), TEST_EDITOR_INPUT_ID); + const typedEditor = createTestFileEditorInput(URI.file('file.editor-service-override-tests'), TEST_EDITOR_INPUT_ID); const pane = await openEditor({ editor: typedEditor, options: { sticky: true, preserveFocus: true } }); assert.strictEqual(pane?.group, rootGroup); @@ -942,7 +947,7 @@ suite('EditorService', () => { // typed editor, options (override default), no group { - const typedEditor = new TestFileEditorInput(URI.file('file.editor-service-override-tests'), TEST_EDITOR_INPUT_ID); + const typedEditor = createTestFileEditorInput(URI.file('file.editor-service-override-tests'), TEST_EDITOR_INPUT_ID); const pane = await openEditor({ editor: typedEditor, options: { override: DEFAULT_EDITOR_ASSOCIATION.id } }); assert.strictEqual(pane?.group, rootGroup); @@ -963,7 +968,7 @@ suite('EditorService', () => { // typed editor, options (override: TEST_EDITOR_INPUT_ID), no group { - const typedEditor = new TestFileEditorInput(URI.file('file.editor-service-override-tests'), TEST_EDITOR_INPUT_ID); + const typedEditor = createTestFileEditorInput(URI.file('file.editor-service-override-tests'), TEST_EDITOR_INPUT_ID); const pane = await openEditor({ editor: typedEditor, options: { override: TEST_EDITOR_INPUT_ID } }); assert.strictEqual(pane?.group, rootGroup); @@ -983,7 +988,7 @@ suite('EditorService', () => { // typed editor, options (sticky: true, preserveFocus: true), no group { - const typedEditor = new TestFileEditorInput(URI.file('file.editor-service-override-tests'), TEST_EDITOR_INPUT_ID); + const typedEditor = createTestFileEditorInput(URI.file('file.editor-service-override-tests'), TEST_EDITOR_INPUT_ID); const pane = await openEditor({ editor: typedEditor, options: { sticky: true, preserveFocus: true } }); assert.strictEqual(pane?.group, rootGroup); @@ -1005,7 +1010,7 @@ suite('EditorService', () => { // typed editor, options (override: TEST_EDITOR_INPUT_ID, sticky: true, preserveFocus: true), no group { - const typedEditor = new TestFileEditorInput(URI.file('file.editor-service-override-tests'), TEST_EDITOR_INPUT_ID); + const typedEditor = createTestFileEditorInput(URI.file('file.editor-service-override-tests'), TEST_EDITOR_INPUT_ID); const pane = await openEditor({ editor: typedEditor, options: { sticky: true, preserveFocus: true, override: TEST_EDITOR_INPUT_ID } }); assert.strictEqual(pane?.group, rootGroup); @@ -1027,7 +1032,7 @@ suite('EditorService', () => { // typed editor, no options, SIDE_GROUP { - const typedEditor = new TestFileEditorInput(URI.file('file.editor-service-override-tests'), TEST_EDITOR_INPUT_ID); + const typedEditor = createTestFileEditorInput(URI.file('file.editor-service-override-tests'), TEST_EDITOR_INPUT_ID); const pane = await openEditor({ editor: typedEditor }, SIDE_GROUP); assert.strictEqual(accessor.editorGroupService.groups.length, 2); @@ -1048,7 +1053,7 @@ suite('EditorService', () => { // typed editor, options (no override), SIDE_GROUP { - const typedEditor = new TestFileEditorInput(URI.file('file.editor-service-override-tests'), TEST_EDITOR_INPUT_ID); + const typedEditor = createTestFileEditorInput(URI.file('file.editor-service-override-tests'), TEST_EDITOR_INPUT_ID); const pane = await openEditor({ editor: typedEditor }, SIDE_GROUP); assert.strictEqual(accessor.editorGroupService.groups.length, 2); @@ -1244,7 +1249,7 @@ suite('EditorService', () => { // no options, no group { - const typedEditor = new TestFileEditorInput(URI.file('file.something'), TEST_EDITOR_INPUT_ID); + const typedEditor = createTestFileEditorInput(URI.file('file.something'), TEST_EDITOR_INPUT_ID); const pane = await openEditor({ editor: typedEditor }); assert.strictEqual(pane?.group, rootGroup); @@ -1264,7 +1269,7 @@ suite('EditorService', () => { // no options, SIDE_GROUP { - const typedEditor = new TestFileEditorInput(URI.file('file.something'), TEST_EDITOR_INPUT_ID); + const typedEditor = createTestFileEditorInput(URI.file('file.something'), TEST_EDITOR_INPUT_ID); const pane = await openEditor({ editor: typedEditor }, SIDE_GROUP); assert.strictEqual(accessor.editorGroupService.groups.length, 2); @@ -1289,7 +1294,7 @@ suite('EditorService', () => { // no options, no group { - const typedEditor = new TestFileEditorInput(URI.file('file.something'), TEST_EDITOR_INPUT_ID); + const typedEditor = createTestFileEditorInput(URI.file('file.something'), TEST_EDITOR_INPUT_ID); typedEditor.disableToUntyped = true; const pane = await openEditor({ editor: typedEditor }); @@ -1310,7 +1315,7 @@ suite('EditorService', () => { // no options, SIDE_GROUP { - const typedEditor = new TestFileEditorInput(URI.file('file.something'), TEST_EDITOR_INPUT_ID); + const typedEditor = createTestFileEditorInput(URI.file('file.something'), TEST_EDITOR_INPUT_ID); typedEditor.disableToUntyped = true; const pane = await openEditor({ editor: typedEditor }, SIDE_GROUP); @@ -1338,8 +1343,8 @@ suite('EditorService', () => { { const untypedEditor1: IResourceEditorInput = { resource: URI.file('file1.editor-service-override-tests') }; const untypedEditor2: IResourceEditorInput = { resource: URI.file('file2.editor-service-override-tests') }; - const untypedEditor3: EditorInputWithOptions = { editor: new TestFileEditorInput(URI.file('file3.editor-service-override-tests'), TEST_EDITOR_INPUT_ID) }; - const untypedEditor4: EditorInputWithOptions = { editor: new TestFileEditorInput(URI.file('file4.editor-service-override-tests'), TEST_EDITOR_INPUT_ID) }; + const untypedEditor3: EditorInputWithOptions = { editor: createTestFileEditorInput(URI.file('file3.editor-service-override-tests'), TEST_EDITOR_INPUT_ID) }; + const untypedEditor4: EditorInputWithOptions = { editor: createTestFileEditorInput(URI.file('file4.editor-service-override-tests'), TEST_EDITOR_INPUT_ID) }; const untypedEditor5: IResourceEditorInput = { resource: URI.file('file5.editor-service-override-tests') }; const pane = (await service.openEditors([untypedEditor1, untypedEditor2, untypedEditor3, untypedEditor4, untypedEditor5]))[0]; @@ -1417,13 +1422,13 @@ suite('EditorService', () => { { id: TEST_EDITOR_INPUT_ID, label: 'Label', priority: RegisteredEditorPriority.exclusive }, {}, { - createEditorInput: editor => ({ editor: new TestFileEditorInput(editor.resource, TEST_EDITOR_INPUT_ID) }) + createEditorInput: editor => ({ editor: createTestFileEditorInput(editor.resource, TEST_EDITOR_INPUT_ID) }) } )); // Typed editor - let pane = await service.openEditor(new TestFileEditorInput(URI.parse('my://resource-openEditors'), TEST_EDITOR_INPUT_ID)); - pane = await service.openEditor(new TestFileEditorInput(URI.parse('my://resource-openEditors'), TEST_EDITOR_INPUT_ID), { sticky: true, preserveFocus: true }); + let pane = await service.openEditor(createTestFileEditorInput(URI.parse('my://resource-openEditors'), TEST_EDITOR_INPUT_ID)); + pane = await service.openEditor(createTestFileEditorInput(URI.parse('my://resource-openEditors'), TEST_EDITOR_INPUT_ID), { sticky: true, preserveFocus: true }); assert.strictEqual(pane?.options?.sticky, true); assert.strictEqual(pane?.options?.preserveFocus, true); @@ -1449,8 +1454,8 @@ suite('EditorService', () => { test('isOpen() with side by side editor', async () => { const [part, service] = await createEditorService(); - const input = new TestFileEditorInput(URI.parse('my://resource-openEditors'), TEST_EDITOR_INPUT_ID); - const otherInput = new TestFileEditorInput(URI.parse('my://resource2-openEditors'), TEST_EDITOR_INPUT_ID); + const input = createTestFileEditorInput(URI.parse('my://resource-openEditors'), TEST_EDITOR_INPUT_ID); + const otherInput = createTestFileEditorInput(URI.parse('my://resource2-openEditors'), TEST_EDITOR_INPUT_ID); const sideBySideInput = new SideBySideEditorInput('sideBySide', '', input, otherInput, service); const editor1 = await service.openEditor(sideBySideInput, { pinned: true }); @@ -1488,9 +1493,9 @@ suite('EditorService', () => { test('openEditors() / replaceEditors()', async () => { const [part, service] = await createEditorService(); - const input = new TestFileEditorInput(URI.parse('my://resource-openEditors'), TEST_EDITOR_INPUT_ID); - const otherInput = new TestFileEditorInput(URI.parse('my://resource2-openEditors'), TEST_EDITOR_INPUT_ID); - const replaceInput = new TestFileEditorInput(URI.parse('my://resource3-openEditors'), TEST_EDITOR_INPUT_ID); + const input = createTestFileEditorInput(URI.parse('my://resource-openEditors'), TEST_EDITOR_INPUT_ID); + const otherInput = createTestFileEditorInput(URI.parse('my://resource2-openEditors'), TEST_EDITOR_INPUT_ID); + const replaceInput = createTestFileEditorInput(URI.parse('my://resource3-openEditors'), TEST_EDITOR_INPUT_ID); // Open editors await service.openEditors([{ editor: input }, { editor: otherInput }]); @@ -1505,11 +1510,11 @@ suite('EditorService', () => { test('openEditors() handles workspace trust (typed editors)', async () => { const [part, service, accessor] = await createEditorService(); - const input1 = new TestFileEditorInput(URI.parse('my://resource1-openEditors'), TEST_EDITOR_INPUT_ID); - const input2 = new TestFileEditorInput(URI.parse('my://resource2-openEditors'), TEST_EDITOR_INPUT_ID); + const input1 = createTestFileEditorInput(URI.parse('my://resource1-openEditors'), TEST_EDITOR_INPUT_ID); + const input2 = createTestFileEditorInput(URI.parse('my://resource2-openEditors'), TEST_EDITOR_INPUT_ID); - const input3 = new TestFileEditorInput(URI.parse('my://resource3-openEditors'), TEST_EDITOR_INPUT_ID); - const input4 = new TestFileEditorInput(URI.parse('my://resource4-openEditors'), TEST_EDITOR_INPUT_ID); + const input3 = createTestFileEditorInput(URI.parse('my://resource3-openEditors'), TEST_EDITOR_INPUT_ID); + const input4 = createTestFileEditorInput(URI.parse('my://resource4-openEditors'), TEST_EDITOR_INPUT_ID); const sideBySideInput = new SideBySideEditorInput('side by side', undefined, input3, input4, service); const oldHandler = accessor.workspaceTrustRequestService.requestOpenUrisHandler; @@ -1550,11 +1555,11 @@ suite('EditorService', () => { test('openEditors() ignores trust when `validateTrust: false', async () => { const [part, service, accessor] = await createEditorService(); - const input1 = new TestFileEditorInput(URI.parse('my://resource1-openEditors'), TEST_EDITOR_INPUT_ID); - const input2 = new TestFileEditorInput(URI.parse('my://resource2-openEditors'), TEST_EDITOR_INPUT_ID); + const input1 = createTestFileEditorInput(URI.parse('my://resource1-openEditors'), TEST_EDITOR_INPUT_ID); + const input2 = createTestFileEditorInput(URI.parse('my://resource2-openEditors'), TEST_EDITOR_INPUT_ID); - const input3 = new TestFileEditorInput(URI.parse('my://resource3-openEditors'), TEST_EDITOR_INPUT_ID); - const input4 = new TestFileEditorInput(URI.parse('my://resource4-openEditors'), TEST_EDITOR_INPUT_ID); + const input3 = createTestFileEditorInput(URI.parse('my://resource3-openEditors'), TEST_EDITOR_INPUT_ID); + const input4 = createTestFileEditorInput(URI.parse('my://resource4-openEditors'), TEST_EDITOR_INPUT_ID); const sideBySideInput = new SideBySideEditorInput('side by side', undefined, input3, input4, service); const oldHandler = accessor.workspaceTrustRequestService.requestOpenUrisHandler; @@ -1574,7 +1579,7 @@ suite('EditorService', () => { test('openEditors() extracts proper resources from untyped editors for workspace trust', async () => { const [, service, accessor] = await createEditorService(); - const input = { resource: URI.parse('my://resource-openEditors') }; + const input = { resource: URI.file('resource-openEditors') }; const otherInput: IResourceDiffEditorInput = { original: { resource: URI.parse('my://resource2-openEditors') }, modified: { resource: URI.parse('my://resource3-openEditors') } @@ -1602,7 +1607,7 @@ suite('EditorService', () => { test('close editor does not dispose when editor opened in other group', async () => { const [part, service] = await createEditorService(); - const input = new TestFileEditorInput(URI.parse('my://resource-close1'), TEST_EDITOR_INPUT_ID); + const input = createTestFileEditorInput(URI.parse('my://resource-close1'), TEST_EDITOR_INPUT_ID); const rootGroup = part.activeGroup; const rightGroup = part.addGroup(rootGroup, GroupDirection.RIGHT); @@ -1627,8 +1632,8 @@ suite('EditorService', () => { test('open to the side', async () => { const [part, service] = await createEditorService(); - const input1 = new TestFileEditorInput(URI.parse('my://resource1-openside'), TEST_EDITOR_INPUT_ID); - const input2 = new TestFileEditorInput(URI.parse('my://resource2-openside'), TEST_EDITOR_INPUT_ID); + const input1 = createTestFileEditorInput(URI.parse('my://resource1-openside'), TEST_EDITOR_INPUT_ID); + const input2 = createTestFileEditorInput(URI.parse('my://resource2-openside'), TEST_EDITOR_INPUT_ID); const rootGroup = part.activeGroup; @@ -1655,8 +1660,8 @@ suite('EditorService', () => { test('editor group activation', async () => { const [part, service] = await createEditorService(); - const input1 = new TestFileEditorInput(URI.parse('my://resource1-openside'), TEST_EDITOR_INPUT_ID); - const input2 = new TestFileEditorInput(URI.parse('my://resource2-openside'), TEST_EDITOR_INPUT_ID); + const input1 = createTestFileEditorInput(URI.parse('my://resource1-openside'), TEST_EDITOR_INPUT_ID); + const input2 = createTestFileEditorInput(URI.parse('my://resource2-openside'), TEST_EDITOR_INPUT_ID); const rootGroup = part.activeGroup; @@ -1686,8 +1691,8 @@ suite('EditorService', () => { test('inactive editor group does not activate when closing editor (#117686)', async () => { const [part, service] = await createEditorService(); - const input1 = new TestFileEditorInput(URI.parse('my://resource1-openside'), TEST_EDITOR_INPUT_ID); - const input2 = new TestFileEditorInput(URI.parse('my://resource2-openside'), TEST_EDITOR_INPUT_ID); + const input1 = createTestFileEditorInput(URI.parse('my://resource1-openside'), TEST_EDITOR_INPUT_ID); + const input2 = createTestFileEditorInput(URI.parse('my://resource2-openside'), TEST_EDITOR_INPUT_ID); const rootGroup = part.activeGroup; @@ -1710,8 +1715,8 @@ suite('EditorService', () => { test('active editor change / visible editor change events', async function () { const [part, service] = await createEditorService(); - let input = new TestFileEditorInput(URI.parse('my://resource-active'), TEST_EDITOR_INPUT_ID); - let otherInput = new TestFileEditorInput(URI.parse('my://resource2-active'), TEST_EDITOR_INPUT_ID); + let input = createTestFileEditorInput(URI.parse('my://resource-active'), TEST_EDITOR_INPUT_ID); + let otherInput = createTestFileEditorInput(URI.parse('my://resource2-active'), TEST_EDITOR_INPUT_ID); let activeEditorChangeEventFired = false; const activeEditorChangeListener = service.onDidActiveEditorChange(() => { @@ -1761,8 +1766,8 @@ suite('EditorService', () => { assertVisibleEditorsChangedEvent(true); // 2.) open, open same (forced open) (recreate inputs that got disposed) - input = new TestFileEditorInput(URI.parse('my://resource-active'), TEST_EDITOR_INPUT_ID); - otherInput = new TestFileEditorInput(URI.parse('my://resource2-active'), TEST_EDITOR_INPUT_ID); + input = createTestFileEditorInput(URI.parse('my://resource-active'), TEST_EDITOR_INPUT_ID); + otherInput = createTestFileEditorInput(URI.parse('my://resource2-active'), TEST_EDITOR_INPUT_ID); editor = await service.openEditor(input); assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); @@ -1774,8 +1779,8 @@ suite('EditorService', () => { await closeEditorAndWaitForNextToOpen(group, input); // 3.) open, open inactive, close (recreate inputs that got disposed) - input = new TestFileEditorInput(URI.parse('my://resource-active'), TEST_EDITOR_INPUT_ID); - otherInput = new TestFileEditorInput(URI.parse('my://resource2-active'), TEST_EDITOR_INPUT_ID); + input = createTestFileEditorInput(URI.parse('my://resource-active'), TEST_EDITOR_INPUT_ID); + otherInput = createTestFileEditorInput(URI.parse('my://resource2-active'), TEST_EDITOR_INPUT_ID); editor = await service.openEditor(input, { pinned: true }); assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); @@ -1789,8 +1794,8 @@ suite('EditorService', () => { assertVisibleEditorsChangedEvent(true); // 4.) open, open inactive, close inactive (recreate inputs that got disposed) - input = new TestFileEditorInput(URI.parse('my://resource-active'), TEST_EDITOR_INPUT_ID); - otherInput = new TestFileEditorInput(URI.parse('my://resource2-active'), TEST_EDITOR_INPUT_ID); + input = createTestFileEditorInput(URI.parse('my://resource-active'), TEST_EDITOR_INPUT_ID); + otherInput = createTestFileEditorInput(URI.parse('my://resource2-active'), TEST_EDITOR_INPUT_ID); editor = await service.openEditor(input, { pinned: true }); assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); @@ -1808,8 +1813,8 @@ suite('EditorService', () => { assertVisibleEditorsChangedEvent(true); // 5.) add group, remove group (recreate inputs that got disposed) - input = new TestFileEditorInput(URI.parse('my://resource-active'), TEST_EDITOR_INPUT_ID); - otherInput = new TestFileEditorInput(URI.parse('my://resource2-active'), TEST_EDITOR_INPUT_ID); + input = createTestFileEditorInput(URI.parse('my://resource-active'), TEST_EDITOR_INPUT_ID); + otherInput = createTestFileEditorInput(URI.parse('my://resource2-active'), TEST_EDITOR_INPUT_ID); editor = await service.openEditor(input, { pinned: true }); assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); @@ -1831,8 +1836,8 @@ suite('EditorService', () => { assertVisibleEditorsChangedEvent(true); // 6.) open editor in inactive group (recreate inputs that got disposed) - input = new TestFileEditorInput(URI.parse('my://resource-active'), TEST_EDITOR_INPUT_ID); - otherInput = new TestFileEditorInput(URI.parse('my://resource2-active'), TEST_EDITOR_INPUT_ID); + input = createTestFileEditorInput(URI.parse('my://resource-active'), TEST_EDITOR_INPUT_ID); + otherInput = createTestFileEditorInput(URI.parse('my://resource2-active'), TEST_EDITOR_INPUT_ID); editor = await service.openEditor(input, { pinned: true }); assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); @@ -1854,8 +1859,8 @@ suite('EditorService', () => { assertVisibleEditorsChangedEvent(true); // 7.) activate group (recreate inputs that got disposed) - input = new TestFileEditorInput(URI.parse('my://resource-active'), TEST_EDITOR_INPUT_ID); - otherInput = new TestFileEditorInput(URI.parse('my://resource2-active'), TEST_EDITOR_INPUT_ID); + input = createTestFileEditorInput(URI.parse('my://resource-active'), TEST_EDITOR_INPUT_ID); + otherInput = createTestFileEditorInput(URI.parse('my://resource2-active'), TEST_EDITOR_INPUT_ID); editor = await service.openEditor(input, { pinned: true }); assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); @@ -1881,8 +1886,8 @@ suite('EditorService', () => { assertVisibleEditorsChangedEvent(true); // 8.) move editor (recreate inputs that got disposed) - input = new TestFileEditorInput(URI.parse('my://resource-active'), TEST_EDITOR_INPUT_ID); - otherInput = new TestFileEditorInput(URI.parse('my://resource2-active'), TEST_EDITOR_INPUT_ID); + input = createTestFileEditorInput(URI.parse('my://resource-active'), TEST_EDITOR_INPUT_ID); + otherInput = createTestFileEditorInput(URI.parse('my://resource2-active'), TEST_EDITOR_INPUT_ID); editor = await service.openEditor(input, { pinned: true }); assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); @@ -1900,8 +1905,8 @@ suite('EditorService', () => { assertVisibleEditorsChangedEvent(true); // 9.) close editor in inactive group (recreate inputs that got disposed) - input = new TestFileEditorInput(URI.parse('my://resource-active'), TEST_EDITOR_INPUT_ID); - otherInput = new TestFileEditorInput(URI.parse('my://resource2-active'), TEST_EDITOR_INPUT_ID); + input = createTestFileEditorInput(URI.parse('my://resource-active'), TEST_EDITOR_INPUT_ID); + otherInput = createTestFileEditorInput(URI.parse('my://resource2-active'), TEST_EDITOR_INPUT_ID); editor = await service.openEditor(input, { pinned: true }); assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); @@ -1927,8 +1932,8 @@ suite('EditorService', () => { const [part, service] = await createEditorService(); const rootGroup = part.activeGroup; - let input = new TestFileEditorInput(URI.parse('my://resource-active'), TEST_EDITOR_INPUT_ID); - let otherInput = new TestFileEditorInput(URI.parse('my://resource2-active'), TEST_EDITOR_INPUT_ID); + let input = createTestFileEditorInput(URI.parse('my://resource-active'), TEST_EDITOR_INPUT_ID); + let otherInput = createTestFileEditorInput(URI.parse('my://resource2-active'), TEST_EDITOR_INPUT_ID); let editorsChangeEventCounter = 0; async function assertEditorsChangeEvent(fn: () => Promise, expected: number) { @@ -1952,8 +1957,8 @@ suite('EditorService', () => { // close (active) await assertEditorsChangeEvent(() => rootGroup.closeEditor(otherInput), 4); - input = new TestFileEditorInput(URI.parse('my://resource-active'), TEST_EDITOR_INPUT_ID); - otherInput = new TestFileEditorInput(URI.parse('my://resource2-active'), TEST_EDITOR_INPUT_ID); + input = createTestFileEditorInput(URI.parse('my://resource-active'), TEST_EDITOR_INPUT_ID); + otherInput = createTestFileEditorInput(URI.parse('my://resource2-active'), TEST_EDITOR_INPUT_ID); // open editors await assertEditorsChangeEvent(() => service.openEditors([{ editor: input, options: { pinned: true } }, { editor: otherInput, options: { pinned: true } }]), 5); @@ -1974,7 +1979,7 @@ suite('EditorService', () => { test('two active editor change events when opening editor to the side', async function () { const [, service] = await createEditorService(); - const input = new TestFileEditorInput(URI.parse('my://resource-active'), TEST_EDITOR_INPUT_ID); + const input = createTestFileEditorInput(URI.parse('my://resource-active'), TEST_EDITOR_INPUT_ID); let activeEditorChangeEvents = 0; const activeEditorChangeListener = service.onDidActiveEditorChange(() => { @@ -2018,8 +2023,8 @@ suite('EditorService', () => { test('openEditor returns undefined when inactive', async function () { const [, service] = await createEditorService(); - const input = new TestFileEditorInput(URI.parse('my://resource-active'), TEST_EDITOR_INPUT_ID); - const otherInput = new TestFileEditorInput(URI.parse('my://resource2-inactive'), TEST_EDITOR_INPUT_ID); + const input = createTestFileEditorInput(URI.parse('my://resource-active'), TEST_EDITOR_INPUT_ID); + const otherInput = createTestFileEditorInput(URI.parse('my://resource2-inactive'), TEST_EDITOR_INPUT_ID); const editor = await service.openEditor(input, { pinned: true }); assert.ok(editor); @@ -2031,7 +2036,7 @@ suite('EditorService', () => { test('openEditor shows placeholder when opening fails', async function () { const [, service] = await createEditorService(); - const failingInput = new TestFileEditorInput(URI.parse('my://resource-failing'), TEST_EDITOR_INPUT_ID); + const failingInput = createTestFileEditorInput(URI.parse('my://resource-failing'), TEST_EDITOR_INPUT_ID); failingInput.setFailToOpen(); const failingEditor = await service.openEditor(failingInput); @@ -2041,8 +2046,8 @@ suite('EditorService', () => { test('openEditor shows placeholder when restoring fails', async function () { const [, service] = await createEditorService(); - const input = new TestFileEditorInput(URI.parse('my://resource-active'), TEST_EDITOR_INPUT_ID); - const failingInput = new TestFileEditorInput(URI.parse('my://resource-failing'), TEST_EDITOR_INPUT_ID); + const input = createTestFileEditorInput(URI.parse('my://resource-active'), TEST_EDITOR_INPUT_ID); + const failingInput = createTestFileEditorInput(URI.parse('my://resource-failing'), TEST_EDITOR_INPUT_ID); await service.openEditor(input, { pinned: true }); await service.openEditor(failingInput, { inactive: true }); @@ -2055,11 +2060,11 @@ suite('EditorService', () => { test('save, saveAll, revertAll', async function () { const [part, service] = await createEditorService(); - const input1 = new TestFileEditorInput(URI.parse('my://resource1'), TEST_EDITOR_INPUT_ID); + const input1 = createTestFileEditorInput(URI.parse('my://resource1'), TEST_EDITOR_INPUT_ID); input1.dirty = true; - const input2 = new TestFileEditorInput(URI.parse('my://resource2'), TEST_EDITOR_INPUT_ID); + const input2 = createTestFileEditorInput(URI.parse('my://resource2'), TEST_EDITOR_INPUT_ID); input2.dirty = true; - const sameInput1 = new TestFileEditorInput(URI.parse('my://resource1'), TEST_EDITOR_INPUT_ID); + const sameInput1 = createTestFileEditorInput(URI.parse('my://resource1'), TEST_EDITOR_INPUT_ID); sameInput1.dirty = true; const rootGroup = part.activeGroup; @@ -2137,11 +2142,11 @@ suite('EditorService', () => { test('saveAll, revertAll (sticky editor)', async function () { const [, service] = await createEditorService(); - const input1 = new TestFileEditorInput(URI.parse('my://resource1'), TEST_EDITOR_INPUT_ID); + const input1 = createTestFileEditorInput(URI.parse('my://resource1'), TEST_EDITOR_INPUT_ID); input1.dirty = true; - const input2 = new TestFileEditorInput(URI.parse('my://resource2'), TEST_EDITOR_INPUT_ID); + const input2 = createTestFileEditorInput(URI.parse('my://resource2'), TEST_EDITOR_INPUT_ID); input2.dirty = true; - const sameInput1 = new TestFileEditorInput(URI.parse('my://resource1'), TEST_EDITOR_INPUT_ID); + const sameInput1 = createTestFileEditorInput(URI.parse('my://resource1'), TEST_EDITOR_INPUT_ID); sameInput1.dirty = true; await service.openEditor(input1, { pinned: true, sticky: true }); @@ -2189,12 +2194,12 @@ suite('EditorService', () => { async function testSaveRevertUntitled(options: IBaseSaveRevertAllEditorOptions, expectUntitled: boolean, expectScratchpad: boolean) { const [, service] = await createEditorService(); - const input1 = new TestFileEditorInput(URI.parse('my://resource1'), TEST_EDITOR_INPUT_ID); + const input1 = createTestFileEditorInput(URI.parse('my://resource1'), TEST_EDITOR_INPUT_ID); input1.dirty = true; - const untitledInput = new TestFileEditorInput(URI.parse('my://resource2'), TEST_EDITOR_INPUT_ID); + const untitledInput = createTestFileEditorInput(URI.parse('my://resource2'), TEST_EDITOR_INPUT_ID); untitledInput.dirty = true; untitledInput.capabilities = EditorInputCapabilities.Untitled; - const scratchpadInput = new TestFileEditorInput(URI.parse('my://resource3'), TEST_EDITOR_INPUT_ID); + const scratchpadInput = createTestFileEditorInput(URI.parse('my://resource3'), TEST_EDITOR_INPUT_ID); scratchpadInput.modified = true; scratchpadInput.capabilities = EditorInputCapabilities.Scratchpad | EditorInputCapabilities.Untitled; @@ -2239,9 +2244,9 @@ suite('EditorService', () => { async function testFileDeleteEditorClose(dirty: boolean): Promise { const [part, service, accessor] = await createEditorService(); - const input1 = new TestFileEditorInput(URI.parse('my://resource1'), TEST_EDITOR_INPUT_ID); + const input1 = createTestFileEditorInput(URI.parse('my://resource1'), TEST_EDITOR_INPUT_ID); input1.dirty = dirty; - const input2 = new TestFileEditorInput(URI.parse('my://resource2'), TEST_EDITOR_INPUT_ID); + const input2 = createTestFileEditorInput(URI.parse('my://resource2'), TEST_EDITOR_INPUT_ID); input2.dirty = dirty; const rootGroup = part.activeGroup; @@ -2267,8 +2272,8 @@ suite('EditorService', () => { test('file move asks input to move', async function () { const [part, service, accessor] = await createEditorService(); - const input1 = new TestFileEditorInput(URI.parse('my://resource1'), TEST_EDITOR_INPUT_ID); - const movedInput = new TestFileEditorInput(URI.parse('my://resource2'), TEST_EDITOR_INPUT_ID); + const input1 = createTestFileEditorInput(URI.parse('my://resource1'), TEST_EDITOR_INPUT_ID); + const movedInput = createTestFileEditorInput(URI.parse('my://resource2'), TEST_EDITOR_INPUT_ID); input1.movedEditor = { editor: movedInput }; const rootGroup = part.activeGroup; @@ -2302,8 +2307,8 @@ suite('EditorService', () => { test('file watcher gets installed for out of workspace files', async function () { const [, service, accessor] = await createEditorService(); - const input1 = new TestFileEditorInput(URI.parse('file://resource1'), TEST_EDITOR_INPUT_ID); - const input2 = new TestFileEditorInput(URI.parse('file://resource2'), TEST_EDITOR_INPUT_ID); + const input1 = createTestFileEditorInput(URI.parse('file://resource1'), TEST_EDITOR_INPUT_ID); + const input2 = createTestFileEditorInput(URI.parse('file://resource2'), TEST_EDITOR_INPUT_ID); await service.openEditor(input1, { pinned: true }); assert.strictEqual(accessor.fileService.watches.length, 1); @@ -2321,8 +2326,8 @@ suite('EditorService', () => { const instantiationService = workbenchInstantiationService({ contextKeyService: instantiationService => instantiationService.createInstance(MockScopableContextKeyService) }, disposables); const [part, service] = await createEditorService(instantiationService); - const input1 = new TestFileEditorInput(URI.parse('file://resource1'), TEST_EDITOR_INPUT_ID); - new TestFileEditorInput(URI.parse('file://resource2'), TEST_EDITOR_INPUT_ID); + const input1 = createTestFileEditorInput(URI.parse('file://resource1'), TEST_EDITOR_INPUT_ID); + createTestFileEditorInput(URI.parse('file://resource2'), TEST_EDITOR_INPUT_ID); await service.openEditor(input1, { pinned: true }); @@ -2401,10 +2406,10 @@ suite('EditorService', () => { ); assert.strictEqual(editorCount, 0); - const input1 = new TestFileEditorInput(URI.parse('file://test/path/resource1.txt'), TEST_EDITOR_INPUT_ID).toUntyped(); - const input2 = new TestFileEditorInput(URI.parse('file://test/path/resource2.txt'), TEST_EDITOR_INPUT_ID).toUntyped(); - const input3 = new TestFileEditorInput(URI.parse('file://test/path/resource3.md'), TEST_EDITOR_INPUT_ID).toUntyped(); - const input4 = new TestFileEditorInput(URI.parse('file://test/path/resource4.md'), TEST_EDITOR_INPUT_ID).toUntyped(); + const input1 = createTestFileEditorInput(URI.parse('file://test/path/resource1.txt'), TEST_EDITOR_INPUT_ID).toUntyped(); + const input2 = createTestFileEditorInput(URI.parse('file://test/path/resource2.txt'), TEST_EDITOR_INPUT_ID).toUntyped(); + const input3 = createTestFileEditorInput(URI.parse('file://test/path/resource3.md'), TEST_EDITOR_INPUT_ID).toUntyped(); + const input4 = createTestFileEditorInput(URI.parse('file://test/path/resource4.md'), TEST_EDITOR_INPUT_ID).toUntyped(); assert.ok(input1); assert.ok(input2); @@ -2446,7 +2451,7 @@ suite('EditorService', () => { assert.strictEqual(editorCount, 0); - const input1 = new TestFileEditorInput(URI.parse('file://test/path/resource2.md'), TEST_EDITOR_INPUT_ID); + const input1 = createTestFileEditorInput(URI.parse('file://test/path/resource2.md'), TEST_EDITOR_INPUT_ID); const untypedInput1 = input1.toUntyped(); assert.ok(untypedInput1); @@ -2466,8 +2471,8 @@ suite('EditorService', () => { test('closeEditor', async () => { const [part, service] = await createEditorService(); - const input = new TestFileEditorInput(URI.parse('my://resource-openEditors'), TEST_EDITOR_INPUT_ID); - const otherInput = new TestFileEditorInput(URI.parse('my://resource2-openEditors'), TEST_EDITOR_INPUT_ID); + const input = createTestFileEditorInput(URI.parse('my://resource-openEditors'), TEST_EDITOR_INPUT_ID); + const otherInput = createTestFileEditorInput(URI.parse('my://resource2-openEditors'), TEST_EDITOR_INPUT_ID); // Open editors await service.openEditors([{ editor: input }, { editor: otherInput }]); @@ -2490,8 +2495,8 @@ suite('EditorService', () => { test('closeEditors', async () => { const [part, service] = await createEditorService(); - const input = new TestFileEditorInput(URI.parse('my://resource-openEditors'), TEST_EDITOR_INPUT_ID); - const otherInput = new TestFileEditorInput(URI.parse('my://resource2-openEditors'), TEST_EDITOR_INPUT_ID); + const input = createTestFileEditorInput(URI.parse('my://resource-openEditors'), TEST_EDITOR_INPUT_ID); + const otherInput = createTestFileEditorInput(URI.parse('my://resource2-openEditors'), TEST_EDITOR_INPUT_ID); // Open editors await service.openEditors([{ editor: input }, { editor: otherInput }]); @@ -2505,8 +2510,8 @@ suite('EditorService', () => { test('findEditors (in group)', async () => { const [part, service] = await createEditorService(); - const input = new TestFileEditorInput(URI.parse('my://resource-openEditors'), TEST_EDITOR_INPUT_ID); - const otherInput = new TestFileEditorInput(URI.parse('my://resource2-openEditors'), TEST_EDITOR_INPUT_ID); + const input = createTestFileEditorInput(URI.parse('my://resource-openEditors'), TEST_EDITOR_INPUT_ID); + const otherInput = createTestFileEditorInput(URI.parse('my://resource2-openEditors'), TEST_EDITOR_INPUT_ID); // Open editors await service.openEditors([{ editor: input }, { editor: otherInput }]); @@ -2541,7 +2546,7 @@ suite('EditorService', () => { // Make sure we don't find editors across groups { - const newEditor = await service.openEditor(new TestFileEditorInput(URI.parse('my://other-group-resource'), TEST_EDITOR_INPUT_ID), { pinned: true, preserveFocus: true }, SIDE_GROUP); + const newEditor = await service.openEditor(createTestFileEditorInput(URI.parse('my://other-group-resource'), TEST_EDITOR_INPUT_ID), { pinned: true, preserveFocus: true }, SIDE_GROUP); const found1 = service.findEditors(input.resource, undefined, newEditor!.group!.id); assert.strictEqual(found1.length, 0); @@ -2566,8 +2571,8 @@ suite('EditorService', () => { const rootGroup = part.activeGroup; - const input = new TestFileEditorInput(URI.parse('my://resource-openEditors'), TEST_EDITOR_INPUT_ID); - const otherInput = new TestFileEditorInput(URI.parse('my://resource2-openEditors'), TEST_EDITOR_INPUT_ID); + const input = createTestFileEditorInput(URI.parse('my://resource-openEditors'), TEST_EDITOR_INPUT_ID); + const otherInput = createTestFileEditorInput(URI.parse('my://resource2-openEditors'), TEST_EDITOR_INPUT_ID); // Open editors await service.openEditors([{ editor: input }, { editor: otherInput }]); @@ -2625,8 +2630,8 @@ suite('EditorService', () => { test('findEditors (support side by side via options)', async () => { const [, service] = await createEditorService(); - const secondaryInput = new TestFileEditorInput(URI.parse('my://resource-findEditors-secondary'), TEST_EDITOR_INPUT_ID); - const primaryInput = new TestFileEditorInput(URI.parse('my://resource-findEditors-primary'), TEST_EDITOR_INPUT_ID); + const secondaryInput = createTestFileEditorInput(URI.parse('my://resource-findEditors-secondary'), TEST_EDITOR_INPUT_ID); + const primaryInput = createTestFileEditorInput(URI.parse('my://resource-findEditors-primary'), TEST_EDITOR_INPUT_ID); const sideBySideInput = new SideBySideEditorInput(undefined, undefined, secondaryInput, primaryInput, service); @@ -2659,8 +2664,8 @@ suite('EditorService', () => { const rootGroup = part.activeGroup; - const input = new TestFileEditorInput(URI.parse('my://resource-openEditors'), TEST_EDITOR_INPUT_ID); - const otherInput = new TestFileEditorInput(URI.parse('my://resource2-openEditors'), TEST_EDITOR_INPUT_ID); + const input = createTestFileEditorInput(URI.parse('my://resource-openEditors'), TEST_EDITOR_INPUT_ID); + const otherInput = createTestFileEditorInput(URI.parse('my://resource2-openEditors'), TEST_EDITOR_INPUT_ID); const sideBySideInput = new SideBySideEditorInput(undefined, undefined, input, input, service); const otherSideBySideInput = new SideBySideEditorInput(undefined, undefined, otherInput, otherInput, service); @@ -2678,8 +2683,8 @@ suite('EditorService', () => { const rootGroup = part.activeGroup; - const input1 = new TestFileEditorInput(URI.parse('my://resource-onDidCloseEditor1'), TEST_EDITOR_INPUT_ID); - const input2 = new TestFileEditorInput(URI.parse('my://resource-onDidCloseEditor2'), TEST_EDITOR_INPUT_ID); + const input1 = createTestFileEditorInput(URI.parse('my://resource-onDidCloseEditor1'), TEST_EDITOR_INPUT_ID); + const input2 = createTestFileEditorInput(URI.parse('my://resource-onDidCloseEditor2'), TEST_EDITOR_INPUT_ID); await service.openEditor(input1, { pinned: true }); await service.openEditor(input2, { pinned: true }); @@ -2687,9 +2692,9 @@ suite('EditorService', () => { const sidegroup = part.addGroup(rootGroup, GroupDirection.RIGHT); const events: IEditorCloseEvent[] = []; - service.onDidCloseEditor(e => { + disposables.add(service.onDidCloseEditor(e => { events.push(e); - }); + })); rootGroup.moveEditor(input1, sidegroup); @@ -2705,18 +2710,20 @@ suite('EditorService', () => { const rootGroup = part.activeGroup; - const input1 = new TestFileEditorInput(URI.parse('my://resource-onDidCloseEditor1'), TEST_EDITOR_INPUT_ID); - const input2 = new TestFileEditorInput(URI.parse('my://resource-onDidCloseEditor2'), TEST_EDITOR_INPUT_ID); + const input1 = createTestFileEditorInput(URI.parse('my://resource-onDidCloseEditor1'), TEST_EDITOR_INPUT_ID); + const input2 = createTestFileEditorInput(URI.parse('my://resource-onDidCloseEditor2'), TEST_EDITOR_INPUT_ID); await service.openEditor(input1, { pinned: true }); const events: IEditorCloseEvent[] = []; - service.onDidCloseEditor(e => { + disposables.add(service.onDidCloseEditor(e => { events.push(e); - }); + })); await rootGroup.replaceEditors([{ editor: input1, replacement: input2 }]); assert.strictEqual(events[0].context, EditorCloseContext.REPLACE); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts b/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts index 2ff788fd408..d2f72d46b93 100644 --- a/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts @@ -19,6 +19,7 @@ import { timeout } from 'vs/base/common/async'; import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('EditorsObserver', function () { @@ -51,7 +52,7 @@ suite('EditorsObserver', function () { async function createEditorObserver(): Promise<[EditorPart, EditorsObserver, IInstantiationService]> { const [part, instantiationService] = await createPart(); - const observer = disposables.add(new EditorsObserver(part, new TestStorageService())); + const observer = disposables.add(new EditorsObserver(part, disposables.add(new TestStorageService()))); return [part, observer, instantiationService]; } @@ -60,9 +61,9 @@ suite('EditorsObserver', function () { const [part, observer] = await createEditorObserver(); let onDidMostRecentlyActiveEditorsChangeCalled = false; - const listener = observer.onDidMostRecentlyActiveEditorsChange(() => { + disposables.add(observer.onDidMostRecentlyActiveEditorsChange(() => { onDidMostRecentlyActiveEditorsChangeCalled = true; - }); + })); let currentEditorsMRU = observer.editors; assert.strictEqual(currentEditorsMRU.length, 0); @@ -135,8 +136,6 @@ suite('EditorsObserver', function () { assert.strictEqual(observer.hasEditor({ resource: input1.resource, typeId: input1.typeId, editorId: input1.editorId }), false); assert.strictEqual(observer.hasEditor({ resource: input2.resource, typeId: input2.typeId, editorId: input2.editorId }), false); assert.strictEqual(observer.hasEditor({ resource: input3.resource, typeId: input3.typeId, editorId: input3.editorId }), false); - - listener.dispose(); }); test('basics (multi group)', async () => { @@ -147,7 +146,7 @@ suite('EditorsObserver', function () { let currentEditorsMRU = observer.editors; assert.strictEqual(currentEditorsMRU.length, 0); - const sideGroup = part.addGroup(rootGroup, GroupDirection.RIGHT); + const sideGroup = disposables.add(part.addGroup(rootGroup, GroupDirection.RIGHT)); const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); @@ -176,7 +175,7 @@ suite('EditorsObserver', function () { // Opening an editor inactive should not change // the most recent editor, but rather put it behind - const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); + const input2 = disposables.add(new TestFileEditorInput(URI.parse('foo://bar2'), TEST_SERIALIZABLE_EDITOR_INPUT_ID)); await rootGroup.openEditor(input2, { inactive: true }); @@ -212,13 +211,15 @@ suite('EditorsObserver', function () { assert.strictEqual(observer.hasEditors(input2.resource), false); assert.strictEqual(observer.hasEditor({ resource: input1.resource, typeId: input1.typeId, editorId: input1.editorId }), false); assert.strictEqual(observer.hasEditor({ resource: input2.resource, typeId: input2.typeId, editorId: input2.editorId }), false); + + part.removeGroup(sideGroup); }); test('hasEditor/hasEditors - same resource, different type id', async () => { const [part, observer] = await createEditorObserver(); - const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); - const input2 = new TestFileEditorInput(input1.resource, 'otherTypeId'); + const input1 = disposables.add(new TestFileEditorInput(URI.parse('foo://bar1'), TEST_SERIALIZABLE_EDITOR_INPUT_ID)); + const input2 = disposables.add(new TestFileEditorInput(input1.resource, 'otherTypeId')); assert.strictEqual(observer.hasEditors(input1.resource), false); assert.strictEqual(observer.hasEditor({ resource: input1.resource, typeId: input1.typeId, editorId: input1.editorId }), false); @@ -252,8 +253,8 @@ suite('EditorsObserver', function () { test('hasEditor/hasEditors - side by side editor support', async () => { const [part, observer, instantiationService] = await createEditorObserver(); - const primary = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); - const secondary = new TestFileEditorInput(URI.parse('foo://bar2'), 'otherTypeId'); + const primary = disposables.add(new TestFileEditorInput(URI.parse('foo://bar1'), TEST_SERIALIZABLE_EDITOR_INPUT_ID)); + const secondary = disposables.add(new TestFileEditorInput(URI.parse('foo://bar2'), 'otherTypeId')); const input = instantiationService.createInstance(SideBySideEditorInput, 'name', undefined, secondary, primary); @@ -289,9 +290,9 @@ suite('EditorsObserver', function () { test('copy group', async function () { const [part, observer] = await createEditorObserver(); - const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); - const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); - const input3 = new TestFileEditorInput(URI.parse('foo://bar3'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); + const input1 = disposables.add(new TestFileEditorInput(URI.parse('foo://bar1'), TEST_SERIALIZABLE_EDITOR_INPUT_ID)); + const input2 = disposables.add(new TestFileEditorInput(URI.parse('foo://bar2'), TEST_SERIALIZABLE_EDITOR_INPUT_ID)); + const input3 = disposables.add(new TestFileEditorInput(URI.parse('foo://bar3'), TEST_SERIALIZABLE_EDITOR_INPUT_ID)); const rootGroup = part.activeGroup; @@ -351,15 +352,15 @@ suite('EditorsObserver', function () { const rootGroup = part.activeGroup; - const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); - const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); - const input3 = new TestFileEditorInput(URI.parse('foo://bar3'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); + const input1 = disposables.add(new TestFileEditorInput(URI.parse('foo://bar1'), TEST_SERIALIZABLE_EDITOR_INPUT_ID)); + const input2 = disposables.add(new TestFileEditorInput(URI.parse('foo://bar2'), TEST_SERIALIZABLE_EDITOR_INPUT_ID)); + const input3 = disposables.add(new TestFileEditorInput(URI.parse('foo://bar3'), TEST_SERIALIZABLE_EDITOR_INPUT_ID)); await rootGroup.openEditor(input1, { pinned: true }); await rootGroup.openEditor(input2, { pinned: true }); await rootGroup.openEditor(input3, { pinned: true }); - const storage = new TestStorageService(); + const storage = disposables.add(new TestStorageService()); const observer = disposables.add(new EditorsObserver(part, storage)); await part.whenReady; @@ -398,17 +399,17 @@ suite('EditorsObserver', function () { const rootGroup = part.activeGroup; - const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); - const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); - const input3 = new TestFileEditorInput(URI.parse('foo://bar3'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); + const input1 = disposables.add(new TestFileEditorInput(URI.parse('foo://bar1'), TEST_SERIALIZABLE_EDITOR_INPUT_ID)); + const input2 = disposables.add(new TestFileEditorInput(URI.parse('foo://bar2'), TEST_SERIALIZABLE_EDITOR_INPUT_ID)); + const input3 = disposables.add(new TestFileEditorInput(URI.parse('foo://bar3'), TEST_SERIALIZABLE_EDITOR_INPUT_ID)); await rootGroup.openEditor(input1, { pinned: true }); await rootGroup.openEditor(input2, { pinned: true }); - const sideGroup = part.addGroup(rootGroup, GroupDirection.RIGHT); + const sideGroup = disposables.add(part.addGroup(rootGroup, GroupDirection.RIGHT)); await sideGroup.openEditor(input3, { pinned: true }); - const storage = new TestStorageService(); + const storage = disposables.add(new TestStorageService()); const observer = disposables.add(new EditorsObserver(part, storage)); await part.whenReady; @@ -447,11 +448,11 @@ suite('EditorsObserver', function () { const rootGroup = part.activeGroup; - const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID); + const input1 = disposables.add(new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID)); await rootGroup.openEditor(input1, { pinned: true }); - const storage = new TestStorageService(); + const storage = disposables.add(new TestStorageService()); const observer = disposables.add(new EditorsObserver(part, storage)); await part.whenReady; @@ -473,18 +474,18 @@ suite('EditorsObserver', function () { test('observer closes editors when limit reached (across all groups)', async () => { const [part] = await createPart(); - part.enforcePartOptions({ limit: { enabled: true, value: 3 } }); + disposables.add(part.enforcePartOptions({ limit: { enabled: true, value: 3 } })); - const storage = new TestStorageService(); + const storage = disposables.add(new TestStorageService()); const observer = disposables.add(new EditorsObserver(part, storage)); const rootGroup = part.activeGroup; - const sideGroup = part.addGroup(rootGroup, GroupDirection.RIGHT); + const sideGroup = disposables.add(part.addGroup(rootGroup, GroupDirection.RIGHT)); - const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID); - const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_EDITOR_INPUT_ID); - const input3 = new TestFileEditorInput(URI.parse('foo://bar3'), TEST_EDITOR_INPUT_ID); - const input4 = new TestFileEditorInput(URI.parse('foo://bar4'), TEST_EDITOR_INPUT_ID); + const input1 = disposables.add(new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID)); + const input2 = disposables.add(new TestFileEditorInput(URI.parse('foo://bar2'), TEST_EDITOR_INPUT_ID)); + const input3 = disposables.add(new TestFileEditorInput(URI.parse('foo://bar3'), TEST_EDITOR_INPUT_ID)); + const input4 = disposables.add(new TestFileEditorInput(URI.parse('foo://bar4'), TEST_EDITOR_INPUT_ID)); await rootGroup.openEditor(input1, { pinned: true }); await rootGroup.openEditor(input2, { pinned: true }); @@ -502,7 +503,7 @@ suite('EditorsObserver', function () { assert.strictEqual(observer.hasEditor({ resource: input4.resource, typeId: input4.typeId, editorId: input4.editorId }), true); input2.setDirty(); - part.enforcePartOptions({ limit: { enabled: true, value: 1 } }); + disposables.add(part.enforcePartOptions({ limit: { enabled: true, value: 1 } })); await timeout(0); @@ -516,7 +517,7 @@ suite('EditorsObserver', function () { assert.strictEqual(observer.hasEditor({ resource: input3.resource, typeId: input3.typeId, editorId: input3.editorId }), false); assert.strictEqual(observer.hasEditor({ resource: input4.resource, typeId: input4.typeId, editorId: input4.editorId }), true); - const input5 = new TestFileEditorInput(URI.parse('foo://bar5'), TEST_EDITOR_INPUT_ID); + const input5 = disposables.add(new TestFileEditorInput(URI.parse('foo://bar5'), TEST_EDITOR_INPUT_ID)); await sideGroup.openEditor(input5, { pinned: true }); assert.strictEqual(rootGroup.count, 1); @@ -534,18 +535,18 @@ suite('EditorsObserver', function () { test('observer closes editors when limit reached (in group)', async () => { const [part] = await createPart(); - part.enforcePartOptions({ limit: { enabled: true, value: 3, perEditorGroup: true } }); + disposables.add(part.enforcePartOptions({ limit: { enabled: true, value: 3, perEditorGroup: true } })); - const storage = new TestStorageService(); + const storage = disposables.add(new TestStorageService()); const observer = disposables.add(new EditorsObserver(part, storage)); const rootGroup = part.activeGroup; - const sideGroup = part.addGroup(rootGroup, GroupDirection.RIGHT); + const sideGroup = disposables.add(part.addGroup(rootGroup, GroupDirection.RIGHT)); - const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID); - const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_EDITOR_INPUT_ID); - const input3 = new TestFileEditorInput(URI.parse('foo://bar3'), TEST_EDITOR_INPUT_ID); - const input4 = new TestFileEditorInput(URI.parse('foo://bar4'), TEST_EDITOR_INPUT_ID); + const input1 = disposables.add(new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID)); + const input2 = disposables.add(new TestFileEditorInput(URI.parse('foo://bar2'), TEST_EDITOR_INPUT_ID)); + const input3 = disposables.add(new TestFileEditorInput(URI.parse('foo://bar3'), TEST_EDITOR_INPUT_ID)); + const input4 = disposables.add(new TestFileEditorInput(URI.parse('foo://bar4'), TEST_EDITOR_INPUT_ID)); await rootGroup.openEditor(input1, { pinned: true }); await rootGroup.openEditor(input2, { pinned: true }); @@ -577,7 +578,7 @@ suite('EditorsObserver', function () { assert.strictEqual(observer.hasEditor({ resource: input3.resource, typeId: input3.typeId, editorId: input3.editorId }), true); assert.strictEqual(observer.hasEditor({ resource: input4.resource, typeId: input4.typeId, editorId: input4.editorId }), true); - part.enforcePartOptions({ limit: { enabled: true, value: 1, perEditorGroup: true } }); + disposables.add(part.enforcePartOptions({ limit: { enabled: true, value: 1, perEditorGroup: true } })); await timeout(10); @@ -601,17 +602,17 @@ suite('EditorsObserver', function () { test('observer does not close sticky', async () => { const [part] = await createPart(); - part.enforcePartOptions({ limit: { enabled: true, value: 3 } }); + disposables.add(part.enforcePartOptions({ limit: { enabled: true, value: 3 } })); - const storage = new TestStorageService(); + const storage = disposables.add(new TestStorageService()); const observer = disposables.add(new EditorsObserver(part, storage)); const rootGroup = part.activeGroup; - const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID); - const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_EDITOR_INPUT_ID); - const input3 = new TestFileEditorInput(URI.parse('foo://bar3'), TEST_EDITOR_INPUT_ID); - const input4 = new TestFileEditorInput(URI.parse('foo://bar4'), TEST_EDITOR_INPUT_ID); + const input1 = disposables.add(new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID)); + const input2 = disposables.add(new TestFileEditorInput(URI.parse('foo://bar2'), TEST_EDITOR_INPUT_ID)); + const input3 = disposables.add(new TestFileEditorInput(URI.parse('foo://bar3'), TEST_EDITOR_INPUT_ID)); + const input4 = disposables.add(new TestFileEditorInput(URI.parse('foo://bar4'), TEST_EDITOR_INPUT_ID)); await rootGroup.openEditor(input1, { pinned: true, sticky: true }); await rootGroup.openEditor(input2, { pinned: true }); @@ -631,18 +632,18 @@ suite('EditorsObserver', function () { test('observer does not close scratchpads', async () => { const [part] = await createPart(); - part.enforcePartOptions({ limit: { enabled: true, value: 3 } }); + disposables.add(part.enforcePartOptions({ limit: { enabled: true, value: 3 } })); - const storage = new TestStorageService(); + const storage = disposables.add(new TestStorageService()); const observer = disposables.add(new EditorsObserver(part, storage)); const rootGroup = part.activeGroup; - const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID); + const input1 = disposables.add(new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID)); input1.capabilities = EditorInputCapabilities.Untitled | EditorInputCapabilities.Scratchpad; - const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_EDITOR_INPUT_ID); - const input3 = new TestFileEditorInput(URI.parse('foo://bar3'), TEST_EDITOR_INPUT_ID); - const input4 = new TestFileEditorInput(URI.parse('foo://bar4'), TEST_EDITOR_INPUT_ID); + const input2 = disposables.add(new TestFileEditorInput(URI.parse('foo://bar2'), TEST_EDITOR_INPUT_ID)); + const input3 = disposables.add(new TestFileEditorInput(URI.parse('foo://bar3'), TEST_EDITOR_INPUT_ID)); + const input4 = disposables.add(new TestFileEditorInput(URI.parse('foo://bar4'), TEST_EDITOR_INPUT_ID)); await rootGroup.openEditor(input1, { pinned: true }); await rootGroup.openEditor(input2, { pinned: true }); @@ -659,4 +660,6 @@ suite('EditorsObserver', function () { assert.strictEqual(observer.hasEditor({ resource: input3.resource, typeId: input3.typeId, editorId: input3.editorId }), true); assert.strictEqual(observer.hasEditor({ resource: input4.resource, typeId: input4.typeId, editorId: input4.editorId }), true); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts b/src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts index 27f37ac508b..551008eec3a 100644 --- a/src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts @@ -89,17 +89,17 @@ export class BuiltinExtensionsScannerService implements IBuiltinExtensionsScanne private async localizeManifest(extensionId: string, manifest: IExtensionManifest, fallbackTranslations: ITranslations): Promise { if (!this.nlsUrl) { - return localizeManifest(manifest, fallbackTranslations); + return localizeManifest(this.logService, manifest, fallbackTranslations); } // the `package` endpoint returns the translations in a key-value format similar to the package.nls.json file. const uri = URI.joinPath(this.nlsUrl, extensionId, 'package'); try { const res = await this.extensionResourceLoaderService.readExtensionResource(uri); const json = JSON.parse(res.toString()); - return localizeManifest(manifest, json, fallbackTranslations); + return localizeManifest(this.logService, manifest, json, fallbackTranslations); } catch (e) { this.logService.error(e); - return localizeManifest(manifest, fallbackTranslations); + return localizeManifest(this.logService, manifest, fallbackTranslations); } } } diff --git a/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts b/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts index a2426291881..8cc185f5da7 100644 --- a/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts @@ -801,7 +801,7 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten const translations = URI.isUri(nlsURL) ? await this.getTranslations(nlsURL) : nlsURL; const fallbackTranslations = URI.isUri(fallbackNLS) ? await this.getTranslations(fallbackNLS) : fallbackNLS; if (translations) { - manifest = localizeManifest(manifest, translations, fallbackTranslations); + manifest = localizeManifest(this.logService, manifest, translations, fallbackTranslations); } } catch (error) { /* ignore */ } return manifest; diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index 8001988f5ea..5dcc2ffe5a7 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -72,12 +72,12 @@ export class ExtensionManagementService extends Disposable implements IWorkbench this.servers.push(this.extensionManagementServerService.webExtensionManagementServer); } - this.onInstallExtension = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { emitter.add(Event.map(server.extensionManagementService.onInstallExtension, e => ({ ...e, server }))); return emitter; }, new EventMultiplexer())).event; - this.onDidInstallExtensions = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { emitter.add(server.extensionManagementService.onDidInstallExtensions); return emitter; }, new EventMultiplexer())).event; - this.onUninstallExtension = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { emitter.add(Event.map(server.extensionManagementService.onUninstallExtension, e => ({ ...e, server }))); return emitter; }, new EventMultiplexer())).event; - this.onDidUninstallExtension = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { emitter.add(Event.map(server.extensionManagementService.onDidUninstallExtension, e => ({ ...e, server }))); return emitter; }, new EventMultiplexer())).event; - this.onDidUpdateExtensionMetadata = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { emitter.add(server.extensionManagementService.onDidUpdateExtensionMetadata); return emitter; }, new EventMultiplexer())).event; - this.onDidChangeProfile = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { emitter.add(Event.map(server.extensionManagementService.onDidChangeProfile, e => ({ ...e, server }))); return emitter; }, new EventMultiplexer())).event; + this.onInstallExtension = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { this._register(emitter.add(Event.map(server.extensionManagementService.onInstallExtension, e => ({ ...e, server })))); return emitter; }, this._register(new EventMultiplexer()))).event; + this.onDidInstallExtensions = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { this._register(emitter.add(server.extensionManagementService.onDidInstallExtensions)); return emitter; }, this._register(new EventMultiplexer()))).event; + this.onUninstallExtension = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { this._register(emitter.add(Event.map(server.extensionManagementService.onUninstallExtension, e => ({ ...e, server })))); return emitter; }, this._register(new EventMultiplexer()))).event; + this.onDidUninstallExtension = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { this._register(emitter.add(Event.map(server.extensionManagementService.onDidUninstallExtension, e => ({ ...e, server })))); return emitter; }, this._register(new EventMultiplexer()))).event; + this.onDidUpdateExtensionMetadata = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { this._register(emitter.add(server.extensionManagementService.onDidUpdateExtensionMetadata)); return emitter; }, this._register(new EventMultiplexer()))).event; + this.onDidChangeProfile = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { this._register(emitter.add(Event.map(server.extensionManagementService.onDidChangeProfile, e => ({ ...e, server })))); return emitter; }, this._register(new EventMultiplexer()))).event; } async getInstalled(type?: ExtensionType, profileLocation?: URI): Promise { diff --git a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts index 2faa51fa86b..fa8aff5fde3 100644 --- a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts +++ b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts @@ -30,14 +30,15 @@ import { IHostService } from 'vs/workbench/services/host/browser/host'; import { mock } from 'vs/base/test/common/mock'; import { IExtensionBisectService } from 'vs/workbench/services/extensionManagement/browser/extensionBisect'; import { IWorkspaceTrustManagementService, IWorkspaceTrustRequestService, WorkspaceTrustRequestOptions } from 'vs/platform/workspace/common/workspaceTrust'; -import { TestWorkspaceTrustEnablementService, TestWorkspaceTrustManagementService } from 'vs/workbench/services/workspaces/test/common/testWorkspaceTrustService'; import { ExtensionManifestPropertiesService, IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; -import { TestContextService, TestProductService } from 'vs/workbench/test/common/workbenchTestServices'; +import { TestContextService, TestProductService, TestWorkspaceTrustEnablementService, TestWorkspaceTrustManagementService } from 'vs/workbench/test/common/workbenchTestServices'; import { TestWorkspace } from 'vs/platform/workspace/test/common/testWorkspace'; import { ExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagementService'; import { NullLogService } from 'vs/platform/log/common/log'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -function createStorageService(instantiationService: TestInstantiationService): IStorageService { +function createStorageService(instantiationService: TestInstantiationService, disposableStore: DisposableStore): IStorageService { let service = instantiationService.get(IStorageService); if (!service) { let workspaceContextService = instantiationService.get(IWorkspaceContextService); @@ -47,33 +48,34 @@ function createStorageService(instantiationService: TestInstantiationService): I getWorkspace: () => TestWorkspace as IWorkspace }); } - service = instantiationService.stub(IStorageService, new InMemoryStorageService()); + service = instantiationService.stub(IStorageService, disposableStore.add(new InMemoryStorageService())); } return service; } export class TestExtensionEnablementService extends ExtensionEnablementService { constructor(instantiationService: TestInstantiationService) { - const storageService = createStorageService(instantiationService); + const disposables = new DisposableStore(); + const storageService = createStorageService(instantiationService, disposables); const extensionManagementServerService = instantiationService.get(IExtensionManagementServerService) || instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService({ id: 'local', label: 'local', extensionManagementService: { - onInstallExtension: new Emitter().event, - onDidInstallExtensions: new Emitter().event, - onUninstallExtension: new Emitter().event, - onDidUninstallExtension: new Emitter().event, - onDidChangeProfile: new Emitter().event, - onDidUpdateExtensionMetadata: new Emitter().event, + onInstallExtension: disposables.add(new Emitter()).event, + onDidInstallExtensions: disposables.add(new Emitter()).event, + onUninstallExtension: disposables.add(new Emitter()).event, + onDidUninstallExtension: disposables.add(new Emitter()).event, + onDidChangeProfile: disposables.add(new Emitter()).event, + onDidUpdateExtensionMetadata: disposables.add(new Emitter()).event, }, }, null, null)); - const extensionManagementService = instantiationService.createInstance(ExtensionManagementService); + const extensionManagementService = disposables.add(instantiationService.createInstance(ExtensionManagementService)); const workbenchExtensionManagementService = instantiationService.get(IWorkbenchExtensionManagementService) || instantiationService.stub(IWorkbenchExtensionManagementService, extensionManagementService); - const workspaceTrustManagementService = instantiationService.get(IWorkspaceTrustManagementService) || instantiationService.stub(IWorkspaceTrustManagementService, new TestWorkspaceTrustManagementService()); + const workspaceTrustManagementService = instantiationService.get(IWorkspaceTrustManagementService) || instantiationService.stub(IWorkspaceTrustManagementService, disposables.add(new TestWorkspaceTrustManagementService())); super( storageService, - new GlobalExtensionEnablementService(storageService, extensionManagementService), + disposables.add(new GlobalExtensionEnablementService(storageService, extensionManagementService)), instantiationService.get(IWorkspaceContextService) || new TestContextService(), instantiationService.get(IWorkbenchEnvironmentService) || instantiationService.stub(IWorkbenchEnvironmentService, {} as IWorkbenchEnvironmentService), workbenchExtensionManagementService, @@ -81,15 +83,16 @@ export class TestExtensionEnablementService extends ExtensionEnablementService { extensionManagementServerService, instantiationService.get(IUserDataSyncEnablementService) || instantiationService.stub(IUserDataSyncEnablementService, >{ isEnabled() { return false; } }), instantiationService.get(IUserDataSyncAccountService) || instantiationService.stub(IUserDataSyncAccountService, UserDataSyncAccountService), - instantiationService.get(ILifecycleService) || instantiationService.stub(ILifecycleService, new TestLifecycleService()), + instantiationService.get(ILifecycleService) || instantiationService.stub(ILifecycleService, disposables.add(new TestLifecycleService())), instantiationService.get(INotificationService) || instantiationService.stub(INotificationService, new TestNotificationService()), instantiationService.get(IHostService), new class extends mock() { override isDisabledByBisect() { return false; } }, workspaceTrustManagementService, new class extends mock() { override requestWorkspaceTrust(options?: WorkspaceTrustRequestOptions): Promise { return Promise.resolve(true); } }, - instantiationService.get(IExtensionManifestPropertiesService) || instantiationService.stub(IExtensionManifestPropertiesService, new ExtensionManifestPropertiesService(TestProductService, new TestConfigurationService(), new TestWorkspaceTrustEnablementService(), new NullLogService())), + instantiationService.get(IExtensionManifestPropertiesService) || instantiationService.stub(IExtensionManifestPropertiesService, disposables.add(new ExtensionManifestPropertiesService(TestProductService, new TestConfigurationService(), new TestWorkspaceTrustEnablementService(), new NullLogService()))), instantiationService ); + this._register(disposables); } public async waitUntilInitialized(): Promise { @@ -113,6 +116,8 @@ export class TestExtensionEnablementService extends ExtensionEnablementService { suite('ExtensionEnablementService Test', () => { + const disposableStore = ensureNoDisposablesAreLeakedInTestSuite(); + let instantiationService: TestInstantiationService; let testObject: IWorkbenchExtensionEnablementService; @@ -123,7 +128,7 @@ suite('ExtensionEnablementService Test', () => { setup(() => { installed.splice(0, installed.length); - instantiationService = new TestInstantiationService(); + instantiationService = disposableStore.add(new TestInstantiationService()); instantiationService.stub(IConfigurationService, new TestConfigurationService()); instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService({ id: 'local', @@ -135,13 +140,8 @@ suite('ExtensionEnablementService Test', () => { getInstalled: () => Promise.resolve(installed) }, }, null, null)); - instantiationService.stub(IWorkbenchExtensionManagementService, instantiationService.createInstance(ExtensionManagementService)); - testObject = new TestExtensionEnablementService(instantiationService); - }); - - teardown(() => { - (testObject).dispose(); - instantiationService.dispose(); + instantiationService.stub(IWorkbenchExtensionManagementService, disposableStore.add(instantiationService.createInstance(ExtensionManagementService))); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); }); test('test disable an extension globally', async () => { @@ -158,7 +158,7 @@ suite('ExtensionEnablementService Test', () => { test('test disable an extension globally triggers the change event', async () => { const target = sinon.spy(); - testObject.onEnablementChanged(target); + disposableStore.add(testObject.onEnablementChanged(target)); await testObject.setEnablement([aLocalExtension('pub.a')], EnablementState.DisabledGlobally); assert.ok(target.calledOnce); assert.deepStrictEqual((target.args[0][0][0]).identifier, { id: 'pub.a' }); @@ -273,7 +273,7 @@ suite('ExtensionEnablementService Test', () => { test('test disable an extension for workspace and then globally trigger the change event', () => { const target = sinon.spy(); return testObject.setEnablement([aLocalExtension('pub.a')], EnablementState.DisabledWorkspace) - .then(() => testObject.onEnablementChanged(target)) + .then(() => disposableStore.add(testObject.onEnablementChanged(target))) .then(() => testObject.setEnablement([aLocalExtension('pub.a')], EnablementState.DisabledGlobally)) .then(() => { assert.ok(target.calledOnce); @@ -298,7 +298,7 @@ suite('ExtensionEnablementService Test', () => { test('test disable an extension globally and then for workspace triggers the change event', () => { const target = sinon.spy(); return testObject.setEnablement([aLocalExtension('pub.a')], EnablementState.DisabledGlobally) - .then(() => testObject.onEnablementChanged(target)) + .then(() => disposableStore.add(testObject.onEnablementChanged(target))) .then(() => testObject.setEnablement([aLocalExtension('pub.a')], EnablementState.DisabledWorkspace)) .then(() => { assert.ok(target.calledOnce); @@ -329,7 +329,7 @@ suite('ExtensionEnablementService Test', () => { test('test enable an extension globally triggers change event', () => { const target = sinon.spy(); return testObject.setEnablement([aLocalExtension('pub.a')], EnablementState.DisabledGlobally) - .then(() => testObject.onEnablementChanged(target)) + .then(() => disposableStore.add(testObject.onEnablementChanged(target))) .then(() => testObject.setEnablement([aLocalExtension('pub.a')], EnablementState.EnabledGlobally)) .then(() => { assert.ok(target.calledOnce); @@ -359,7 +359,7 @@ suite('ExtensionEnablementService Test', () => { test('test enable an extension for workspace triggers change event', () => { const target = sinon.spy(); return testObject.setEnablement([aLocalExtension('pub.b')], EnablementState.DisabledWorkspace) - .then(() => testObject.onEnablementChanged(target)) + .then(() => disposableStore.add(testObject.onEnablementChanged(target))) .then(() => testObject.setEnablement([aLocalExtension('pub.b')], EnablementState.EnabledWorkspace)) .then(() => { assert.ok(target.calledOnce); @@ -421,7 +421,7 @@ suite('ExtensionEnablementService Test', () => { const remoteWorkspaceDepExtension = aLocalExtension2('pub.b', { extensionKind: ['workspace'] }, { location: URI.file(`pub.b`).with({ scheme: Schemas.vscodeRemote }) }); installed.push(localWorkspaceDepExtension, remoteWorkspaceExtension, remoteWorkspaceDepExtension); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); await (testObject).waitUntilInitialized(); await testObject.setEnablement([remoteWorkspaceExtension], EnablementState.DisabledGlobally); @@ -445,7 +445,7 @@ suite('ExtensionEnablementService Test', () => { test('test remove an extension from disablement list when uninstalled', async () => { const extension = aLocalExtension('pub.a'); installed.push(extension); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); await testObject.setEnablement([extension], EnablementState.DisabledWorkspace); await testObject.setEnablement([extension], EnablementState.DisabledGlobally); @@ -483,7 +483,7 @@ suite('ExtensionEnablementService Test', () => { instantiationService.stub(IUserDataSyncAccountService, >{ account: { authenticationProviderId: 'b' } }); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); assert.strictEqual(testObject.canChangeEnablement(aLocalExtension('pub.a', { authentication: [{ id: 'a', label: 'a' }] })), true); }); @@ -491,7 +491,7 @@ suite('ExtensionEnablementService Test', () => { instantiationService.stub(IUserDataSyncAccountService, >{ account: { authenticationProviderId: 'a' } }); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); assert.strictEqual(testObject.canChangeEnablement(aLocalExtension('pub.a', { authentication: [{ id: 'a', label: 'a' }] })), true); }); @@ -500,7 +500,7 @@ suite('ExtensionEnablementService Test', () => { instantiationService.stub(IUserDataSyncAccountService, >{ account: { authenticationProviderId: 'a' } }); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); assert.strictEqual(testObject.canChangeEnablement(aLocalExtension('pub.a', { authentication: [{ id: 'a', label: 'a' }] })), false); }); @@ -519,26 +519,26 @@ suite('ExtensionEnablementService Test', () => { test('test canChangeEnablement return false when extensions are disabled in environment', () => { instantiationService.stub(IWorkbenchEnvironmentService, { disableExtensions: true } as IWorkbenchEnvironmentService); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); assert.strictEqual(testObject.canChangeEnablement(aLocalExtension('pub.a')), false); }); test('test canChangeEnablement return false when the extension is disabled in environment', () => { instantiationService.stub(IWorkbenchEnvironmentService, { disableExtensions: ['pub.a'] } as IWorkbenchEnvironmentService); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); assert.strictEqual(testObject.canChangeEnablement(aLocalExtension('pub.a')), false); }); test('test canChangeEnablement return true for system extensions when extensions are disabled in environment', () => { instantiationService.stub(IWorkbenchEnvironmentService, { disableExtensions: true } as IWorkbenchEnvironmentService); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); const extension = aLocalExtension('pub.a', undefined, ExtensionType.System); assert.strictEqual(testObject.canChangeEnablement(extension), true); }); test('test canChangeEnablement return false for system extension when extension is disabled in environment', () => { instantiationService.stub(IWorkbenchEnvironmentService, { disableExtensions: ['pub.a'] } as IWorkbenchEnvironmentService); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); const extension = aLocalExtension('pub.a', undefined, ExtensionType.System); assert.ok(!testObject.canChangeEnablement(extension)); }); @@ -548,7 +548,7 @@ suite('ExtensionEnablementService Test', () => { installed.push(extension); instantiationService.stub(IWorkbenchEnvironmentService, { disableExtensions: ['pub.a'] } as IWorkbenchEnvironmentService); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); assert.ok(!testObject.isEnabled(extension)); assert.deepStrictEqual(testObject.getEnablementState(extension), EnablementState.DisabledByEnvironment); @@ -559,7 +559,7 @@ suite('ExtensionEnablementService Test', () => { installed.push(extension); instantiationService.stub(IWorkbenchEnvironmentService, { enableExtensions: ['pub.a'] } as IWorkbenchEnvironmentService); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); assert.ok(testObject.isEnabled(extension)); assert.deepStrictEqual(testObject.getEnablementState(extension), EnablementState.EnabledGlobally); @@ -571,7 +571,7 @@ suite('ExtensionEnablementService Test', () => { await testObject.setEnablement([extension], EnablementState.EnabledWorkspace); instantiationService.stub(IWorkbenchEnvironmentService, { enableExtensions: ['pub.a'] } as IWorkbenchEnvironmentService); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); assert.ok(testObject.isEnabled(extension)); assert.deepStrictEqual(testObject.getEnablementState(extension), EnablementState.EnabledWorkspace); @@ -583,7 +583,7 @@ suite('ExtensionEnablementService Test', () => { await testObject.setEnablement([extension], EnablementState.DisabledGlobally); instantiationService.stub(IWorkbenchEnvironmentService, { enableExtensions: ['pub.a'] } as IWorkbenchEnvironmentService); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); assert.ok(testObject.isEnabled(extension)); assert.deepStrictEqual(testObject.getEnablementState(extension), EnablementState.EnabledByEnvironment); @@ -595,7 +595,7 @@ suite('ExtensionEnablementService Test', () => { await testObject.setEnablement([extension], EnablementState.DisabledWorkspace); instantiationService.stub(IWorkbenchEnvironmentService, { enableExtensions: ['pub.a'] } as IWorkbenchEnvironmentService); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); assert.ok(testObject.isEnabled(extension)); assert.deepStrictEqual(testObject.getEnablementState(extension), EnablementState.EnabledByEnvironment); @@ -607,7 +607,7 @@ suite('ExtensionEnablementService Test', () => { testObject.setEnablement([extension], EnablementState.DisabledWorkspace); instantiationService.stub(IWorkbenchEnvironmentService, { disableExtensions: true, enableExtensions: ['pub.a'] } as IWorkbenchEnvironmentService); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); assert.ok(!testObject.isEnabled(extension)); assert.deepStrictEqual(testObject.getEnablementState(extension), EnablementState.DisabledByEnvironment); @@ -615,14 +615,14 @@ suite('ExtensionEnablementService Test', () => { test('test canChangeEnablement return false when the extension is enabled in environment', () => { instantiationService.stub(IWorkbenchEnvironmentService, { enableExtensions: ['pub.a'] } as IWorkbenchEnvironmentService); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); assert.strictEqual(testObject.canChangeEnablement(aLocalExtension('pub.a')), false); }); test('test extension does not support vitrual workspace is not enabled in virtual workspace', async () => { const extension = aLocalExtension2('pub.a', { capabilities: { virtualWorkspaces: false } }); instantiationService.stub(IWorkspaceContextService, 'getWorkspace', { folders: [{ uri: URI.file('worskapceA').with(({ scheme: 'virtual' })) }] }); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); assert.ok(!testObject.isEnabled(extension)); assert.deepStrictEqual(testObject.getEnablementState(extension), EnablementState.DisabledByVirtualWorkspace); }); @@ -631,7 +631,7 @@ suite('ExtensionEnablementService Test', () => { instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService(null, anExtensionManagementServer('vscode-remote', instantiationService), anExtensionManagementServer('web', instantiationService))); const extension = aLocalExtension2('pub.a', { capabilities: { virtualWorkspaces: false }, browser: 'browser.js' }, { location: URI.file(`pub.a`).with({ scheme: 'web' }) }); instantiationService.stub(IWorkspaceContextService, 'getWorkspace', { folders: [{ uri: URI.file('worskapceA').with(({ scheme: 'virtual' })) }] }); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); assert.ok(testObject.isEnabled(extension)); assert.deepStrictEqual(testObject.getEnablementState(extension), EnablementState.EnabledGlobally); }); @@ -640,7 +640,7 @@ suite('ExtensionEnablementService Test', () => { instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService(null, anExtensionManagementServer('vscode-remote', instantiationService), anExtensionManagementServer('web', instantiationService))); const extension = aLocalExtension2('pub.a', { capabilities: { virtualWorkspaces: false }, browser: 'browser.js' }, { location: URI.file(`pub.a`).with({ scheme: 'vscode-remote' }) }); instantiationService.stub(IWorkspaceContextService, 'getWorkspace', { folders: [{ uri: URI.file('worskapceA').with(({ scheme: 'virtual' })) }] }); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); assert.ok(!testObject.isEnabled(extension)); assert.deepStrictEqual(testObject.getEnablementState(extension), EnablementState.DisabledByVirtualWorkspace); }); @@ -650,7 +650,7 @@ suite('ExtensionEnablementService Test', () => { const localUIExtension = aLocalExtension2('pub.a', { main: 'main.js', extensionKind: ['ui'] }, { location: URI.file(`pub.a`) }); const remoteUIExtension = aLocalExtension2('pub.a', { main: 'main.js', extensionKind: ['ui'] }, { location: URI.file(`pub.a`).with({ scheme: 'vscode-remote' }) }); const target = aLocalExtension2('pub.b', { main: 'main.js', extensionDependencies: ['pub.a'] }, { location: URI.file(`pub.b`).with({ scheme: 'vscode-remote' }) }); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); installed.push(localUIExtension, remoteUIExtension, target); await testObject.setEnablement([target, localUIExtension], EnablementState.DisabledGlobally); @@ -666,7 +666,7 @@ suite('ExtensionEnablementService Test', () => { const localUIExtension = aLocalExtension2('pub.a', { main: 'main.js', extensionKind: ['ui'] }, { location: URI.file(`pub.a`) }); const remoteUIExtension = aLocalExtension2('pub.a', { main: 'main.js', extensionKind: ['ui'] }, { location: URI.file(`pub.a`).with({ scheme: 'vscode-remote' }) }); const target = aLocalExtension2('pub.b', { main: 'main.js', extensionDependencies: ['pub.a'] }, { location: URI.file(`pub.b`).with({ scheme: 'vscode-remote' }) }); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); installed.push(localUIExtension, remoteUIExtension, target); await testObject.setEnablement([target, localUIExtension], EnablementState.DisabledGlobally); @@ -680,14 +680,14 @@ suite('ExtensionEnablementService Test', () => { test('test canChangeEnablement return false when extension is disabled in virtual workspace', () => { const extension = aLocalExtension2('pub.a', { capabilities: { virtualWorkspaces: false } }); instantiationService.stub(IWorkspaceContextService, 'getWorkspace', { folders: [{ uri: URI.file('worskapceA').with(({ scheme: 'virtual' })) }] }); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); assert.ok(!testObject.canChangeEnablement(extension)); }); test('test extension does not support vitrual workspace is enabled in normal workspace', async () => { const extension = aLocalExtension2('pub.a', { capabilities: { virtualWorkspaces: false } }); instantiationService.stub(IWorkspaceContextService, 'getWorkspace', { folders: [{ uri: URI.file('worskapceA') }] }); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); assert.ok(testObject.isEnabled(extension)); assert.deepStrictEqual(testObject.getEnablementState(extension), EnablementState.EnabledGlobally); }); @@ -695,7 +695,7 @@ suite('ExtensionEnablementService Test', () => { test('test extension supports virtual workspace is enabled in virtual workspace', async () => { const extension = aLocalExtension2('pub.a', { capabilities: { virtualWorkspaces: true } }); instantiationService.stub(IWorkspaceContextService, 'getWorkspace', { folders: [{ uri: URI.file('worskapceA').with(({ scheme: 'virtual' })) }] }); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); assert.ok(testObject.isEnabled(extension)); assert.deepStrictEqual(testObject.getEnablementState(extension), EnablementState.EnabledGlobally); }); @@ -703,42 +703,42 @@ suite('ExtensionEnablementService Test', () => { test('test extension does not support untrusted workspaces is disabled in untrusted workspace', () => { const extension = aLocalExtension2('pub.a', { main: 'main.js', capabilities: { untrustedWorkspaces: { supported: false, description: 'hello' } } }); instantiationService.stub(IWorkspaceTrustManagementService, >{ isWorkspaceTrusted() { return false; } }); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); assert.strictEqual(testObject.getEnablementState(extension), EnablementState.DisabledByTrustRequirement); }); test('test canChangeEnablement return true when extension is disabled by workspace trust', () => { const extension = aLocalExtension2('pub.a', { main: 'main.js', capabilities: { untrustedWorkspaces: { supported: false, description: 'hello' } } }); instantiationService.stub(IWorkspaceTrustManagementService, >{ isWorkspaceTrusted() { return false; } }); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); assert.ok(testObject.canChangeEnablement(extension)); }); test('test extension supports untrusted workspaces is enabled in untrusted workspace', () => { const extension = aLocalExtension2('pub.a', { main: 'main.js', capabilities: { untrustedWorkspaces: { supported: true } } }); instantiationService.stub(IWorkspaceTrustManagementService, >{ isWorkspaceTrusted() { return false; } }); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); assert.strictEqual(testObject.getEnablementState(extension), EnablementState.EnabledGlobally); }); test('test extension does not support untrusted workspaces is enabled in trusted workspace', () => { const extension = aLocalExtension2('pub.a', { main: 'main.js', capabilities: { untrustedWorkspaces: { supported: false, description: '' } } }); instantiationService.stub(IWorkspaceTrustManagementService, >{ isWorkspaceTrusted() { return true; } }); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); assert.strictEqual(testObject.getEnablementState(extension), EnablementState.EnabledGlobally); }); test('test extension supports untrusted workspaces is enabled in trusted workspace', () => { const extension = aLocalExtension2('pub.a', { main: 'main.js', capabilities: { untrustedWorkspaces: { supported: true } } }); instantiationService.stub(IWorkspaceTrustManagementService, >{ isWorkspaceTrusted() { return true; } }); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); assert.strictEqual(testObject.getEnablementState(extension), EnablementState.EnabledGlobally); }); test('test extension without any value for virtual worksapce is enabled in virtual workspace', async () => { const extension = aLocalExtension2('pub.a'); instantiationService.stub(IWorkspaceContextService, 'getWorkspace', { folders: [{ uri: URI.file('worskapceA').with(({ scheme: 'virtual' })) }] }); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); assert.ok(testObject.isEnabled(extension)); assert.deepStrictEqual(testObject.getEnablementState(extension), EnablementState.EnabledGlobally); }); @@ -746,7 +746,7 @@ suite('ExtensionEnablementService Test', () => { test('test local workspace extension is disabled by kind', async () => { instantiationService.stub(IExtensionManagementServerService, aMultiExtensionManagementServerService(instantiationService)); const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['workspace'] }, { location: URI.file(`pub.a`) }); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); assert.ok(!testObject.isEnabled(localWorkspaceExtension)); assert.deepStrictEqual(testObject.getEnablementState(localWorkspaceExtension), EnablementState.DisabledByExtensionKind); }); @@ -754,7 +754,7 @@ suite('ExtensionEnablementService Test', () => { test('test local workspace + ui extension is enabled by kind', async () => { instantiationService.stub(IExtensionManagementServerService, aMultiExtensionManagementServerService(instantiationService)); const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['workspace', 'ui'] }, { location: URI.file(`pub.a`) }); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); assert.ok(testObject.isEnabled(localWorkspaceExtension)); assert.deepStrictEqual(testObject.getEnablementState(localWorkspaceExtension), EnablementState.EnabledGlobally); }); @@ -762,7 +762,7 @@ suite('ExtensionEnablementService Test', () => { test('test local ui extension is not disabled by kind', async () => { instantiationService.stub(IExtensionManagementServerService, aMultiExtensionManagementServerService(instantiationService)); const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['ui'] }, { location: URI.file(`pub.a`) }); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); assert.ok(testObject.isEnabled(localWorkspaceExtension)); assert.deepStrictEqual(testObject.getEnablementState(localWorkspaceExtension), EnablementState.EnabledGlobally); }); @@ -770,21 +770,21 @@ suite('ExtensionEnablementService Test', () => { test('test canChangeEnablement return true when the local workspace extension is disabled by kind', () => { instantiationService.stub(IExtensionManagementServerService, aMultiExtensionManagementServerService(instantiationService)); const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['workspace'] }, { location: URI.file(`pub.a`) }); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); assert.strictEqual(testObject.canChangeEnablement(localWorkspaceExtension), false); }); test('test canChangeEnablement return true for local ui extension', () => { instantiationService.stub(IExtensionManagementServerService, aMultiExtensionManagementServerService(instantiationService)); const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['ui'] }, { location: URI.file(`pub.a`) }); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); assert.strictEqual(testObject.canChangeEnablement(localWorkspaceExtension), true); }); test('test remote ui extension is disabled by kind', async () => { instantiationService.stub(IExtensionManagementServerService, aMultiExtensionManagementServerService(instantiationService)); const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['ui'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); assert.ok(!testObject.isEnabled(localWorkspaceExtension)); assert.deepStrictEqual(testObject.getEnablementState(localWorkspaceExtension), EnablementState.DisabledByExtensionKind); }); @@ -792,7 +792,7 @@ suite('ExtensionEnablementService Test', () => { test('test remote ui+workspace extension is disabled by kind', async () => { instantiationService.stub(IExtensionManagementServerService, aMultiExtensionManagementServerService(instantiationService)); const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['ui', 'workspace'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); assert.ok(testObject.isEnabled(localWorkspaceExtension)); assert.deepStrictEqual(testObject.getEnablementState(localWorkspaceExtension), EnablementState.EnabledGlobally); }); @@ -800,7 +800,7 @@ suite('ExtensionEnablementService Test', () => { test('test remote ui extension is disabled by kind when there is no local server', async () => { instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService(null, anExtensionManagementServer('vscode-remote', instantiationService), null)); const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['ui'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); assert.ok(!testObject.isEnabled(localWorkspaceExtension)); assert.deepStrictEqual(testObject.getEnablementState(localWorkspaceExtension), EnablementState.DisabledByExtensionKind); }); @@ -808,7 +808,7 @@ suite('ExtensionEnablementService Test', () => { test('test remote workspace extension is not disabled by kind', async () => { instantiationService.stub(IExtensionManagementServerService, aMultiExtensionManagementServerService(instantiationService)); const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['workspace'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); assert.ok(testObject.isEnabled(localWorkspaceExtension)); assert.deepStrictEqual(testObject.getEnablementState(localWorkspaceExtension), EnablementState.EnabledGlobally); }); @@ -816,14 +816,14 @@ suite('ExtensionEnablementService Test', () => { test('test canChangeEnablement return true when the remote ui extension is disabled by kind', () => { instantiationService.stub(IExtensionManagementServerService, aMultiExtensionManagementServerService(instantiationService)); const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['ui'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); assert.strictEqual(testObject.canChangeEnablement(localWorkspaceExtension), false); }); test('test canChangeEnablement return true for remote workspace extension', () => { instantiationService.stub(IExtensionManagementServerService, aMultiExtensionManagementServerService(instantiationService)); const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['workspace'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); assert.strictEqual(testObject.canChangeEnablement(localWorkspaceExtension), true); }); @@ -831,7 +831,7 @@ suite('ExtensionEnablementService Test', () => { instantiationService.stub(IExtensionManagementServerService, aMultiExtensionManagementServerService(instantiationService)); const localWorkspaceExtension = aLocalExtension2('pub.a', { browser: 'browser.js' }, { location: URI.file(`pub.a`) }); (instantiationService.get(IConfigurationService)).setUserConfiguration('extensions', { webWorker: false }); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); assert.strictEqual(testObject.isEnabled(localWorkspaceExtension), false); assert.deepStrictEqual(testObject.getEnablementState(localWorkspaceExtension), EnablementState.DisabledByExtensionKind); }); @@ -840,7 +840,7 @@ suite('ExtensionEnablementService Test', () => { instantiationService.stub(IExtensionManagementServerService, aMultiExtensionManagementServerService(instantiationService)); const localWorkspaceExtension = aLocalExtension2('pub.a', { browser: 'browser.js' }, { location: URI.file(`pub.a`) }); (instantiationService.get(IConfigurationService)).setUserConfiguration('extensions', { webWorker: true }); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); assert.strictEqual(testObject.isEnabled(localWorkspaceExtension), true); assert.deepStrictEqual(testObject.getEnablementState(localWorkspaceExtension), EnablementState.EnabledGlobally); }); @@ -849,7 +849,7 @@ suite('ExtensionEnablementService Test', () => { instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService(anExtensionManagementServer('vscode-local', instantiationService), anExtensionManagementServer('vscode-remote', instantiationService), null)); const localWorkspaceExtension = aLocalExtension2('pub.a', { browser: 'browser.js' }, { location: URI.file(`pub.a`).with({ scheme: 'vscode-remote' }) }); (instantiationService.get(IConfigurationService)).setUserConfiguration('extensions', { webWorker: false }); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); assert.strictEqual(testObject.isEnabled(localWorkspaceExtension), false); assert.deepStrictEqual(testObject.getEnablementState(localWorkspaceExtension), EnablementState.DisabledByExtensionKind); }); @@ -858,7 +858,7 @@ suite('ExtensionEnablementService Test', () => { instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService(anExtensionManagementServer('vscode-local', instantiationService), anExtensionManagementServer('vscode-remote', instantiationService), null)); const localWorkspaceExtension = aLocalExtension2('pub.a', { browser: 'browser.js' }, { location: URI.file(`pub.a`).with({ scheme: 'vscode-remote' }) }); (instantiationService.get(IConfigurationService)).setUserConfiguration('extensions', { webWorker: true }); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); assert.strictEqual(testObject.isEnabled(localWorkspaceExtension), false); assert.deepStrictEqual(testObject.getEnablementState(localWorkspaceExtension), EnablementState.DisabledByExtensionKind); }); @@ -867,7 +867,7 @@ suite('ExtensionEnablementService Test', () => { instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService(anExtensionManagementServer('vscode-local', instantiationService), anExtensionManagementServer('vscode-remote', instantiationService), anExtensionManagementServer('web', instantiationService))); const localWorkspaceExtension = aLocalExtension2('pub.a', { browser: 'browser.js' }, { location: URI.file(`pub.a`).with({ scheme: 'vscode-remote' }) }); (instantiationService.get(IConfigurationService)).setUserConfiguration('extensions', { webWorker: false }); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); assert.strictEqual(testObject.isEnabled(localWorkspaceExtension), true); assert.deepStrictEqual(testObject.getEnablementState(localWorkspaceExtension), EnablementState.EnabledGlobally); }); @@ -875,14 +875,14 @@ suite('ExtensionEnablementService Test', () => { test('test web extension on web server is not disabled by kind', async () => { instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService(anExtensionManagementServer('vscode-local', instantiationService), anExtensionManagementServer('vscode-remote', instantiationService), anExtensionManagementServer('web', instantiationService))); const webExtension = aLocalExtension2('pub.a', { browser: 'browser.js' }, { location: URI.file(`pub.a`).with({ scheme: 'web' }) }); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); assert.strictEqual(testObject.isEnabled(webExtension), true); assert.deepStrictEqual(testObject.getEnablementState(webExtension), EnablementState.EnabledGlobally); }); test('test state of multipe extensions', async () => { installed.push(...[aLocalExtension('pub.a'), aLocalExtension('pub.b'), aLocalExtension('pub.c'), aLocalExtension('pub.d'), aLocalExtension('pub.e')]); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); await (testObject).waitUntilInitialized(); await testObject.setEnablement([installed[0]], EnablementState.DisabledGlobally); @@ -895,7 +895,7 @@ suite('ExtensionEnablementService Test', () => { test('test extension is disabled by dependency if it has a dependency that is disabled', async () => { installed.push(...[aLocalExtension2('pub.a'), aLocalExtension2('pub.b', { extensionDependencies: ['pub.a'] })]); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); await (testObject).waitUntilInitialized(); await testObject.setEnablement([installed[0]], EnablementState.DisabledGlobally); @@ -906,7 +906,7 @@ suite('ExtensionEnablementService Test', () => { test('test extension is disabled by dependency if it has a dependency that is disabled by virtual workspace', async () => { installed.push(...[aLocalExtension2('pub.a', { capabilities: { virtualWorkspaces: false } }), aLocalExtension2('pub.b', { extensionDependencies: ['pub.a'], capabilities: { virtualWorkspaces: true } })]); instantiationService.stub(IWorkspaceContextService, 'getWorkspace', { folders: [{ uri: URI.file('worskapceA').with(({ scheme: 'virtual' })) }] }); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); await (testObject).waitUntilInitialized(); assert.strictEqual(testObject.getEnablementState(installed[0]), EnablementState.DisabledByVirtualWorkspace); @@ -916,7 +916,7 @@ suite('ExtensionEnablementService Test', () => { test('test canChangeEnablement return false when extension is disabled by dependency if it has a dependency that is disabled by virtual workspace', async () => { installed.push(...[aLocalExtension2('pub.a', { capabilities: { virtualWorkspaces: false } }), aLocalExtension2('pub.b', { extensionDependencies: ['pub.a'], capabilities: { virtualWorkspaces: true } })]); instantiationService.stub(IWorkspaceContextService, 'getWorkspace', { folders: [{ uri: URI.file('worskapceA').with(({ scheme: 'virtual' })) }] }); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); await (testObject).waitUntilInitialized(); assert.ok(!testObject.canChangeEnablement(installed[1])); @@ -925,7 +925,7 @@ suite('ExtensionEnablementService Test', () => { test('test extension is disabled by dependency if it has a dependency that is disabled by workspace trust', async () => { installed.push(...[aLocalExtension2('pub.a', { main: 'hello.js', capabilities: { untrustedWorkspaces: { supported: false, description: '' } } }), aLocalExtension2('pub.b', { extensionDependencies: ['pub.a'], capabilities: { untrustedWorkspaces: { supported: true } } })]); instantiationService.stub(IWorkspaceTrustManagementService, >{ isWorkspaceTrusted() { return false; } }); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); await (testObject).waitUntilInitialized(); assert.strictEqual(testObject.getEnablementState(installed[0]), EnablementState.DisabledByTrustRequirement); @@ -939,7 +939,7 @@ suite('ExtensionEnablementService Test', () => { const remoteWorkspaceExtension = aLocalExtension2('pub.n', { extensionKind: ['workspace'], extensionDependencies: ['pub.a'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); installed.push(localUIExtension, remoteUIExtension, remoteWorkspaceExtension); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); await (testObject).waitUntilInitialized(); assert.strictEqual(testObject.getEnablementState(localUIExtension), EnablementState.EnabledGlobally); @@ -950,7 +950,7 @@ suite('ExtensionEnablementService Test', () => { test('test canChangeEnablement return true when extension is disabled by dependency if it has a dependency that is disabled by workspace trust', async () => { installed.push(...[aLocalExtension2('pub.a', { main: 'hello.js', capabilities: { untrustedWorkspaces: { supported: false, description: '' } } }), aLocalExtension2('pub.b', { extensionDependencies: ['pub.a'], capabilities: { untrustedWorkspaces: { supported: true } } })]); instantiationService.stub(IWorkspaceTrustManagementService, >{ isWorkspaceTrusted() { return false; } }); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); await (testObject).waitUntilInitialized(); assert.ok(testObject.canChangeEnablement(installed[1])); @@ -964,7 +964,7 @@ suite('ExtensionEnablementService Test', () => { test('test extension is disabled by dependency if it has a dependency that is disabled when all extensions are passed', async () => { installed.push(...[aLocalExtension2('pub.a'), aLocalExtension2('pub.b', { extensionDependencies: ['pub.a'] })]); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); await (testObject).waitUntilInitialized(); await testObject.setEnablement([installed[0]], EnablementState.DisabledGlobally); @@ -975,7 +975,7 @@ suite('ExtensionEnablementService Test', () => { test('test override workspace to trusted when getting extensions enablements', async () => { const extension = aLocalExtension2('pub.a', { main: 'main.js', capabilities: { untrustedWorkspaces: { supported: false, description: 'hello' } } }); instantiationService.stub(IWorkspaceTrustManagementService, >{ isWorkspaceTrusted() { return false; } }); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); assert.strictEqual(testObject.getEnablementStates([extension], { trusted: true })[0], EnablementState.EnabledGlobally); }); @@ -983,7 +983,7 @@ suite('ExtensionEnablementService Test', () => { test('test override workspace to not trusted when getting extensions enablements', async () => { const extension = aLocalExtension2('pub.a', { main: 'main.js', capabilities: { untrustedWorkspaces: { supported: false, description: 'hello' } } }); instantiationService.stub(IWorkspaceTrustManagementService, >{ isWorkspaceTrusted() { return true; } }); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); assert.strictEqual(testObject.getEnablementStates([extension], { trusted: false })[0], EnablementState.DisabledByTrustRequirement); }); @@ -995,9 +995,9 @@ suite('ExtensionEnablementService Test', () => { aLocalExtension2('pub.c', { main: 'main.js', capabilities: { untrustedWorkspaces: { supported: false, description: 'hello' } } }), aLocalExtension2('pub.d', { main: 'main.js', capabilities: { untrustedWorkspaces: { supported: true } } }), ]); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); const target = sinon.spy(); - testObject.onEnablementChanged(target); + disposableStore.add(testObject.onEnablementChanged(target)); await testObject.updateExtensionsEnablementsWhenWorkspaceTrustChanges(); assert.strictEqual(target.args[0][0].length, 2); @@ -1008,11 +1008,11 @@ suite('ExtensionEnablementService Test', () => { test('test adding an extension that was disabled', async () => { const extension = aLocalExtension('pub.a'); installed.push(extension); - testObject = new TestExtensionEnablementService(instantiationService); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); await testObject.setEnablement([extension], EnablementState.DisabledGlobally); const target = sinon.spy(); - testObject.onEnablementChanged(target); + disposableStore.add(testObject.onEnablementChanged(target)); didChangeProfileExtensionsEvent.fire({ added: [extension], removed: [] }); assert.ok(!testObject.isEnabled(extension)); diff --git a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts index ad66755797e..5ec7f0690c6 100644 --- a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts +++ b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts @@ -25,7 +25,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { isLoggingOnly } from 'vs/platform/telemetry/common/telemetryUtils'; import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { parentOriginHash } from 'vs/workbench/browser/iframe'; +import { parentOriginHash } from 'vs/base/browser/iframe'; import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { ExtensionHostExitCode, IExtensionHostInitData, MessageType, UIKind, createMessageOfType, isMessageOfType } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { LocalWebWorkerRunningLocation } from 'vs/workbench/services/extensions/common/extensionRunningLocation'; diff --git a/src/vs/workbench/services/extensions/test/browser/extensionService.test.ts b/src/vs/workbench/services/extensions/test/browser/extensionService.test.ts index f506bbee811..f99c067f3a3 100644 --- a/src/vs/workbench/services/extensions/test/browser/extensionService.test.ts +++ b/src/vs/workbench/services/extensions/test/browser/extensionService.test.ts @@ -222,6 +222,7 @@ suite('ExtensionService', () => { setup(() => { disposables = new DisposableStore(); + const testProductService = { _serviceBrand: undefined, ...product }; disposables.add(instantiationService = createServices(disposables, [ // custom [IExtensionService, MyTestExtensionService], @@ -235,7 +236,7 @@ suite('ExtensionService', () => { [IExtensionManifestPropertiesService, ExtensionManifestPropertiesService], [IConfigurationService, TestConfigurationService], [IWorkspaceContextService, TestContextService], - [IProductService, { _serviceBrand: undefined, ...product }], + [IProductService, testProductService], [IFileService, TestFileService], [IWorkbenchExtensionEnablementService, TestWorkbenchExtensionEnablementService], [ITelemetryService, NullTelemetryService], @@ -245,7 +246,7 @@ suite('ExtensionService', () => { [IUserDataProfileService, TestUserDataProfileService], [IUriIdentityService, UriIdentityService], [IRemoteExtensionsScannerService, TestRemoteExtensionsScannerService], - [IRemoteAuthorityResolverService, RemoteAuthorityResolverService] + [IRemoteAuthorityResolverService, new RemoteAuthorityResolverService(false, undefined, undefined, testProductService, new NullLogService())] ])); extService = instantiationService.get(IExtensionService); }); diff --git a/src/vs/workbench/services/extensions/test/browser/extensionStorageMigration.test.ts b/src/vs/workbench/services/extensions/test/browser/extensionStorageMigration.test.ts index 33a5ed27bdb..5cbda71ba82 100644 --- a/src/vs/workbench/services/extensions/test/browser/extensionStorageMigration.test.ts +++ b/src/vs/workbench/services/extensions/test/browser/extensionStorageMigration.test.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { DisposableStore } from 'vs/base/common/lifecycle'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IFileService } from 'vs/platform/files/common/files'; import { FileService } from 'vs/platform/files/common/fileService'; @@ -23,10 +22,11 @@ import { IUserDataProfilesService, UserDataProfilesService } from 'vs/platform/u import { UserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfileService'; import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('ExtensionStorageMigration', () => { - const disposables = new DisposableStore(); + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); const ROOT = URI.file('tests').with({ scheme: 'vscode-tests' }); const workspaceStorageHome = joinPath(ROOT, 'workspaceStorageHome'); @@ -36,17 +36,15 @@ suite('ExtensionStorageMigration', () => { instantiationService = workbenchInstantiationService(undefined, disposables); const fileService = disposables.add(new FileService(new NullLogService())); - fileService.registerProvider(ROOT.scheme, disposables.add(new InMemoryFileSystemProvider())); + disposables.add(fileService.registerProvider(ROOT.scheme, disposables.add(new InMemoryFileSystemProvider()))); instantiationService.stub(IFileService, fileService); const environmentService = instantiationService.stub(IEnvironmentService, >{ userRoamingDataHome: ROOT, workspaceStorageHome, cacheHome: ROOT }); - const userDataProfilesService = instantiationService.stub(IUserDataProfilesService, new UserDataProfilesService(environmentService, fileService, new UriIdentityService(fileService), new NullLogService())); - instantiationService.stub(IUserDataProfileService, new UserDataProfileService(userDataProfilesService.defaultProfile, userDataProfilesService)); + const userDataProfilesService = instantiationService.stub(IUserDataProfilesService, disposables.add(new UserDataProfilesService(environmentService, fileService, disposables.add(new UriIdentityService(fileService)), new NullLogService()))); + instantiationService.stub(IUserDataProfileService, disposables.add(new UserDataProfileService(userDataProfilesService.defaultProfile, userDataProfilesService))); - instantiationService.stub(IExtensionStorageService, instantiationService.createInstance(ExtensionStorageService)); + instantiationService.stub(IExtensionStorageService, disposables.add(instantiationService.createInstance(ExtensionStorageService))); }); - teardown(() => disposables.clear()); - test('migrate extension storage', async () => { const fromExtensionId = 'pub.from', toExtensionId = 'pub.to', storageMigratedKey = `extensionStorage.migrate.${fromExtensionId}-${toExtensionId}`; const extensionStorageService = instantiationService.get(IExtensionStorageService), fileService = instantiationService.get(IFileService), storageService = instantiationService.get(IStorageService), userDataProfilesService = instantiationService.get(IUserDataProfilesService); diff --git a/src/vs/workbench/services/extensions/test/common/extensionManifestPropertiesService.test.ts b/src/vs/workbench/services/extensions/test/common/extensionManifestPropertiesService.test.ts index 07e5b50f154..1924d01fe44 100644 --- a/src/vs/workbench/services/extensions/test/common/extensionManifestPropertiesService.test.ts +++ b/src/vs/workbench/services/extensions/test/common/extensionManifestPropertiesService.test.ts @@ -7,12 +7,11 @@ import * as assert from 'assert'; import { IExtensionManifest, ExtensionUntrustedWorkspaceSupportType } from 'vs/platform/extensions/common/extensions'; import { ExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; -import { TestProductService } from 'vs/workbench/test/common/workbenchTestServices'; +import { TestProductService, TestWorkspaceTrustEnablementService } from 'vs/workbench/test/common/workbenchTestServices'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IProductService } from 'vs/platform/product/common/productService'; import { isWeb } from 'vs/base/common/platform'; -import { TestWorkspaceTrustEnablementService } from 'vs/workbench/services/workspaces/test/common/testWorkspaceTrustService'; import { IWorkspaceTrustEnablementService } from 'vs/platform/workspace/common/workspaceTrust'; import { NullLogService } from 'vs/platform/log/common/log'; diff --git a/src/vs/workbench/services/history/browser/historyService.ts b/src/vs/workbench/services/history/browser/historyService.ts index fe3579726b1..63bf77e5e15 100644 --- a/src/vs/workbench/services/history/browser/historyService.ts +++ b/src/vs/workbench/services/history/browser/historyService.ts @@ -1115,6 +1115,20 @@ export class HistoryService extends Disposable implements IHistoryService { } //#endregion + + override dispose(): void { + super.dispose(); + + for (const [, stack] of this.editorGroupScopedNavigationStacks) { + stack.disposable.dispose(); + } + + for (const [, editors] of this.editorScopedNavigationStacks) { + for (const [, stack] of editors) { + stack.disposable.dispose(); + } + } + } } registerSingleton(IHistoryService, HistoryService, InstantiationType.Eager); diff --git a/src/vs/workbench/services/history/test/browser/historyService.test.ts b/src/vs/workbench/services/history/test/browser/historyService.test.ts index db24e25d864..d4c6b88601a 100644 --- a/src/vs/workbench/services/history/test/browser/historyService.test.ts +++ b/src/vs/workbench/services/history/test/browser/historyService.test.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { toResource } from 'vs/base/test/common/utils'; +import { ensureNoDisposablesAreLeakedInTestSuite, toResource } from 'vs/base/test/common/utils'; import { URI } from 'vs/base/common/uri'; -import { workbenchInstantiationService, TestFileEditorInput, registerTestEditor, createEditorPart, registerTestFileEditor, TestServiceAccessor, TestTextFileEditor } from 'vs/workbench/test/browser/workbenchTestServices'; +import { workbenchInstantiationService, TestFileEditorInput, registerTestEditor, createEditorPart, registerTestFileEditor, TestServiceAccessor, TestTextFileEditor, workbenchTeardown } from 'vs/workbench/test/browser/workbenchTestServices'; import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IEditorGroupsService, GroupDirection } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -40,7 +40,7 @@ suite('HistoryService', function () { const part = await createEditorPart(instantiationService, disposables); instantiationService.stub(IEditorGroupsService, part); - const editorService = instantiationService.createInstance(EditorService); + const editorService = disposables.add(instantiationService.createInstance(EditorService)); instantiationService.stub(IEditorService, editorService); const configurationService = new TestConfigurationService(); @@ -51,7 +51,7 @@ suite('HistoryService', function () { } instantiationService.stub(IConfigurationService, configurationService); - const historyService = instantiationService.createInstance(HistoryService); + const historyService = disposables.add(instantiationService.createInstance(HistoryService)); instantiationService.stub(IHistoryService, historyService); const accessor = instantiationService.createInstance(TestServiceAccessor); @@ -73,11 +73,11 @@ suite('HistoryService', function () { test('back / forward: basics', async () => { const [part, historyService] = await createServices(); - const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID); + const input1 = disposables.add(new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID)); await part.activeGroup.openEditor(input1, { pinned: true }); assert.strictEqual(part.activeGroup.activeEditor, input1); - const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_EDITOR_INPUT_ID); + const input2 = disposables.add(new TestFileEditorInput(URI.parse('foo://bar2'), TEST_EDITOR_INPUT_ID)); await part.activeGroup.openEditor(input2, { pinned: true }); assert.strictEqual(part.activeGroup.activeEditor, input2); @@ -89,10 +89,10 @@ suite('HistoryService', function () { }); test('back / forward: is editor group aware', async function () { - const [part, historyService, editorService] = await createServices(); + const [part, historyService, editorService, , instantiationService] = await createServices(); - const resource = toResource.call(this, '/path/index.txt'); - const otherResource = toResource.call(this, '/path/other.html'); + const resource: URI = toResource.call(this, '/path/index.txt'); + const otherResource: URI = toResource.call(this, '/path/other.html'); const pane1 = await editorService.openEditor({ resource, options: { pinned: true } }); const pane2 = await editorService.openEditor({ resource, options: { pinned: true } }, SIDE_GROUP); @@ -132,10 +132,12 @@ suite('HistoryService', function () { assert.strictEqual(part.activeGroup.id, pane2?.group?.id); assert.strictEqual(part.activeGroup.activeEditor?.resource?.toString(), otherResource.toString()); + + return workbenchTeardown(instantiationService); }); test('back / forward: in-editor text selection changes (user)', async function () { - const [, historyService, editorService] = await createServices(); + const [, historyService, editorService, , instantiationService] = await createServices(); const resource = toResource.call(this, '/path/index.txt'); @@ -159,10 +161,12 @@ suite('HistoryService', function () { await historyService.goForward(GoFilter.NONE); assertTextSelection(new Selection(17, 1, 17, 1), pane); + + return workbenchTeardown(instantiationService); }); test('back / forward: in-editor text selection changes (navigation)', async function () { - const [, historyService, editorService] = await createServices(); + const [, historyService, editorService, , instantiationService] = await createServices(); const resource = toResource.call(this, '/path/index.txt'); @@ -192,10 +196,12 @@ suite('HistoryService', function () { await historyService.goPrevious(GoFilter.NAVIGATION); assertTextSelection(new Selection(120, 8, 120, 18), pane); + + return workbenchTeardown(instantiationService); }); test('back / forward: in-editor text selection changes (jump)', async function () { - const [, historyService, editorService] = await createServices(); + const [, historyService, editorService, , instantiationService] = await createServices(); const resource = toResource.call(this, '/path/index.txt'); @@ -222,10 +228,12 @@ suite('HistoryService', function () { await historyService.goPrevious(GoFilter.NAVIGATION); assertTextSelection(new Selection(120, 8, 120, 18), pane); + + return workbenchTeardown(instantiationService); }); test('back / forward: selection changes with JUMP or NAVIGATION source are not merged (#143833)', async function () { - const [, historyService, editorService] = await createServices(); + const [, historyService, editorService, , instantiationService] = await createServices(); const resource = toResource.call(this, '/path/index.txt'); @@ -240,10 +248,12 @@ suite('HistoryService', function () { await historyService.goBack(GoFilter.NONE); assertTextSelection(new Selection(2, 2, 2, 10), pane); + + return workbenchTeardown(instantiationService); }); test('back / forward: edit selection changes', async function () { - const [, historyService, editorService] = await createServices(); + const [, historyService, editorService, , instantiationService] = await createServices(); const resource = toResource.call(this, '/path/index.txt'); @@ -265,6 +275,8 @@ suite('HistoryService', function () { await historyService.goForward(GoFilter.EDITS); assertTextSelection(new Selection(5, 3, 5, 20), pane); + + return workbenchTeardown(instantiationService); }); async function setTextSelection(historyService: IHistoryService, pane: TestTextFileEditor, selection: Selection, reason = EditorPaneSelectionChangeReason.USER): Promise { @@ -286,10 +298,10 @@ suite('HistoryService', function () { } test('back / forward: tracks editor moves across groups', async function () { - const [part, historyService, editorService] = await createServices(); + const [part, historyService, editorService, , instantiationService] = await createServices(); - const resource1 = toResource.call(this, '/path/one.txt'); - const resource2 = toResource.call(this, '/path/two.html'); + const resource1: URI = toResource.call(this, '/path/one.txt'); + const resource2: URI = toResource.call(this, '/path/two.html'); const pane1 = await editorService.openEditor({ resource: resource1, options: { pinned: true } }); await editorService.openEditor({ resource: resource2, options: { pinned: true } }); @@ -312,10 +324,12 @@ suite('HistoryService', function () { assert.strictEqual(part.activeGroup.id, pane1?.group?.id); assert.strictEqual(part.activeGroup.activeEditor?.resource?.toString(), resource1.toString()); + + return workbenchTeardown(instantiationService); }); test('back / forward: tracks group removals', async function () { - const [part, historyService, editorService] = await createServices(); + const [part, historyService, editorService, , instantiationService] = await createServices(); const resource1 = toResource.call(this, '/path/one.txt'); const resource2 = toResource.call(this, '/path/two.html'); @@ -337,6 +351,8 @@ suite('HistoryService', function () { assert.strictEqual(part.activeGroup.id, pane2?.group?.id); assert.strictEqual(part.activeGroup.activeEditor?.resource?.toString(), resource2.toString()); + + return workbenchTeardown(instantiationService); }); test('back / forward: editor navigation stack - navigation', async function () { @@ -349,7 +365,7 @@ suite('HistoryService', function () { const pane = await editorService.openEditor({ resource, options: { pinned: true } }); let changed = false; - stack.onDidChange(() => changed = true); + disposables.add(stack.onDidChange(() => changed = true)); assert.strictEqual(stack.canGoBack(), false); assert.strictEqual(stack.canGoForward(), false); @@ -400,15 +416,17 @@ suite('HistoryService', function () { stack.dispose(); assert.strictEqual(stack.canGoBack(), false); + + return workbenchTeardown(instantiationService); }); test('back / forward: editor navigation stack - mutations', async function () { const [, , editorService, , instantiationService] = await createServices(); - const stack = instantiationService.createInstance(EditorNavigationStack, GoFilter.NONE, GoScope.DEFAULT); + const stack = disposables.add(instantiationService.createInstance(EditorNavigationStack, GoFilter.NONE, GoScope.DEFAULT)); - const resource = toResource.call(this, '/path/index.txt'); - const otherResource = toResource.call(this, '/path/index.html'); + const resource: URI = toResource.call(this, '/path/index.txt'); + const otherResource: URI = toResource.call(this, '/path/index.html'); const pane = await editorService.openEditor({ resource, options: { pinned: true } }); stack.notifyNavigation(pane); @@ -488,10 +506,12 @@ suite('HistoryService', function () { stack.move(new FileOperationEvent(resource, FileOperation.MOVE, stat)); await stack.goBack(); assert.strictEqual(pane?.input?.resource?.toString(), stat.resource.toString()); + + return workbenchTeardown(instantiationService); }); test('back / forward: editor group scope', async function () { - const [part, historyService, editorService] = await createServices(GoScope.EDITOR_GROUP); + const [part, historyService, editorService, , instantiationService] = await createServices(GoScope.EDITOR_GROUP); const resource1 = toResource.call(this, '/path/one.txt'); const resource2 = toResource.call(this, '/path/two.html'); @@ -530,10 +550,12 @@ suite('HistoryService', function () { assert.strictEqual(part.activeGroup.id, pane1?.group?.id); assert.strictEqual(part.activeGroup.activeEditor?.resource?.toString(), resource1.toString()); + + return workbenchTeardown(instantiationService); }); test('back / forward: editor scope', async function () { - const [part, historyService, editorService] = await createServices(GoScope.EDITOR); + const [part, historyService, editorService, , instantiationService] = await createServices(GoScope.EDITOR); const resource1 = toResource.call(this, '/path/one.txt'); const resource2 = toResource.call(this, '/path/two.html'); @@ -564,11 +586,13 @@ suite('HistoryService', function () { assertTextSelection(new Selection(2, 2, 2, 10), pane); // no change assert.strictEqual(part.activeGroup.activeEditor?.resource?.toString(), resource1.toString()); + + return workbenchTeardown(instantiationService); }); test('go to last edit location', async function () { - const [, historyService, editorService, textFileService] = await createServices(); + const [, historyService, editorService, textFileService, instantiationService] = await createServices(); const resource = toResource.call(this, '/path/index.txt'); const otherResource = toResource.call(this, '/path/index.html'); @@ -581,18 +605,20 @@ suite('HistoryService', function () { await editorService.openEditor({ resource: otherResource }); const onDidActiveEditorChange = new DeferredPromise(); - editorService.onDidActiveEditorChange(e => { + disposables.add(editorService.onDidActiveEditorChange(e => { onDidActiveEditorChange.complete(e); - }); + })); historyService.goLast(GoFilter.EDITS); await onDidActiveEditorChange.p; assert.strictEqual(editorService.activeEditor?.resource?.toString(), resource.toString()); + + return workbenchTeardown(instantiationService); }); test('reopen closed editor', async function () { - const [, historyService, editorService] = await createServices(); + const [, historyService, editorService, , instantiationService] = await createServices(); const resource = toResource.call(this, '/path/index.txt'); const pane = await editorService.openEditor({ resource }); @@ -600,14 +626,16 @@ suite('HistoryService', function () { await pane?.group?.closeAllEditors(); const onDidActiveEditorChange = new DeferredPromise(); - editorService.onDidActiveEditorChange(e => { + disposables.add(editorService.onDidActiveEditorChange(e => { onDidActiveEditorChange.complete(e); - }); + })); historyService.reopenLastClosedEditor(); await onDidActiveEditorChange.p; assert.strictEqual(editorService.activeEditor?.resource?.toString(), resource.toString()); + + return workbenchTeardown(instantiationService); }); test('getHistory', async () => { @@ -624,21 +652,21 @@ suite('HistoryService', function () { } } - const [part, historyService] = await createServices(); + const [part, historyService, , , instantiationService] = await createServices(); let history = historyService.getHistory(); assert.strictEqual(history.length, 0); - const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID); + const input1 = disposables.add(new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID)); await part.activeGroup.openEditor(input1, { pinned: true }); - const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_EDITOR_INPUT_ID); + const input2 = disposables.add(new TestFileEditorInput(URI.parse('foo://bar2'), TEST_EDITOR_INPUT_ID)); await part.activeGroup.openEditor(input2, { pinned: true }); - const input3 = new TestFileEditorInputWithUntyped(URI.parse('foo://bar3'), TEST_EDITOR_INPUT_ID); + const input3 = disposables.add(new TestFileEditorInputWithUntyped(URI.parse('foo://bar3'), TEST_EDITOR_INPUT_ID)); await part.activeGroup.openEditor(input3, { pinned: true }); - const input4 = new TestFileEditorInputWithUntyped(URI.file('bar4'), TEST_EDITOR_INPUT_ID); + const input4 = disposables.add(new TestFileEditorInputWithUntyped(URI.file('bar4'), TEST_EDITOR_INPUT_ID)); await part.activeGroup.openEditor(input4, { pinned: true }); history = historyService.getHistory(); @@ -656,6 +684,8 @@ suite('HistoryService', function () { history = historyService.getHistory(); assert.strictEqual(history.length, 3); assert.strictEqual(history[0].resource?.toString(), input4.resource.toString()); + + return workbenchTeardown(instantiationService); }); test('getLastActiveFile', async () => { @@ -663,17 +693,17 @@ suite('HistoryService', function () { assert.ok(!historyService.getLastActiveFile('foo')); - const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID); + const input1 = disposables.add(new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID)); await part.activeGroup.openEditor(input1, { pinned: true }); assert.strictEqual(historyService.getLastActiveFile('foo')?.toString(), input1.resource.toString()); }); test('open next/previous recently used editor (single group)', async () => { - const [part, historyService, editorService] = await createServices(); + const [part, historyService, editorService, , instantiationService] = await createServices(); - const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID); - const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_EDITOR_INPUT_ID); + const input1 = disposables.add(new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID)); + const input2 = disposables.add(new TestFileEditorInput(URI.parse('foo://bar2'), TEST_EDITOR_INPUT_ID)); await part.activeGroup.openEditor(input1, { pinned: true }); assert.strictEqual(part.activeGroup.activeEditor, input1); @@ -700,14 +730,16 @@ suite('HistoryService', function () { historyService.openNextRecentlyUsedEditor(part.activeGroup.id); await editorChangePromise; assert.strictEqual(part.activeGroup.activeEditor, input2); + + return workbenchTeardown(instantiationService); }); test('open next/previous recently used editor (multi group)', async () => { - const [part, historyService, editorService] = await createServices(); + const [part, historyService, editorService, , instantiationService] = await createServices(); const rootGroup = part.activeGroup; - const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID); - const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_EDITOR_INPUT_ID); + const input1 = disposables.add(new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID)); + const input2 = disposables.add(new TestFileEditorInput(URI.parse('foo://bar2'), TEST_EDITOR_INPUT_ID)); const sideGroup = part.addGroup(rootGroup, GroupDirection.RIGHT); @@ -725,15 +757,17 @@ suite('HistoryService', function () { await editorChangePromise; assert.strictEqual(part.activeGroup, sideGroup); assert.strictEqual(sideGroup.activeEditor, input2); + + return workbenchTeardown(instantiationService); }); test('open next/previous recently is reset when other input opens', async () => { - const [part, historyService, editorService] = await createServices(); + const [part, historyService, editorService, , instantiationService] = await createServices(); - const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID); - const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_EDITOR_INPUT_ID); - const input3 = new TestFileEditorInput(URI.parse('foo://bar3'), TEST_EDITOR_INPUT_ID); - const input4 = new TestFileEditorInput(URI.parse('foo://bar4'), TEST_EDITOR_INPUT_ID); + const input1 = disposables.add(new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID)); + const input2 = disposables.add(new TestFileEditorInput(URI.parse('foo://bar2'), TEST_EDITOR_INPUT_ID)); + const input3 = disposables.add(new TestFileEditorInput(URI.parse('foo://bar3'), TEST_EDITOR_INPUT_ID)); + const input4 = disposables.add(new TestFileEditorInput(URI.parse('foo://bar4'), TEST_EDITOR_INPUT_ID)); await part.activeGroup.openEditor(input1, { pinned: true }); await part.activeGroup.openEditor(input2, { pinned: true }); @@ -756,5 +790,9 @@ suite('HistoryService', function () { historyService.openNextRecentlyUsedEditor(); await editorChangePromise; assert.strictEqual(part.activeGroup.activeEditor, input4); + + return workbenchTeardown(instantiationService); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/services/keybinding/test/browser/browserKeyboardMapper.test.ts b/src/vs/workbench/services/keybinding/test/browser/browserKeyboardMapper.test.ts index 3921c6f130e..0e2778b6191 100644 --- a/src/vs/workbench/services/keybinding/test/browser/browserKeyboardMapper.test.ts +++ b/src/vs/workbench/services/keybinding/test/browser/browserKeyboardMapper.test.ts @@ -16,6 +16,7 @@ import { TestNotificationService } from 'vs/platform/notification/test/common/te import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; class TestKeyboardMapperFactory extends BrowserKeyboardMapperFactoryBase { constructor(configurationService: IConfigurationService, notificationService: INotificationService, storageService: IStorageService, commandService: ICommandService) { @@ -35,17 +36,22 @@ class TestKeyboardMapperFactory extends BrowserKeyboardMapperFactoryBase { } suite('keyboard layout loader', () => { + const ds = ensureNoDisposablesAreLeakedInTestSuite(); let instantiationService: TestInstantiationService; let instance: TestKeyboardMapperFactory; setup(() => { instantiationService = new TestInstantiationService(); + const storageService = new TestStorageService(); const notitifcationService = instantiationService.stub(INotificationService, new TestNotificationService()); - const storageService = instantiationService.stub(IStorageService, new TestStorageService()); const configurationService = instantiationService.stub(IConfigurationService, new TestConfigurationService()); - const commandService = instantiationService.stub(ICommandService, {}); + + ds.add(instantiationService); + ds.add(storageService); + instance = new TestKeyboardMapperFactory(configurationService, notitifcationService, storageService, commandService); + ds.add(instance); }); teardown(() => { diff --git a/src/vs/workbench/services/keybinding/test/browser/keybindingEditing.test.ts b/src/vs/workbench/services/keybinding/test/browser/keybindingEditing.test.ts index e60d05855d9..213229f21e2 100644 --- a/src/vs/workbench/services/keybinding/test/browser/keybindingEditing.test.ts +++ b/src/vs/workbench/services/keybinding/test/browser/keybindingEditing.test.ts @@ -26,12 +26,12 @@ import { FileUserDataProvider } from 'vs/platform/userData/common/fileUserDataPr import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { joinPath } from 'vs/base/common/resources'; import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; -import { DisposableStore } from 'vs/base/common/lifecycle'; import { VSBuffer } from 'vs/base/common/buffer'; import { UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { UserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfileService'; import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; interface Modifiers { metaKey?: boolean; @@ -44,7 +44,7 @@ const ROOT = URI.file('tests').with({ scheme: 'vscode-tests' }); suite('KeybindingsEditing', () => { - const disposables = new DisposableStore(); + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); let instantiationService: TestInstantiationService; let fileService: IFileService; let environmentService: IEnvironmentService; @@ -67,8 +67,8 @@ suite('KeybindingsEditing', () => { const configService = new TestConfigurationService(); configService.setUserConfiguration('files', { 'eol': '\n' }); - const userDataProfilesService = new UserDataProfilesService(environmentService, fileService, new UriIdentityService(fileService), logService); - userDataProfileService = new UserDataProfileService(userDataProfilesService.defaultProfile, userDataProfilesService); + const userDataProfilesService = disposables.add(new UserDataProfilesService(environmentService, fileService, disposables.add(new UriIdentityService(fileService)), logService)); + userDataProfileService = disposables.add(new UserDataProfileService(userDataProfilesService.defaultProfile, userDataProfilesService)); instantiationService = workbenchInstantiationService({ fileService: () => fileService, @@ -79,8 +79,6 @@ suite('KeybindingsEditing', () => { testObject = disposables.add(instantiationService.createInstance(KeybindingsEditingService)); }); - teardown(() => disposables.clear()); - test('errors cases - parse errors', async () => { await fileService.writeFile(userDataProfileService.currentProfile.keybindingsResource, VSBuffer.fromString(',,,,,,,,,,,,,,')); try { diff --git a/src/vs/workbench/services/label/test/browser/label.test.ts b/src/vs/workbench/services/label/test/browser/label.test.ts index 720417ed0a4..520ed89e35d 100644 --- a/src/vs/workbench/services/label/test/browser/label.test.ts +++ b/src/vs/workbench/services/label/test/browser/label.test.ts @@ -16,6 +16,8 @@ import { StorageScope, StorageTarget } from 'vs/platform/storage/common/storage' import { Memento } from 'vs/workbench/common/memento'; import { ResourceLabelFormatter } from 'vs/platform/label/common/label'; import { sep } from 'vs/base/common/path'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { DisposableStore } from 'vs/base/common/lifecycle'; suite('URI Label', () => { let labelService: LabelService; @@ -225,13 +227,14 @@ suite('URI Label', () => { suite('multi-root workspace', () => { let labelService: LabelService; + const disposables = new DisposableStore(); setup(() => { const sources = URI.file('folder1/src'); const tests = URI.file('folder1/test'); const other = URI.file('folder2'); - labelService = new LabelService( + labelService = disposables.add(new LabelService( TestEnvironmentService, new TestContextService( new Workspace('test-workspace', [ @@ -241,9 +244,13 @@ suite('multi-root workspace', () => { ])), new TestPathService(), new TestRemoteAgentService(), - new TestStorageService(), - new TestLifecycleService() - ); + disposables.add(new TestStorageService()), + disposables.add(new TestLifecycleService()) + )); + }); + + teardown(() => { + disposables.clear(); }); test('labels of files in multiroot workspaces are the foldername followed by offset from the folder', () => { @@ -320,7 +327,7 @@ suite('multi-root workspace', () => { test('relative label without formatter', () => { const rootFolder = URI.parse('myscheme://myauthority/'); - labelService = new LabelService( + labelService = disposables.add(new LabelService( TestEnvironmentService, new TestContextService( new Workspace('test-workspace', [ @@ -328,9 +335,9 @@ suite('multi-root workspace', () => { ])), new TestPathService(undefined, rootFolder.scheme), new TestRemoteAgentService(), - new TestStorageService(), - new TestLifecycleService() - ); + disposables.add(new TestStorageService()), + disposables.add(new TestLifecycleService()) + )); const generated = labelService.getUriLabel(URI.parse('myscheme://myauthority/some/folder/test.txt'), { relative: true }); if (isWindows) { @@ -339,6 +346,8 @@ suite('multi-root workspace', () => { assert.strictEqual(generated, 'some/folder/test.txt'); } }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); suite('workspace at FSP root', () => { @@ -405,4 +414,6 @@ suite('workspace at FSP root', () => { generated = labelService.getUriLabel(URI.parse('myscheme://myauthority/some/folder/test.txt'), { relative: true, separator: '\\' }); assert.strictEqual(generated, 'some\\folder\\test.txt'); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/services/languageDetection/browser/languageDetectionWorkerServiceImpl.ts b/src/vs/workbench/services/languageDetection/browser/languageDetectionWorkerServiceImpl.ts index 7dd83df0fd0..0476bcd9a0f 100644 --- a/src/vs/workbench/services/languageDetection/browser/languageDetectionWorkerServiceImpl.ts +++ b/src/vs/workbench/services/languageDetection/browser/languageDetectionWorkerServiceImpl.ts @@ -66,7 +66,7 @@ export class LanguageDetectionService extends Disposable implements ILanguageDet ) { super(); - this._languageDetectionWorkerClient = new LanguageDetectionWorkerClient( + this._languageDetectionWorkerClient = this._register(new LanguageDetectionWorkerClient( modelService, languageService, telemetryService, @@ -84,7 +84,7 @@ export class LanguageDetectionService extends Disposable implements ILanguageDet ? FileAccess.asBrowserUri(`${regexpModuleLocationAsar}/dist/index.js`).toString(true) : FileAccess.asBrowserUri(`${regexpModuleLocation}/dist/index.js`).toString(true), languageConfigurationService - ); + )); this.initEditorOpenedListeners(storageService); } diff --git a/src/vs/workbench/services/lifecycle/test/electron-sandbox/lifecycleService.test.ts b/src/vs/workbench/services/lifecycle/test/electron-sandbox/lifecycleService.test.ts index 34604db9ea4..948741f9d0a 100644 --- a/src/vs/workbench/services/lifecycle/test/electron-sandbox/lifecycleService.test.ts +++ b/src/vs/workbench/services/lifecycle/test/electron-sandbox/lifecycleService.test.ts @@ -5,6 +5,7 @@ import * as assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ShutdownReason } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { NativeLifecycleService } from 'vs/workbench/services/lifecycle/electron-sandbox/lifecycleService'; import { workbenchInstantiationService } from 'vs/workbench/test/electron-sandbox/workbenchTestServices'; @@ -12,7 +13,7 @@ import { workbenchInstantiationService } from 'vs/workbench/test/electron-sandbo suite('Lifecycleservice', function () { let lifecycleService: TestLifecycleService; - let disposables: DisposableStore; + const disposables = new DisposableStore(); class TestLifecycleService extends NativeLifecycleService { @@ -26,14 +27,12 @@ suite('Lifecycleservice', function () { } setup(async () => { - disposables = new DisposableStore(); - const instantiationService = workbenchInstantiationService(undefined, disposables); - lifecycleService = instantiationService.createInstance(TestLifecycleService); + lifecycleService = disposables.add(instantiationService.createInstance(TestLifecycleService)); }); teardown(async () => { - disposables.dispose(); + disposables.clear(); }); test('onBeforeShutdown - final veto called after other vetos', async function () { @@ -42,16 +41,16 @@ suite('Lifecycleservice', function () { const order: number[] = []; - lifecycleService.onBeforeShutdown(e => { + disposables.add(lifecycleService.onBeforeShutdown(e => { e.veto(new Promise(resolve => { vetoCalled = true; order.push(1); resolve(false); }), 'test'); - }); + })); - lifecycleService.onBeforeShutdown(e => { + disposables.add(lifecycleService.onBeforeShutdown(e => { e.finalVeto(() => { return new Promise(resolve => { finalVetoCalled = true; @@ -60,7 +59,7 @@ suite('Lifecycleservice', function () { resolve(true); }); }, 'test'); - }); + })); const veto = await lifecycleService.testHandleBeforeShutdown(ShutdownReason.QUIT); @@ -75,15 +74,15 @@ suite('Lifecycleservice', function () { let vetoCalled = false; let finalVetoCalled = false; - lifecycleService.onBeforeShutdown(e => { + disposables.add(lifecycleService.onBeforeShutdown(e => { e.veto(new Promise(resolve => { vetoCalled = true; resolve(true); }), 'test'); - }); + })); - lifecycleService.onBeforeShutdown(e => { + disposables.add(lifecycleService.onBeforeShutdown(e => { e.finalVeto(() => { return new Promise(resolve => { finalVetoCalled = true; @@ -91,7 +90,7 @@ suite('Lifecycleservice', function () { resolve(true); }); }, 'test'); - }); + })); const veto = await lifecycleService.testHandleBeforeShutdown(ShutdownReason.QUIT); @@ -101,11 +100,11 @@ suite('Lifecycleservice', function () { }); test('onBeforeShutdown - veto with error is treated as veto', async function () { - lifecycleService.onBeforeShutdown(e => { + disposables.add(lifecycleService.onBeforeShutdown(e => { e.veto(new Promise((resolve, reject) => { reject(new Error('Fail')); }), 'test'); - }); + })); const veto = await lifecycleService.testHandleBeforeShutdown(ShutdownReason.QUIT); @@ -113,11 +112,11 @@ suite('Lifecycleservice', function () { }); test('onBeforeShutdown - final veto with error is treated as veto', async function () { - lifecycleService.onBeforeShutdown(e => { + disposables.add(lifecycleService.onBeforeShutdown(e => { e.finalVeto(() => new Promise((resolve, reject) => { reject(new Error('Fail')); }), 'test'); - }); + })); const veto = await lifecycleService.testHandleBeforeShutdown(ShutdownReason.QUIT); @@ -127,13 +126,13 @@ suite('Lifecycleservice', function () { test('onWillShutdown - join', async function () { let joinCalled = false; - lifecycleService.onWillShutdown(e => { + disposables.add(lifecycleService.onWillShutdown(e => { e.join(new Promise(resolve => { joinCalled = true; resolve(); }), { id: 'test', label: 'test' }); - }); + })); await lifecycleService.testHandleWillShutdown(ShutdownReason.QUIT); @@ -143,16 +142,18 @@ suite('Lifecycleservice', function () { test('onWillShutdown - join with error is handled', async function () { let joinCalled = false; - lifecycleService.onWillShutdown(e => { + disposables.add(lifecycleService.onWillShutdown(e => { e.join(new Promise((resolve, reject) => { joinCalled = true; reject(new Error('Fail')); }), { id: 'test', label: 'test' }); - }); + })); await lifecycleService.testHandleWillShutdown(ShutdownReason.QUIT); assert.strictEqual(joinCalled, true); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/services/preferences/test/browser/keybindingsEditorModel.test.ts b/src/vs/workbench/services/preferences/test/browser/keybindingsEditorModel.test.ts index 1e5631e8a67..58c5d5169c3 100644 --- a/src/vs/workbench/services/preferences/test/browser/keybindingsEditorModel.test.ts +++ b/src/vs/workbench/services/preferences/test/browser/keybindingsEditorModel.test.ts @@ -20,6 +20,7 @@ import { TestInstantiationService } from 'vs/platform/instantiation/test/common/ import { IKeybindingItemEntry } from 'vs/workbench/services/preferences/common/preferences'; import { Action2, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; interface Modifiers { metaKey?: boolean; @@ -30,26 +31,23 @@ interface Modifiers { suite('KeybindingsEditorModel', () => { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); let instantiationService: TestInstantiationService; let testObject: KeybindingsEditorModel; let extensions: Partial[] = []; setup(() => { extensions = []; - instantiationService = new TestInstantiationService(); + instantiationService = disposables.add(new TestInstantiationService()); instantiationService.stub(IKeybindingService, {}); instantiationService.stub(IExtensionService, >{ whenInstalledExtensionsRegistered: () => Promise.resolve(true), get extensions() { return extensions; } }); - testObject = instantiationService.createInstance(KeybindingsEditorModel, OS); + testObject = disposables.add(instantiationService.createInstance(KeybindingsEditorModel, OS)); - CommandsRegistry.registerCommand('command_without_keybinding', () => { }); - }); - - teardown(() => { - instantiationService.dispose(); + disposables.add(CommandsRegistry.registerCommand('command_without_keybinding', () => { })); }); test('fetch returns default keybindings', async () => { @@ -190,7 +188,7 @@ suite('KeybindingsEditorModel', () => { }); test('convert without title and binding to entry', async () => { - CommandsRegistry.registerCommand('command_without_keybinding', () => { }); + disposables.add(CommandsRegistry.registerCommand('command_without_keybinding', () => { })); prepareKeybindingService(); await testObject.resolve(new Map()); @@ -310,7 +308,7 @@ suite('KeybindingsEditorModel', () => { }); test('filter by cmd key', async () => { - testObject = instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh); + testObject = disposables.add(instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh)); const command = 'a' + uuid.generateUuid(); const expected = aResolvedKeybindingItem({ command, firstChord: { keyCode: KeyCode.Escape, modifiers: { metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }); @@ -324,7 +322,7 @@ suite('KeybindingsEditorModel', () => { }); test('filter by meta key', async () => { - testObject = instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh); + testObject = disposables.add(instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh)); const command = 'a' + uuid.generateUuid(); const expected = aResolvedKeybindingItem({ command, firstChord: { keyCode: KeyCode.Escape, modifiers: { metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }); @@ -338,7 +336,7 @@ suite('KeybindingsEditorModel', () => { }); test('filter by command key', async () => { - testObject = instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh); + testObject = disposables.add(instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh)); const command = 'a' + uuid.generateUuid(); const expected = aResolvedKeybindingItem({ command, firstChord: { keyCode: KeyCode.Escape, modifiers: { metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }); @@ -352,7 +350,7 @@ suite('KeybindingsEditorModel', () => { }); test('filter by windows key', async () => { - testObject = instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Windows); + testObject = disposables.add(instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Windows)); const command = 'a' + uuid.generateUuid(); const expected = aResolvedKeybindingItem({ command, firstChord: { keyCode: KeyCode.Escape, modifiers: { metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }); @@ -460,7 +458,7 @@ suite('KeybindingsEditorModel', () => { }); test('filter by modifiers and key', async () => { - testObject = instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh); + testObject = disposables.add(instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh)); const command = 'a' + uuid.generateUuid(); const expected = aResolvedKeybindingItem({ command, firstChord: { keyCode: KeyCode.Escape, modifiers: { altKey: true, metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }); prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstChord: { keyCode: KeyCode.Escape, modifiers: { metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false })); @@ -473,7 +471,7 @@ suite('KeybindingsEditorModel', () => { }); test('filter by modifiers in random order and key', async () => { - testObject = instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh); + testObject = disposables.add(instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh)); const command = 'a' + uuid.generateUuid(); const expected = aResolvedKeybindingItem({ command, firstChord: { keyCode: KeyCode.Escape, modifiers: { shiftKey: true, metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }); prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstChord: { keyCode: KeyCode.Escape, modifiers: { metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false })); @@ -486,7 +484,7 @@ suite('KeybindingsEditorModel', () => { }); test('filter by first part', async () => { - testObject = instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh); + testObject = disposables.add(instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh)); const command = 'a' + uuid.generateUuid(); const expected = aResolvedKeybindingItem({ command, firstChord: { keyCode: KeyCode.Escape, modifiers: { shiftKey: true, metaKey: true } }, secondChord: { keyCode: KeyCode.Delete }, when: 'whenContext1 && whenContext2', isDefault: false }); prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstChord: { keyCode: KeyCode.Escape, modifiers: { metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false })); @@ -499,7 +497,7 @@ suite('KeybindingsEditorModel', () => { }); test('filter matches in chord part', async () => { - testObject = instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh); + testObject = disposables.add(instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh)); const command = 'a' + uuid.generateUuid(); const expected = aResolvedKeybindingItem({ command, firstChord: { keyCode: KeyCode.Escape, modifiers: { shiftKey: true, metaKey: true } }, secondChord: { keyCode: KeyCode.Delete }, when: 'whenContext1 && whenContext2', isDefault: false }); prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstChord: { keyCode: KeyCode.Escape, modifiers: { metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false })); @@ -512,7 +510,7 @@ suite('KeybindingsEditorModel', () => { }); test('filter matches first part and in chord part', async () => { - testObject = instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh); + testObject = disposables.add(instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh)); const command = 'a' + uuid.generateUuid(); const expected = aResolvedKeybindingItem({ command, firstChord: { keyCode: KeyCode.Escape, modifiers: { shiftKey: true, metaKey: true } }, secondChord: { keyCode: KeyCode.Delete }, when: 'whenContext1 && whenContext2', isDefault: false }); prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstChord: { keyCode: KeyCode.Escape, modifiers: { shiftKey: true, metaKey: true } }, secondChord: { keyCode: KeyCode.UpArrow }, when: 'whenContext1 && whenContext2', isDefault: false })); @@ -549,7 +547,7 @@ suite('KeybindingsEditorModel', () => { }); test('filter exact matches with first and chord part no results', async () => { - testObject = instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh); + testObject = disposables.add(instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh)); const command = 'a' + uuid.generateUuid(); const expected = aResolvedKeybindingItem({ command, firstChord: { keyCode: KeyCode.Escape, modifiers: { shiftKey: true, metaKey: true } }, secondChord: { keyCode: KeyCode.Delete, modifiers: { metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }); prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstChord: { keyCode: KeyCode.Escape, modifiers: { shiftKey: true, metaKey: true } }, secondChord: { keyCode: KeyCode.UpArrow }, when: 'whenContext1 && whenContext2', isDefault: false })); @@ -618,7 +616,7 @@ suite('KeybindingsEditorModel', () => { }); test('filter exact matches with user settings label', async () => { - testObject = instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh); + testObject = disposables.add(instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh)); const command = 'a' + uuid.generateUuid(); const expected = aResolvedKeybindingItem({ command, firstChord: { keyCode: KeyCode.DownArrow } }); prepareKeybindingService(expected, aResolvedKeybindingItem({ command: 'down', firstChord: { keyCode: KeyCode.Escape } })); @@ -642,7 +640,7 @@ suite('KeybindingsEditorModel', () => { }); test('filter modifiers are not matched when not completely matched (prefix)', async () => { - testObject = instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh); + testObject = disposables.add(instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh)); const term = `alt.${uuid.generateUuid()}`; const command = `command.${term}`; const expected = aResolvedKeybindingItem({ command, firstChord: { keyCode: KeyCode.Escape }, isDefault: false }); @@ -656,7 +654,7 @@ suite('KeybindingsEditorModel', () => { }); test('filter modifiers are not matched when not completely matched (includes)', async () => { - testObject = instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh); + testObject = disposables.add(instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh)); const term = `abcaltdef.${uuid.generateUuid()}`; const command = `command.${term}`; const expected = aResolvedKeybindingItem({ command, firstChord: { keyCode: KeyCode.Escape }, isDefault: false }); @@ -670,7 +668,7 @@ suite('KeybindingsEditorModel', () => { }); test('filter modifiers are matched with complete term', async () => { - testObject = instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh); + testObject = disposables.add(instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh)); const command = `command.${uuid.generateUuid()}`; const expected = aResolvedKeybindingItem({ command, firstChord: { keyCode: KeyCode.Escape, modifiers: { altKey: true } }, isDefault: false }); prepareKeybindingService(expected, aResolvedKeybindingItem({ command: 'some_command', firstChord: { keyCode: KeyCode.Escape }, isDefault: false })); @@ -682,11 +680,11 @@ suite('KeybindingsEditorModel', () => { }); test('filter by extension', async () => { - testObject = instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh); + testObject = disposables.add(instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh)); const command1 = `command.${uuid.generateUuid()}`; const command2 = `command.${uuid.generateUuid()}`; extensions.push({ identifier: new ExtensionIdentifier('foo'), displayName: 'foo bar' }, { identifier: new ExtensionIdentifier('bar'), displayName: 'bar foo' }); - MenuRegistry.addCommand({ id: command2, title: 'title', category: 'category', source: { id: extensions[1].identifier!.value, title: extensions[1].displayName! } }); + disposables.add(MenuRegistry.addCommand({ id: command2, title: 'title', category: 'category', source: { id: extensions[1].identifier!.value, title: extensions[1].displayName! } })); const expected = aResolvedKeybindingItem({ command: command1, firstChord: { keyCode: KeyCode.Escape, modifiers: { altKey: true } }, isDefault: true, extensionId: extensions[0].identifier!.value }); prepareKeybindingService(expected, aResolvedKeybindingItem({ command: command2, isDefault: true })); @@ -707,7 +705,7 @@ suite('KeybindingsEditorModel', () => { } function registerCommandWithTitle(command: string, title: string): void { - registerAction2(class extends Action2 { + disposables.add(registerAction2(class extends Action2 { constructor() { super({ id: command, @@ -716,7 +714,7 @@ suite('KeybindingsEditorModel', () => { }); } async run(): Promise { } - }); + })); } function assertKeybindingItems(actual: ResolvedKeybindingItem[], expected: ResolvedKeybindingItem[]) { diff --git a/src/vs/workbench/services/progress/test/browser/progressIndicator.test.ts b/src/vs/workbench/services/progress/test/browser/progressIndicator.test.ts index fd0ee28a234..16f3067679c 100644 --- a/src/vs/workbench/services/progress/test/browser/progressIndicator.test.ts +++ b/src/vs/workbench/services/progress/test/browser/progressIndicator.test.ts @@ -4,6 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { AbstractProgressScope, ScopedProgressIndicator } from 'vs/workbench/services/progress/browser/progressIndicator'; class TestProgressBar { @@ -63,14 +65,20 @@ class TestProgressBar { suite('Progress Indicator', () => { + const disposables = new DisposableStore(); + + teardown(() => { + disposables.clear(); + }); + test('ScopedProgressIndicator', async () => { const testProgressBar = new TestProgressBar(); - const progressScope = new class extends AbstractProgressScope { + const progressScope = disposables.add(new class extends AbstractProgressScope { constructor() { super('test.scopeId', true); } testOnScopeOpened(scopeId: string) { super.onScopeOpened(scopeId); } testOnScopeClosed(scopeId: string): void { super.onScopeClosed(scopeId); } - }(); - const testObject = new ScopedProgressIndicator((testProgressBar), progressScope); + }()); + const testObject = disposables.add(new ScopedProgressIndicator((testProgressBar), progressScope)); // Active: Show (Infinite) let fn = testObject.show(true); @@ -117,4 +125,6 @@ suite('Progress Indicator', () => { progressScope.testOnScopeOpened('test.scopeId'); assert.strictEqual(true, testProgressBar.fDone); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/services/search/common/search.ts b/src/vs/workbench/services/search/common/search.ts index 6935850b671..46189842db4 100644 --- a/src/vs/workbench/services/search/common/search.ts +++ b/src/vs/workbench/services/search/common/search.ts @@ -26,6 +26,7 @@ export { TextSearchCompleteMessageType }; export const VIEWLET_ID = 'workbench.view.search'; export const PANEL_ID = 'workbench.panel.search'; export const VIEW_ID = 'workbench.view.search'; +export const SEARCH_RESULT_LANGUAGE_ID = 'search-result'; export const SEARCH_EXCLUDE_CONFIG = 'search.exclude'; diff --git a/src/vs/workbench/services/search/common/searchService.ts b/src/vs/workbench/services/search/common/searchService.ts index bbabce133c8..4e8fb9a5805 100644 --- a/src/vs/workbench/services/search/common/searchService.ts +++ b/src/vs/workbench/services/search/common/searchService.ts @@ -21,7 +21,7 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' import { EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { deserializeSearchError, FileMatch, ICachedSearchStats, IFileMatch, IFileQuery, IFileSearchStats, IFolderQuery, IProgressMessage, ISearchComplete, ISearchEngineStats, ISearchProgressItem, ISearchQuery, ISearchResultProvider, ISearchService, isFileMatch, isProgressMessage, ITextQuery, pathIncludedInQuery, QueryType, SearchError, SearchErrorCode, SearchProviderType } from 'vs/workbench/services/search/common/search'; +import { deserializeSearchError, FileMatch, ICachedSearchStats, IFileMatch, IFileQuery, IFileSearchStats, IFolderQuery, IProgressMessage, ISearchComplete, ISearchEngineStats, ISearchProgressItem, ISearchQuery, ISearchResultProvider, ISearchService, isFileMatch, isProgressMessage, ITextQuery, pathIncludedInQuery, QueryType, SEARCH_RESULT_LANGUAGE_ID, SearchError, SearchErrorCode, SearchProviderType } from 'vs/workbench/services/search/common/search'; import { addContextToEditorMatches, editorMatchesToTextSearchResults } from 'vs/workbench/services/search/common/searchHelpers'; export class SearchService extends Disposable implements ISearchService { @@ -462,7 +462,7 @@ export class SearchService extends Disposable implements ISearchService { } // Skip search results - if (model.getLanguageId() === 'search-result' && !(query.includePattern && query.includePattern['**/*.code-search'])) { + if (model.getLanguageId() === SEARCH_RESULT_LANGUAGE_ID && !(query.includePattern && query.includePattern['**/*.code-search'])) { // TODO: untitled search editors will be excluded from search even when include *.code-search is specified return; } diff --git a/src/vs/workbench/services/secrets/electron-sandbox/secretStorageService.ts b/src/vs/workbench/services/secrets/electron-sandbox/secretStorageService.ts index 70ae34239f7..700329e7361 100644 --- a/src/vs/workbench/services/secrets/electron-sandbox/secretStorageService.ts +++ b/src/vs/workbench/services/secrets/electron-sandbox/secretStorageService.ts @@ -31,8 +31,7 @@ export class NativeSecretStorageService extends BaseSecretStorageService { @ILogService logService: ILogService ) { super( - // TODO: rename disableKeytar to disableSecretStorage or similar - !!_environmentService.disableKeytar, + !!_environmentService.useInMemorySecretStorage, storageService, encryptionService, logService @@ -43,7 +42,7 @@ export class NativeSecretStorageService extends BaseSecretStorageService { this._sequencer.queue(key, async () => { await this.resolvedStorageService; - if (this.type !== 'persisted' && !this._environmentService.disableKeytar) { + if (this.type !== 'persisted' && !this._environmentService.useInMemorySecretStorage) { this._logService.trace('[NativeSecretStorageService] Notifying user that secrets are not being stored on disk.'); await this.notifyOfNoEncryptionOnce(); } diff --git a/src/vs/workbench/services/storage/test/browser/storageService.test.ts b/src/vs/workbench/services/storage/test/browser/storageService.test.ts index 6653a3efbd1..25472109405 100644 --- a/src/vs/workbench/services/storage/test/browser/storageService.test.ts +++ b/src/vs/workbench/services/storage/test/browser/storageService.test.ts @@ -11,6 +11,7 @@ import { URI } from 'vs/base/common/uri'; import { IStorageChangeEvent, Storage } from 'vs/base/parts/storage/common/storage'; import { flakySuite } from 'vs/base/test/common/testUtils'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { FileService } from 'vs/platform/files/common/fileService'; import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; import { NullLogService } from 'vs/platform/log/common/log'; @@ -49,7 +50,7 @@ async function createStorageService(): Promise<[DisposableStore, BrowserStorageS cacheHome: joinPath(inMemoryExtraProfileRoot, 'cache') }; - const storageService = disposables.add(new BrowserStorageService({ id: 'workspace-storage-test' }, new UserDataProfileService(inMemoryExtraProfile, new UserDataProfilesService(TestEnvironmentService, fileService, new UriIdentityService(fileService), logService)), logService)); + const storageService = disposables.add(new BrowserStorageService({ id: 'workspace-storage-test' }, disposables.add(new UserDataProfileService(inMemoryExtraProfile, new UserDataProfilesService(TestEnvironmentService, fileService, disposables.add(new UriIdentityService(fileService)), logService))), logService)); await storageService.initialize(); @@ -73,6 +74,8 @@ flakySuite('StorageService (browser)', function () { disposables.clear(); } }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); flakySuite('StorageService (browser specific)', () => { @@ -110,6 +113,8 @@ flakySuite('StorageService (browser specific)', () => { } }); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); flakySuite('IndexDBStorageDatabase (browser)', () => { @@ -117,13 +122,17 @@ flakySuite('IndexDBStorageDatabase (browser)', () => { const id = 'workspace-storage-db-test'; const logService = new NullLogService(); + const disposables = new DisposableStore(); + teardown(async () => { - const storage = await IndexedDBStorageDatabase.create({ id }, logService); + const storage = disposables.add(await IndexedDBStorageDatabase.create({ id }, logService)); await storage.clear(); + + disposables.clear(); }); test('Basics', async () => { - let storage = new Storage(await IndexedDBStorageDatabase.create({ id }, logService)); + let storage = disposables.add(new Storage(disposables.add(await IndexedDBStorageDatabase.create({ id }, logService)))); await storage.init(); @@ -145,7 +154,7 @@ flakySuite('IndexDBStorageDatabase (browser)', () => { await storage.close(); - storage = new Storage(await IndexedDBStorageDatabase.create({ id }, logService)); + storage = disposables.add(new Storage(disposables.add(await IndexedDBStorageDatabase.create({ id }, logService)))); await storage.init(); @@ -168,7 +177,7 @@ flakySuite('IndexDBStorageDatabase (browser)', () => { await storage.close(); - storage = new Storage(await IndexedDBStorageDatabase.create({ id }, logService)); + storage = disposables.add(new Storage(disposables.add(await IndexedDBStorageDatabase.create({ id }, logService)))); await storage.init(); @@ -196,7 +205,7 @@ flakySuite('IndexDBStorageDatabase (browser)', () => { await storage.close(); - storage = new Storage(await IndexedDBStorageDatabase.create({ id }, logService)); + storage = disposables.add(new Storage(disposables.add(await IndexedDBStorageDatabase.create({ id }, logService)))); await storage.init(); @@ -209,7 +218,7 @@ flakySuite('IndexDBStorageDatabase (browser)', () => { }); test('Clear', async () => { - let storage = new Storage(await IndexedDBStorageDatabase.create({ id }, logService)); + let storage = disposables.add(new Storage(disposables.add(await IndexedDBStorageDatabase.create({ id }, logService)))); await storage.init(); @@ -219,13 +228,13 @@ flakySuite('IndexDBStorageDatabase (browser)', () => { await storage.close(); - const db = await IndexedDBStorageDatabase.create({ id }, logService); - storage = new Storage(db); + const db = disposables.add(await IndexedDBStorageDatabase.create({ id }, logService)); + storage = disposables.add(new Storage(db)); await storage.init(); await db.clear(); - storage = new Storage(await IndexedDBStorageDatabase.create({ id }, logService)); + storage = disposables.add(new Storage(disposables.add(await IndexedDBStorageDatabase.create({ id }, logService)))); await storage.init(); @@ -238,7 +247,7 @@ flakySuite('IndexDBStorageDatabase (browser)', () => { }); test('Inserts and Deletes at the same time', async () => { - let storage = new Storage(await IndexedDBStorageDatabase.create({ id }, logService)); + let storage = disposables.add(new Storage(disposables.add(await IndexedDBStorageDatabase.create({ id }, logService)))); await storage.init(); @@ -248,7 +257,7 @@ flakySuite('IndexDBStorageDatabase (browser)', () => { await storage.close(); - storage = new Storage(await IndexedDBStorageDatabase.create({ id }, logService)); + storage = disposables.add(new Storage(disposables.add(await IndexedDBStorageDatabase.create({ id }, logService)))); await storage.init(); @@ -260,7 +269,7 @@ flakySuite('IndexDBStorageDatabase (browser)', () => { await storage.close(); - storage = new Storage(await IndexedDBStorageDatabase.create({ id }, logService)); + storage = disposables.add(new Storage(disposables.add(await IndexedDBStorageDatabase.create({ id }, logService)))); await storage.init(); @@ -271,9 +280,9 @@ flakySuite('IndexDBStorageDatabase (browser)', () => { }); test('Storage change event', async () => { - const storage = new Storage(await IndexedDBStorageDatabase.create({ id }, logService)); + const storage = disposables.add(new Storage(disposables.add(await IndexedDBStorageDatabase.create({ id }, logService)))); let storageChangeEvents: IStorageChangeEvent[] = []; - storage.onDidChangeStorage(e => storageChangeEvents.push(e)); + disposables.add(storage.onDidChangeStorage(e => storageChangeEvents.push(e))); await storage.init(); @@ -294,4 +303,6 @@ flakySuite('IndexDBStorageDatabase (browser)', () => { storageValueChangeEvent = storageChangeEvents.find(e => e.key === 'isExternal'); strictEqual(storageValueChangeEvent?.external, true); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/services/telemetry/test/node/commonProperties.test.ts b/src/vs/workbench/services/telemetry/test/node/commonProperties.test.ts index 8759a80f67d..8600bd13f15 100644 --- a/src/vs/workbench/services/telemetry/test/node/commonProperties.test.ts +++ b/src/vs/workbench/services/telemetry/test/node/commonProperties.test.ts @@ -8,6 +8,7 @@ import { release, hostname } from 'os'; import { resolveWorkbenchCommonProperties } from 'vs/workbench/services/telemetry/common/workbenchCommonProperties'; import { IStorageService, StorageScope, InMemoryStorageService, StorageTarget } from 'vs/platform/storage/common/storage'; import { timeout } from 'vs/base/common/async'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('Telemetry - common properties', function () { const commit: string = (undefined)!; @@ -18,6 +19,8 @@ suite('Telemetry - common properties', function () { testStorageService = new InMemoryStorageService(); }); + ensureNoDisposablesAreLeakedInTestSuite(); + test('default', function () { const props = resolveWorkbenchCommonProperties(testStorageService, release(), hostname(), commit, version, 'someMachineId', false, process); assert.ok('commitHash' in props); diff --git a/src/vs/workbench/test/browser/arrayOperation.test.ts b/src/vs/workbench/services/textMate/test/browser/arrayOperation.test.ts similarity index 100% rename from src/vs/workbench/test/browser/arrayOperation.test.ts rename to src/vs/workbench/services/textMate/test/browser/arrayOperation.test.ts diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index a60dc9a4560..4ae7f6692c6 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -168,7 +168,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // exists (network shares issue: https://github.com/microsoft/vscode/issues/13665). // Since we do not want to mark the model as orphaned, we have to check if the // file is really gone and not just a faulty file event. - await timeout(100); + await timeout(100, CancellationToken.None); if (this.isDisposed()) { newInOrphanModeValidated = true; diff --git a/src/vs/workbench/services/textfile/test/browser/browserTextFileService.io.test.ts b/src/vs/workbench/services/textfile/test/browser/browserTextFileService.io.test.ts index 58d93d4b786..4ae038ffe92 100644 --- a/src/vs/workbench/services/textfile/test/browser/browserTextFileService.io.test.ts +++ b/src/vs/workbench/services/textfile/test/browser/browserTextFileService.io.test.ts @@ -22,6 +22,7 @@ import { isWeb } from 'vs/base/common/platform'; import { IWorkingCopyFileService, WorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import { WorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; // optimization: we don't need to run this suite in native environment, // because we have nativeTextFileService.io.test.ts for it, @@ -39,18 +40,17 @@ if (isWeb) { const instantiationService = workbenchInstantiationService(undefined, disposables); const logService = new NullLogService(); - const fileService = new FileService(logService); + const fileService = disposables.add(new FileService(logService)); - fileProvider = new TestInMemoryFileSystemProvider(); + fileProvider = disposables.add(new TestInMemoryFileSystemProvider()); disposables.add(fileService.registerProvider(Schemas.file, fileProvider)); - disposables.add(fileProvider); const collection = new ServiceCollection(); collection.set(IFileService, fileService); + collection.set(IWorkingCopyFileService, disposables.add(new WorkingCopyFileService(fileService, disposables.add(new WorkingCopyService()), instantiationService, disposables.add(new UriIdentityService(fileService))))); - collection.set(IWorkingCopyFileService, new WorkingCopyFileService(fileService, new WorkingCopyService(), instantiationService, new UriIdentityService(fileService))); - - service = instantiationService.createChild(collection).createInstance(TestBrowserTextFileServiceWithEncodingOverrides); + service = disposables.add(instantiationService.createChild(collection).createInstance(TestBrowserTextFileServiceWithEncodingOverrides)); + disposables.add(service.files); await fileProvider.mkdir(URI.file(testDir)); for (const fileName in files) { @@ -65,8 +65,6 @@ if (isWeb) { }, teardown: async () => { - (service.files).dispose(); - disposables.clear(); }, @@ -111,5 +109,7 @@ if (isWeb) { return null; // ignore errors (like file not found) } } + + ensureNoDisposablesAreLeakedInTestSuite(); }); } diff --git a/src/vs/workbench/services/textfile/test/browser/textEditorService.test.ts b/src/vs/workbench/services/textfile/test/browser/textEditorService.test.ts index 9c051e10cbc..4e1ea75097f 100644 --- a/src/vs/workbench/services/textfile/test/browser/textEditorService.test.ts +++ b/src/vs/workbench/services/textfile/test/browser/textEditorService.test.ts @@ -11,7 +11,7 @@ import { TextResourceEditorInput } from 'vs/workbench/common/editor/textResource import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/fileEditorInput'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; -import { toResource } from 'vs/base/test/common/utils'; +import { ensureNoDisposablesAreLeakedInTestSuite, toResource } from 'vs/base/test/common/utils'; import { IFileService } from 'vs/platform/files/common/files'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { UntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; @@ -22,6 +22,7 @@ import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEdit import { ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; import { TextEditorService } from 'vs/workbench/services/textfile/common/textEditorService'; import { ILanguageService } from 'vs/editor/common/languages/language'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; suite('TextEditorService', () => { @@ -51,22 +52,22 @@ suite('TextEditorService', () => { test('createTextEditor - basics', async function () { const instantiationService = workbenchInstantiationService(undefined, disposables); const languageService = instantiationService.get(ILanguageService); - const service = instantiationService.createInstance(TextEditorService); + const service = disposables.add(instantiationService.createInstance(TextEditorService)); const languageId = 'create-input-test'; - const registration = languageService.registerLanguage({ + disposables.add(languageService.registerLanguage({ id: languageId, - }); + })); // Untyped Input (file) - let input = service.createTextEditor({ resource: toResource.call(this, '/index.html'), options: { selection: { startLineNumber: 1, startColumn: 1 } } }); + let input: EditorInput = disposables.add(service.createTextEditor({ resource: toResource.call(this, '/index.html'), options: { selection: { startLineNumber: 1, startColumn: 1 } } })); assert(input instanceof FileEditorInput); let contentInput = input; assert.strictEqual(contentInput.resource.fsPath, toResource.call(this, '/index.html').fsPath); // Untyped Input (file casing) - input = service.createTextEditor({ resource: toResource.call(this, '/index.html') }); - const inputDifferentCase = service.createTextEditor({ resource: toResource.call(this, '/INDEX.html') }); + input = disposables.add(service.createTextEditor({ resource: toResource.call(this, '/index.html') })); + const inputDifferentCase = disposables.add(service.createTextEditor({ resource: toResource.call(this, '/INDEX.html') })); if (!isLinux) { assert.strictEqual(input, inputDifferentCase); @@ -77,83 +78,83 @@ suite('TextEditorService', () => { } // Typed Input - assert.strictEqual(service.createTextEditor(input), input); + assert.strictEqual(disposables.add(service.createTextEditor(input)), input); // Untyped Input (file, encoding) - input = service.createTextEditor({ resource: toResource.call(this, '/index.html'), encoding: 'utf16le', options: { selection: { startLineNumber: 1, startColumn: 1 } } }); + input = disposables.add(service.createTextEditor({ resource: toResource.call(this, '/index.html'), encoding: 'utf16le', options: { selection: { startLineNumber: 1, startColumn: 1 } } })); assert(input instanceof FileEditorInput); contentInput = input; assert.strictEqual(contentInput.getPreferredEncoding(), 'utf16le'); // Untyped Input (file, language) - input = service.createTextEditor({ resource: toResource.call(this, '/index.html'), languageId: languageId }); + input = disposables.add(service.createTextEditor({ resource: toResource.call(this, '/index.html'), languageId: languageId })); assert(input instanceof FileEditorInput); contentInput = input; assert.strictEqual(contentInput.getPreferredLanguageId(), languageId); - let fileModel = (await contentInput.resolve() as ITextFileEditorModel); + let fileModel = disposables.add((await contentInput.resolve() as ITextFileEditorModel)); assert.strictEqual(fileModel.textEditorModel?.getLanguageId(), languageId); // Untyped Input (file, contents) - input = service.createTextEditor({ resource: toResource.call(this, '/index.html'), contents: 'My contents' }); + input = disposables.add(service.createTextEditor({ resource: toResource.call(this, '/index.html'), contents: 'My contents' })); assert(input instanceof FileEditorInput); contentInput = input; - fileModel = (await contentInput.resolve() as ITextFileEditorModel); + fileModel = disposables.add((await contentInput.resolve() as ITextFileEditorModel)); assert.strictEqual(fileModel.textEditorModel?.getValue(), 'My contents'); assert.strictEqual(fileModel.isDirty(), true); // Untyped Input (file, different language) - input = service.createTextEditor({ resource: toResource.call(this, '/index.html'), languageId: 'text' }); + input = disposables.add(service.createTextEditor({ resource: toResource.call(this, '/index.html'), languageId: 'text' })); assert(input instanceof FileEditorInput); contentInput = input; assert.strictEqual(contentInput.getPreferredLanguageId(), 'text'); // Untyped Input (untitled) - input = service.createTextEditor({ resource: undefined, options: { selection: { startLineNumber: 1, startColumn: 1 } } }); + input = disposables.add(service.createTextEditor({ resource: undefined, options: { selection: { startLineNumber: 1, startColumn: 1 } } })); assert(input instanceof UntitledTextEditorInput); // Untyped Input (untitled with contents) let untypedInput: any = { contents: 'Hello Untitled', options: { selection: { startLineNumber: 1, startColumn: 1 } } }; - input = service.createTextEditor(untypedInput); + input = disposables.add(service.createTextEditor(untypedInput)); assert.ok(isUntitledResourceEditorInput(untypedInput)); assert(input instanceof UntitledTextEditorInput); - let model = await input.resolve() as UntitledTextEditorModel; + let model = disposables.add(await input.resolve() as UntitledTextEditorModel); assert.strictEqual(model.textEditorModel?.getValue(), 'Hello Untitled'); - // Untyped Input (untitled withtoUntyped2 - input = service.createTextEditor({ resource: undefined, languageId: languageId, options: { selection: { startLineNumber: 1, startColumn: 1 } } }); + // Untyped Input (untitled with language id) + input = disposables.add(service.createTextEditor({ resource: undefined, languageId: languageId, options: { selection: { startLineNumber: 1, startColumn: 1 } } })); assert(input instanceof UntitledTextEditorInput); - model = await input.resolve() as UntitledTextEditorModel; + model = disposables.add(await input.resolve() as UntitledTextEditorModel); assert.strictEqual(model.getLanguageId(), languageId); // Untyped Input (untitled with file path) - input = service.createTextEditor({ resource: URI.file('/some/path.txt'), forceUntitled: true, options: { selection: { startLineNumber: 1, startColumn: 1 } } }); + input = disposables.add(service.createTextEditor({ resource: URI.file('/some/path.txt'), forceUntitled: true, options: { selection: { startLineNumber: 1, startColumn: 1 } } })); assert(input instanceof UntitledTextEditorInput); assert.ok((input as UntitledTextEditorInput).model.hasAssociatedFilePath); // Untyped Input (untitled with untitled resource) untypedInput = { resource: URI.parse('untitled://Untitled-1'), forceUntitled: true, options: { selection: { startLineNumber: 1, startColumn: 1 } } }; assert.ok(isUntitledResourceEditorInput(untypedInput)); - input = service.createTextEditor(untypedInput); + input = disposables.add(service.createTextEditor(untypedInput)); assert(input instanceof UntitledTextEditorInput); assert.ok(!(input as UntitledTextEditorInput).model.hasAssociatedFilePath); // Untyped input (untitled with custom resource, but forceUntitled) untypedInput = { resource: URI.file('/fake'), forceUntitled: true }; assert.ok(isUntitledResourceEditorInput(untypedInput)); - input = service.createTextEditor(untypedInput); + input = disposables.add(service.createTextEditor(untypedInput)); assert(input instanceof UntitledTextEditorInput); // Untyped Input (untitled with custom resource) - const provider = instantiationService.createInstance(FileServiceProvider, 'untitled-custom'); + const provider = disposables.add(instantiationService.createInstance(FileServiceProvider, 'untitled-custom')); - input = service.createTextEditor({ resource: URI.parse('untitled-custom://some/path'), forceUntitled: true, options: { selection: { startLineNumber: 1, startColumn: 1 } } }); + input = disposables.add(service.createTextEditor({ resource: URI.parse('untitled-custom://some/path'), forceUntitled: true, options: { selection: { startLineNumber: 1, startColumn: 1 } } })); assert(input instanceof UntitledTextEditorInput); assert.ok((input as UntitledTextEditorInput).model.hasAssociatedFilePath); provider.dispose(); // Untyped Input (resource) - input = service.createTextEditor({ resource: URI.parse('custom:resource') }); + input = disposables.add(service.createTextEditor({ resource: URI.parse('custom:resource') })); assert(input instanceof TextResourceEditorInput); // Untyped Input (diff) @@ -162,8 +163,10 @@ suite('TextEditorService', () => { original: { resource: toResource.call(this, '/original.html') } }; assert.strictEqual(isResourceDiffEditorInput(resourceDiffInput), true); - input = service.createTextEditor(resourceDiffInput); + input = disposables.add(service.createTextEditor(resourceDiffInput)); assert(input instanceof DiffEditorInput); + disposables.add(input.modified); + disposables.add(input.original); assert.strictEqual(input.original.resource?.toString(), resourceDiffInput.original.resource.toString()); assert.strictEqual(input.modified.resource?.toString(), resourceDiffInput.modified.resource.toString()); const untypedDiffInput = input.toUntyped() as IResourceDiffEditorInput; @@ -176,63 +179,65 @@ suite('TextEditorService', () => { secondary: { resource: toResource.call(this, '/secondary.html') } }; assert.strictEqual(isResourceSideBySideEditorInput(sideBySideResourceInput), true); - input = service.createTextEditor(sideBySideResourceInput); + input = disposables.add(service.createTextEditor(sideBySideResourceInput)); assert(input instanceof SideBySideEditorInput); + disposables.add(input.primary); + disposables.add(input.secondary); assert.strictEqual(input.primary.resource?.toString(), sideBySideResourceInput.primary.resource.toString()); assert.strictEqual(input.secondary.resource?.toString(), sideBySideResourceInput.secondary.resource.toString()); const untypedSideBySideInput = input.toUntyped() as IResourceSideBySideEditorInput; assert.strictEqual(untypedSideBySideInput.primary.resource?.toString(), sideBySideResourceInput.primary.resource.toString()); assert.strictEqual(untypedSideBySideInput.secondary.resource?.toString(), sideBySideResourceInput.secondary.resource.toString()); - - registration.dispose(); }); test('createTextEditor- caching', function () { const instantiationService = workbenchInstantiationService(undefined, disposables); - const service = instantiationService.createInstance(TextEditorService); + const service = disposables.add(instantiationService.createInstance(TextEditorService)); // Cached Input (Files) - const fileResource1 = toResource.call(this, '/foo/bar/cache1.js'); - const fileEditorInput1 = service.createTextEditor({ resource: fileResource1 }); + const fileResource1: URI = toResource.call(this, '/foo/bar/cache1.js'); + const fileEditorInput1 = disposables.add(service.createTextEditor({ resource: fileResource1 })); assert.ok(fileEditorInput1); const fileResource2 = toResource.call(this, '/foo/bar/cache2.js'); - const fileEditorInput2 = service.createTextEditor({ resource: fileResource2 }); + const fileEditorInput2 = disposables.add(service.createTextEditor({ resource: fileResource2 })); assert.ok(fileEditorInput2); assert.notStrictEqual(fileEditorInput1, fileEditorInput2); - const fileEditorInput1Again = service.createTextEditor({ resource: fileResource1 }); + const fileEditorInput1Again = disposables.add(service.createTextEditor({ resource: fileResource1 })); assert.strictEqual(fileEditorInput1Again, fileEditorInput1); fileEditorInput1Again.dispose(); assert.ok(fileEditorInput1.isDisposed()); - const fileEditorInput1AgainAndAgain = service.createTextEditor({ resource: fileResource1 }); + const fileEditorInput1AgainAndAgain = disposables.add(service.createTextEditor({ resource: fileResource1 })); assert.notStrictEqual(fileEditorInput1AgainAndAgain, fileEditorInput1); assert.ok(!fileEditorInput1AgainAndAgain.isDisposed()); // Cached Input (Resource) const resource1 = URI.from({ scheme: 'custom', path: '/foo/bar/cache1.js' }); - const input1 = service.createTextEditor({ resource: resource1 }); + const input1 = disposables.add(service.createTextEditor({ resource: resource1 })); assert.ok(input1); const resource2 = URI.from({ scheme: 'custom', path: '/foo/bar/cache2.js' }); - const input2 = service.createTextEditor({ resource: resource2 }); + const input2 = disposables.add(service.createTextEditor({ resource: resource2 })); assert.ok(input2); assert.notStrictEqual(input1, input2); - const input1Again = service.createTextEditor({ resource: resource1 }); + const input1Again = disposables.add(service.createTextEditor({ resource: resource1 })); assert.strictEqual(input1Again, input1); input1Again.dispose(); assert.ok(input1.isDisposed()); - const input1AgainAndAgain = service.createTextEditor({ resource: resource1 }); + const input1AgainAndAgain = disposables.add(service.createTextEditor({ resource: resource1 })); assert.notStrictEqual(input1AgainAndAgain, input1); assert.ok(!input1AgainAndAgain.isDisposed()); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.integrationTest.ts b/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.integrationTest.ts index 4f4d43cf31c..866265c14fa 100644 --- a/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.integrationTest.ts +++ b/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.integrationTest.ts @@ -7,32 +7,32 @@ import * as assert from 'assert'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench/test/browser/workbenchTestServices'; -import { toResource } from 'vs/base/test/common/utils'; +import { ensureNoDisposablesAreLeakedInTestSuite, toResource } from 'vs/base/test/common/utils'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; import { createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel'; import { CancellationToken } from 'vs/base/common/cancellation'; import { URI } from 'vs/base/common/uri'; import { bufferToStream, VSBuffer } from 'vs/base/common/buffer'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; suite('Files - TextFileEditorModel (integration)', () => { - let disposables: DisposableStore; + const disposables = new DisposableStore(); + let instantiationService: IInstantiationService; let accessor: TestServiceAccessor; let content: string; setup(() => { - disposables = new DisposableStore(); instantiationService = workbenchInstantiationService(undefined, disposables); accessor = instantiationService.createInstance(TestServiceAccessor); content = accessor.fileService.getContent(); + disposables.add(toDisposable(() => accessor.fileService.setContent(content))); + disposables.add(accessor.textFileService.files); }); teardown(() => { - (accessor.textFileService.files).dispose(); - accessor.fileService.setContent(content); - disposables.dispose(); + disposables.clear(); }); test('backup and restore (simple)', async function () { @@ -45,7 +45,7 @@ suite('Files - TextFileEditorModel (integration)', () => { }); async function testBackupAndRestore(resourceA: URI, resourceB: URI, contents: string): Promise { - const originalModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, resourceA, 'utf8', undefined); + const originalModel: TextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, resourceA, 'utf8', undefined)); await originalModel.resolve({ contents: await createTextBufferFactoryFromStream(await accessor.textFileService.getDecodedStream(resourceA, bufferToStream(VSBuffer.fromString(contents)))) }); @@ -56,13 +56,12 @@ suite('Files - TextFileEditorModel (integration)', () => { const modelRestoredIdentifier = { typeId: originalModel.typeId, resource: resourceB }; await accessor.workingCopyBackupService.backup(modelRestoredIdentifier, backup.content); - const modelRestored: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, modelRestoredIdentifier.resource, 'utf8', undefined); + const modelRestored: TextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, modelRestoredIdentifier.resource, 'utf8', undefined)); await modelRestored.resolve(); assert.strictEqual(modelRestored.textEditorModel?.getValue(), contents); assert.strictEqual(modelRestored.isDirty(), true); - - originalModel.dispose(); - modelRestored.dispose(); } + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts b/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts index 7ca2a09bfb1..8528d367687 100644 --- a/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts +++ b/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts @@ -8,13 +8,13 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { EncodingMode, TextFileEditorModelState, snapshotToString, isTextFileEditorModel, ITextFileEditorModelSaveEvent } from 'vs/workbench/services/textfile/common/textfiles'; import { createFileEditorInput, workbenchInstantiationService, TestServiceAccessor, TestReadonlyTextFileEditorModel, getLastResolvedFileStat } from 'vs/workbench/test/browser/workbenchTestServices'; -import { toResource } from 'vs/base/test/common/utils'; +import { ensureNoDisposablesAreLeakedInTestSuite, toResource } from 'vs/base/test/common/utils'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; import { FileOperationResult, FileOperationError } from 'vs/platform/files/common/files'; import { DeferredPromise, timeout } from 'vs/base/common/async'; import { assertIsDefined } from 'vs/base/common/types'; import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { SaveReason, SaveSourceRegistry } from 'vs/workbench/common/editor'; import { isEqual } from 'vs/base/common/resources'; import { UTF16be } from 'vs/workbench/services/textfile/common/encoding'; @@ -28,40 +28,43 @@ suite('Files - TextFileEditorModel', () => { return stat ? stat.mtime : -1; } - let disposables: DisposableStore; + const disposables = new DisposableStore(); let instantiationService: IInstantiationService; let accessor: TestServiceAccessor; let content: string; setup(() => { - disposables = new DisposableStore(); instantiationService = workbenchInstantiationService(undefined, disposables); accessor = instantiationService.createInstance(TestServiceAccessor); content = accessor.fileService.getContent(); + disposables.add(accessor.textFileService.files); + disposables.add(toDisposable(() => accessor.fileService.setContent(content))); }); - teardown(() => { - (accessor.textFileService.files).dispose(); - accessor.fileService.setContent(content); - disposables.dispose(); + teardown(async () => { + for (const textFileEditorModel of accessor.textFileService.files.models) { + textFileEditorModel.dispose(); + } + + disposables.clear(); }); test('basic events', async function () { - const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + const model = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined)); accessor.workingCopyService.testUnregisterWorkingCopy(model); // causes issues with subsequent resolves otherwise let onDidResolveCounter = 0; - model.onDidResolve(() => onDidResolveCounter++); + disposables.add(model.onDidResolve(() => onDidResolveCounter++)); await model.resolve(); assert.strictEqual(onDidResolveCounter, 1); let onDidChangeContentCounter = 0; - model.onDidChangeContent(() => onDidChangeContentCounter++); + disposables.add(model.onDidChangeContent(() => onDidChangeContentCounter++)); let onDidChangeDirtyCounter = 0; - model.onDidChangeDirty(() => onDidChangeDirtyCounter++); + disposables.add(model.onDidChangeDirty(() => onDidChangeDirtyCounter++)); model.updateTextEditorModel(createTextBufferFactory('bar')); @@ -76,8 +79,6 @@ suite('Files - TextFileEditorModel', () => { await model.revert(); assert.strictEqual(onDidChangeDirtyCounter, 2); - - model.dispose(); }); test('isTextFileEditorModel', async function () { @@ -96,7 +97,7 @@ suite('Files - TextFileEditorModel', () => { assert.strictEqual(accessor.workingCopyService.dirtyCount, 0); let savedEvent: ITextFileEditorModelSaveEvent | undefined = undefined; - model.onDidSave(e => savedEvent = e); + disposables.add(model.onDidSave(e => savedEvent = e)); await model.save(); assert.ok(!savedEvent); @@ -110,11 +111,11 @@ suite('Files - TextFileEditorModel', () => { assert.strictEqual(accessor.workingCopyService.isDirty(model.resource, model.typeId), true); let workingCopyEvent = false; - accessor.workingCopyService.onDidChangeDirty(e => { + disposables.add(accessor.workingCopyService.onDidChangeDirty(e => { if (e.resource.toString() === model.resource.toString()) { workingCopyEvent = true; } - }); + })); const source = SaveSourceRegistry.registerSource('testSource', 'Hello Save'); const pendingSave = model.save({ reason: SaveReason.AUTO, source }); @@ -149,14 +150,14 @@ suite('Files - TextFileEditorModel', () => { await model.resolve(); let savedEvent = false; - model.onDidSave(() => savedEvent = true); + disposables.add(model.onDidSave(() => savedEvent = true)); let workingCopyEvent = false; - accessor.workingCopyService.onDidChangeDirty(e => { + disposables.add(accessor.workingCopyService.onDidChangeDirty(e => { if (e.resource.toString() === model.resource.toString()) { workingCopyEvent = true; } - }); + })); await model.save({ force: true }); @@ -173,10 +174,10 @@ suite('Files - TextFileEditorModel', () => { await model.resolve(); let saveErrorEvent = false; - model.onDidSaveError(() => saveErrorEvent = true); + disposables.add(model.onDidSaveError(() => saveErrorEvent = true)); let savedEvent = false; - model.onDidSave(() => savedEvent = true); + disposables.add(model.onDidSave(() => savedEvent = true)); accessor.fileService.writeShouldThrowError = new Error('failed to write'); try { @@ -230,7 +231,7 @@ suite('Files - TextFileEditorModel', () => { model.updateTextEditorModel(createTextBufferFactory('bar')); let saveErrorEvent = false; - model.onDidSaveError(() => saveErrorEvent = true); + disposables.add(model.onDidSaveError(() => saveErrorEvent = true)); accessor.fileService.writeShouldThrowError = new Error('failed to write'); try { @@ -261,7 +262,7 @@ suite('Files - TextFileEditorModel', () => { model.updateTextEditorModel(createTextBufferFactory('bar')); let saveErrorEvent = false; - model.onDidSaveError(() => saveErrorEvent = true); + disposables.add(model.onDidSaveError(() => saveErrorEvent = true)); accessor.fileService.writeShouldThrowError = new FileOperationError('save conflict', FileOperationResult.FILE_MODIFIED_SINCE); try { @@ -285,10 +286,10 @@ suite('Files - TextFileEditorModel', () => { }); test('setEncoding - encode', async function () { - const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + const model: TextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined)); let encodingEvent = false; - model.onDidChangeEncoding(() => encodingEvent = true); + disposables.add(model.onDidChangeEncoding(() => encodingEvent = true)); await model.setEncoding('utf8', EncodingMode.Encode); // no-op assert.strictEqual(getLastModifiedTime(model), -1); @@ -300,12 +301,10 @@ suite('Files - TextFileEditorModel', () => { assert.ok(encodingEvent); assert.ok(getLastModifiedTime(model) <= Date.now()); // indicates model was saved due to encoding change - - model.dispose(); }); test('setEncoding - decode', async function () { - let model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + let model: TextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined)); accessor.workingCopyService.testUnregisterWorkingCopy(model); // causes issues with subsequent resolves otherwise await model.setEncoding('utf16', EncodingMode.Decode); @@ -316,11 +315,10 @@ suite('Files - TextFileEditorModel', () => { model = accessor.workingCopyService.get(model) as TextFileEditorModel; assert.ok(model.isResolved()); // model got resolved due to decoding - model.dispose(); }); test('setEncoding - decode dirty file saves first', async function () { - const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + const model: TextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined)); accessor.workingCopyService.testUnregisterWorkingCopy(model); // causes issues with subsequent resolves otherwise await model.resolve(); @@ -331,18 +329,17 @@ suite('Files - TextFileEditorModel', () => { await model.setEncoding('utf16', EncodingMode.Decode); assert.strictEqual(model.isDirty(), false); - model.dispose(); }); test('encoding updates with language based configuration', async function () { const languageId = 'text-file-model-test'; - const registration = accessor.languageService.registerLanguage({ + disposables.add(accessor.languageService.registerLanguage({ id: languageId, - }); + })); accessor.testConfigurationService.setOverrideIdentifiers('files.encoding', [languageId]); - const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + const model: TextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined)); accessor.workingCopyService.testUnregisterWorkingCopy(model); // causes issues with subsequent resolves otherwise await model.resolve(); @@ -351,11 +348,11 @@ suite('Files - TextFileEditorModel', () => { // We use this listener as a way to figure out that the working // copy was resolved again as part of the language change - const listener = accessor.workingCopyService.onDidRegister(e => { + disposables.add(accessor.workingCopyService.onDidRegister(e => { if (isEqual(e.resource, model.resource)) { deferredPromise.complete(model as TextFileEditorModel); } - }); + })); accessor.testConfigurationService.setUserConfiguration('files.encoding', UTF16be); @@ -364,17 +361,13 @@ suite('Files - TextFileEditorModel', () => { await deferredPromise.p; assert.strictEqual(model.getEncoding(), UTF16be); - - model.dispose(); - listener.dispose(); - registration.dispose(); }); test('create with language', async function () { const languageId = 'text-file-model-test'; - const registration = accessor.languageService.registerLanguage({ + disposables.add(accessor.languageService.registerLanguage({ id: languageId, - }); + })); const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', languageId); @@ -384,8 +377,6 @@ suite('Files - TextFileEditorModel', () => { model.dispose(); assert.ok(!accessor.modelService.getModel(model.resource)); - - registration.dispose(); }); test('disposes when underlying model is destroyed', async function () { @@ -401,8 +392,8 @@ suite('Files - TextFileEditorModel', () => { const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index.txt'), 'utf8', undefined); assert.ok(model.hasState(TextFileEditorModelState.SAVED)); - model.onDidSave(() => assert.fail()); - model.onDidChangeDirty(() => assert.fail()); + disposables.add(model.onDidSave(() => assert.fail())); + disposables.add(model.onDidChangeDirty(() => assert.fail())); await model.resolve(); assert.ok(model.isResolved()); @@ -411,7 +402,7 @@ suite('Files - TextFileEditorModel', () => { }); test('Resolve returns dirty model as long as model is dirty', async function () { - const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + const model = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined)); await model.resolve(); model.updateTextEditorModel(createTextBufferFactory('foo')); @@ -420,7 +411,6 @@ suite('Files - TextFileEditorModel', () => { await model.resolve(); assert.ok(model.isDirty()); - model.dispose(); }); test('Resolve with contents', async function () { @@ -448,16 +438,16 @@ suite('Files - TextFileEditorModel', () => { test('Revert', async function () { let eventCounter = 0; - let model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + let model: TextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined)); - model.onDidRevert(() => eventCounter++); + disposables.add(model.onDidRevert(() => eventCounter++)); let workingCopyEvent = false; - accessor.workingCopyService.onDidChangeDirty(e => { + disposables.add(accessor.workingCopyService.onDidChangeDirty(e => { if (e.resource.toString() === model.resource.toString()) { workingCopyEvent = true; } - }); + })); await model.resolve(); model.updateTextEditorModel(createTextBufferFactory('foo')); @@ -483,23 +473,21 @@ suite('Files - TextFileEditorModel', () => { assert.ok(workingCopyEvent); assert.strictEqual(accessor.workingCopyService.dirtyCount, 0); assert.strictEqual(accessor.workingCopyService.isDirty(model.resource, model.typeId), false); - - model.dispose(); }); test('Revert (soft)', async function () { let eventCounter = 0; - const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + const model: TextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined)); - model.onDidRevert(() => eventCounter++); + disposables.add(model.onDidRevert(() => eventCounter++)); let workingCopyEvent = false; - accessor.workingCopyService.onDidChangeDirty(e => { + disposables.add(accessor.workingCopyService.onDidChangeDirty(e => { if (e.resource.toString() === model.resource.toString()) { workingCopyEvent = true; } - }); + })); await model.resolve(); model.updateTextEditorModel(createTextBufferFactory('foo')); @@ -518,12 +506,10 @@ suite('Files - TextFileEditorModel', () => { assert.ok(workingCopyEvent); assert.strictEqual(accessor.workingCopyService.dirtyCount, 0); assert.strictEqual(accessor.workingCopyService.isDirty(model.resource, model.typeId), false); - - model.dispose(); }); test('Undo to saved state turns model non-dirty', async function () { - const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + const model: TextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined)); await model.resolve(); model.updateTextEditorModel(createTextBufferFactory('Hello Text')); assert.ok(model.isDirty()); @@ -533,7 +519,7 @@ suite('Files - TextFileEditorModel', () => { }); test('Resolve and undo turns model dirty', async function () { - const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + const model: TextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined)); await model.resolve(); accessor.fileService.setContent('Hello Change'); @@ -548,7 +534,7 @@ suite('Files - TextFileEditorModel', () => { test('Update Dirty', async function () { let eventCounter = 0; - const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + const model: TextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined)); model.setDirty(true); assert.ok(!model.isDirty()); // needs to be resolved @@ -560,14 +546,14 @@ suite('Files - TextFileEditorModel', () => { await model.revert({ soft: true }); assert.strictEqual(model.isDirty(), false); - model.onDidChangeDirty(() => eventCounter++); + disposables.add(model.onDidChangeDirty(() => eventCounter++)); let workingCopyEvent = false; - accessor.workingCopyService.onDidChangeDirty(e => { + disposables.add(accessor.workingCopyService.onDidChangeDirty(e => { if (e.resource.toString() === model.resource.toString()) { workingCopyEvent = true; } - }); + })); model.setDirty(true); assert.ok(model.isDirty()); @@ -577,24 +563,22 @@ suite('Files - TextFileEditorModel', () => { model.setDirty(false); assert.strictEqual(model.isDirty(), false); assert.strictEqual(eventCounter, 2); - - model.dispose(); }); test('No Dirty or saving for readonly models', async function () { let workingCopyEvent = false; - accessor.workingCopyService.onDidChangeDirty(e => { + disposables.add(accessor.workingCopyService.onDidChangeDirty(e => { if (e.resource.toString() === model.resource.toString()) { workingCopyEvent = true; } - }); + })); - const model = instantiationService.createInstance(TestReadonlyTextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + const model = disposables.add(instantiationService.createInstance(TestReadonlyTextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined)); let saveEvent = false; - model.onDidSave(() => { + disposables.add(model.onDidSave(() => { saveEvent = true; - }); + })); await model.resolve(); model.updateTextEditorModel(createTextBufferFactory('foo')); @@ -607,12 +591,10 @@ suite('Files - TextFileEditorModel', () => { assert.ok(!model.isDirty()); assert.ok(!workingCopyEvent); - - model.dispose(); }); test('File not modified error is handled gracefully', async function () { - const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + const model: TextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined)); await model.resolve(); @@ -623,26 +605,24 @@ suite('Files - TextFileEditorModel', () => { assert.ok(model); assert.strictEqual(getLastModifiedTime(model), mtime); - model.dispose(); }); test('Resolve error is handled gracefully if model already exists', async function () { - const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + const model: TextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined)); await model.resolve(); accessor.textFileService.setReadStreamErrorOnce(new FileOperationError('error', FileOperationResult.FILE_NOT_FOUND)); await model.resolve(); assert.ok(model); - model.dispose(); }); test('save() and isDirty() - proper with check for mtimes', async function () { const input1 = disposables.add(createFileEditorInput(instantiationService, toResource.call(this, '/path/index_async2.txt'))); const input2 = disposables.add(createFileEditorInput(instantiationService, toResource.call(this, '/path/index_async.txt'))); - const model1 = await input1.resolve() as TextFileEditorModel; - const model2 = await input2.resolve() as TextFileEditorModel; + const model1 = disposables.add(await input1.resolve() as TextFileEditorModel); + const model2 = disposables.add(await input2.resolve() as TextFileEditorModel); model1.updateTextEditorModel(createTextBufferFactory('foo')); @@ -674,20 +654,17 @@ suite('Files - TextFileEditorModel', () => { assert.ok(assertIsDefined(getLastResolvedFileStat(model1)).mtime > m1Mtime); assert.ok(assertIsDefined(getLastResolvedFileStat(model2)).mtime > m2Mtime); } - - model1.dispose(); - model2.dispose(); }); test('Save Participant', async function () { let eventCounter = 0; - const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + const model: TextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined)); - model.onDidSave(() => { + disposables.add(model.onDidSave(() => { assert.strictEqual(snapshotToString(model.createSnapshot()!), eventCounter === 1 ? 'bar' : 'foobar'); assert.ok(!model.isDirty()); eventCounter++; - }); + })); const participant = accessor.textFileService.files.addSaveParticipant({ participate: async model => { @@ -711,40 +688,35 @@ suite('Files - TextFileEditorModel', () => { await model.save(); assert.strictEqual(eventCounter, 3); - - model.dispose(); }); test('Save Participant - skip', async function () { let eventCounter = 0; - const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + const model: TextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined)); - const participant = accessor.textFileService.files.addSaveParticipant({ + disposables.add(accessor.textFileService.files.addSaveParticipant({ participate: async () => { eventCounter++; } - }); + })); await model.resolve(); model.updateTextEditorModel(createTextBufferFactory('foo')); await model.save({ skipSaveParticipants: true }); assert.strictEqual(eventCounter, 0); - - participant.dispose(); - model.dispose(); }); test('Save Participant, async participant', async function () { let eventCounter = 0; - const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + const model: TextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined)); - model.onDidSave(() => { + disposables.add(model.onDidSave(() => { assert.ok(!model.isDirty()); eventCounter++; - }); + })); - const participant = accessor.textFileService.files.addSaveParticipant({ + disposables.add(accessor.textFileService.files.addSaveParticipant({ participate: model => { assert.ok(model.isDirty()); (model as TextFileEditorModel).updateTextEditorModel(createTextBufferFactory('bar')); @@ -753,7 +725,7 @@ suite('Files - TextFileEditorModel', () => { return timeout(10); } - }); + })); await model.resolve(); model.updateTextEditorModel(createTextBufferFactory('foo')); @@ -762,35 +734,29 @@ suite('Files - TextFileEditorModel', () => { await model.save(); assert.strictEqual(eventCounter, 2); assert.ok(Date.now() - now >= 10); - - model.dispose(); - participant.dispose(); }); test('Save Participant, bad participant', async function () { - const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + const model: TextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined)); - const participant = accessor.textFileService.files.addSaveParticipant({ + disposables.add(accessor.textFileService.files.addSaveParticipant({ participate: async () => { new Error('boom'); } - }); + })); await model.resolve(); model.updateTextEditorModel(createTextBufferFactory('foo')); await model.save(); - - model.dispose(); - participant.dispose(); }); test('Save Participant, participant cancelled when saved again', async function () { - const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + const model: TextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined)); const participations: boolean[] = []; - const participant = accessor.textFileService.files.addSaveParticipant({ + disposables.add(accessor.textFileService.files.addSaveParticipant({ participate: async (model, context, progress, token) => { await timeout(10); @@ -798,7 +764,7 @@ suite('Files - TextFileEditorModel', () => { participations.push(true); } } - }); + })); await model.resolve(); @@ -816,54 +782,41 @@ suite('Files - TextFileEditorModel', () => { await Promise.all([p1, p2, p3, p4]); assert.strictEqual(participations.length, 1); - - model.dispose(); - participant.dispose(); }); test('Save Participant, calling save from within is unsupported but does not explode (sync save, no model change)', async function () { - const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + const model: TextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined)); await testSaveFromSaveParticipant(model, false, false, false); - - model.dispose(); }); test('Save Participant, calling save from within is unsupported but does not explode (async save, no model change)', async function () { - const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + const model: TextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined)); await testSaveFromSaveParticipant(model, true, false, false); - - model.dispose(); }); test('Save Participant, calling save from within is unsupported but does not explode (sync save, model change)', async function () { - const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + const model: TextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined)); await testSaveFromSaveParticipant(model, false, true, false); - - model.dispose(); }); test('Save Participant, calling save from within is unsupported but does not explode (async save, model change)', async function () { - const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + const model: TextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined)); await testSaveFromSaveParticipant(model, true, true, false); - - model.dispose(); }); test('Save Participant, calling save from within is unsupported but does not explode (force)', async function () { - const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + const model: TextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined)); await testSaveFromSaveParticipant(model, false, false, true); - - model.dispose(); }); async function testSaveFromSaveParticipant(model: TextFileEditorModel, async: boolean, modelChange: boolean, force: boolean): Promise { - const disposable = accessor.textFileService.files.addSaveParticipant({ + disposables.add(accessor.textFileService.files.addSaveParticipant({ participate: async () => { if (async) { await timeout(10); @@ -887,14 +840,14 @@ suite('Files - TextFileEditorModel', () => { await savePromise; } } - }); + })); await model.resolve(); model.updateTextEditorModel(createTextBufferFactory('foo')); const savePromise = model.save(force ? { force } : undefined); await savePromise; - - disposable.dispose(); } + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/services/textfile/test/browser/textFileEditorModelManager.test.ts b/src/vs/workbench/services/textfile/test/browser/textFileEditorModelManager.test.ts index 3a46ef121f9..6cbeccc8876 100644 --- a/src/vs/workbench/services/textfile/test/browser/textFileEditorModelManager.test.ts +++ b/src/vs/workbench/services/textfile/test/browser/textFileEditorModelManager.test.ts @@ -9,35 +9,35 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { workbenchInstantiationService, TestServiceAccessor, ITestTextFileEditorModelManager } from 'vs/workbench/test/browser/workbenchTestServices'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { FileChangesEvent, FileChangeType, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; -import { toResource } from 'vs/base/test/common/utils'; +import { ensureNoDisposablesAreLeakedInTestSuite, toResource } from 'vs/base/test/common/utils'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; import { ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; import { timeout } from 'vs/base/common/async'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; suite('Files - TextFileEditorModelManager', () => { - let disposables: DisposableStore; + const disposables = new DisposableStore(); let instantiationService: IInstantiationService; let accessor: TestServiceAccessor; setup(() => { - disposables = new DisposableStore(); instantiationService = workbenchInstantiationService(undefined, disposables); accessor = instantiationService.createInstance(TestServiceAccessor); + disposables.add(toDisposable(() => accessor.textFileService.files as ITestTextFileEditorModelManager)); }); teardown(() => { - disposables.dispose(); + disposables.clear(); }); test('add, remove, clear, get, getAll', function () { const manager = accessor.textFileService.files as ITestTextFileEditorModelManager; - const model1: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random1.txt'), 'utf8', undefined); - const model2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random2.txt'), 'utf8', undefined); - const model3: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random3.txt'), 'utf8', undefined); + const model1: TextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random1.txt'), 'utf8', undefined)); + const model2: TextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random2.txt'), 'utf8', undefined)); + const model3: TextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random3.txt'), 'utf8', undefined)); manager.add(URI.file('/test.html'), model1); manager.add(URI.file('/some/other.html'), model2); @@ -81,12 +81,6 @@ suite('Files - TextFileEditorModelManager', () => { manager.dispose(); results = manager.models; assert.strictEqual(0, results.length); - - model1.dispose(); - model2.dispose(); - model3.dispose(); - - manager.dispose(); }); test('resolve', async () => { @@ -95,9 +89,9 @@ suite('Files - TextFileEditorModelManager', () => { const encoding = 'utf8'; const events: ITextFileEditorModel[] = []; - const listener = manager.onDidCreate(model => { + disposables.add(manager.onDidCreate(model => { events.push(model); - }); + })); const modelPromise = manager.resolve(resource, { encoding }); assert.ok(manager.get(resource)); // model known even before resolved() @@ -119,30 +113,22 @@ suite('Files - TextFileEditorModelManager', () => { assert.strictEqual(events.length, 2); assert.strictEqual(events[0].resource.toString(), model1.resource.toString()); assert.strictEqual(events[1].resource.toString(), model2.resource.toString()); - - listener.dispose(); - - model1.dispose(); - model2.dispose(); - model3.dispose(); - - manager.dispose(); }); test('resolve (async)', async () => { const manager = accessor.textFileService.files as ITestTextFileEditorModelManager; const resource = URI.file('/path/index.txt'); - await manager.resolve(resource); + disposables.add(await manager.resolve(resource)); let didResolve = false; const onDidResolve = new Promise(resolve => { - manager.onDidResolve(({ model }) => { + disposables.add(manager.onDidResolve(({ model }) => { if (model.resource.toString() === resource.toString()) { didResolve = true; resolve(); } - }); + })); }); manager.resolve(resource, { reload: { async: true } }); @@ -156,14 +142,14 @@ suite('Files - TextFileEditorModelManager', () => { const manager = accessor.textFileService.files as ITestTextFileEditorModelManager; const resource = URI.file('/path/index.txt'); - await manager.resolve(resource); + disposables.add(await manager.resolve(resource)); let didResolve = false; - manager.onDidResolve(({ model }) => { + disposables.add(manager.onDidResolve(({ model }) => { if (model.resource.toString() === resource.toString()) { didResolve = true; } - }); + })); await manager.resolve(resource, { reload: { async: false } }); assert.strictEqual(didResolve, true); @@ -177,7 +163,7 @@ suite('Files - TextFileEditorModelManager', () => { let error: Error | undefined = undefined; try { - await manager.resolve(resource); + disposables.add(await manager.resolve(resource)); } catch (e) { error = e; } @@ -190,13 +176,13 @@ suite('Files - TextFileEditorModelManager', () => { const manager = accessor.textFileService.files as ITestTextFileEditorModelManager; const resource = URI.file('/path/index.txt'); - await manager.resolve(resource); + disposables.add(await manager.resolve(resource)); accessor.textFileService.setReadStreamErrorOnce(new FileOperationError('fail', FileOperationResult.FILE_OTHER_ERROR)); let error: Error | undefined = undefined; try { - await manager.resolve(resource, { reload: { async: false } }); + disposables.add(await manager.resolve(resource, { reload: { async: false } })); } catch (e) { error = e; } @@ -209,16 +195,13 @@ suite('Files - TextFileEditorModelManager', () => { const manager = accessor.textFileService.files as ITestTextFileEditorModelManager; const resource = URI.file('/test.html'); - const model = await manager.resolve(resource, { contents: createTextBufferFactory('Hello World') }); + const model = disposables.add(await manager.resolve(resource, { contents: createTextBufferFactory('Hello World') })); assert.strictEqual(model.textEditorModel?.getValue(), 'Hello World'); assert.strictEqual(model.isDirty(), true); - await manager.resolve(resource, { contents: createTextBufferFactory('More Changes') }); + disposables.add(await manager.resolve(resource, { contents: createTextBufferFactory('More Changes') })); assert.strictEqual(model.textEditorModel?.getValue(), 'More Changes'); assert.strictEqual(model.isDirty(), true); - - model.dispose(); - manager.dispose(); }); test('multiple resolves execute in sequence', async () => { @@ -228,12 +211,12 @@ suite('Files - TextFileEditorModelManager', () => { let resolvedModel: unknown; const contents: string[] = []; - manager.onDidResolve(e => { + disposables.add(manager.onDidResolve(e => { if (e.model.resource.toString() === resource.toString()) { - resolvedModel = e.model as TextFileEditorModel; + resolvedModel = disposables.add(e.model as TextFileEditorModel); contents.push(e.model.textEditorModel!.getValue()); } - }); + })); await Promise.all([ manager.resolve(resource), @@ -250,17 +233,14 @@ suite('Files - TextFileEditorModelManager', () => { assert.strictEqual(contents[0], 'Hello Html'); assert.strictEqual(contents[1], 'Hello World'); assert.strictEqual(contents[2], 'More Changes'); - - resolvedModel.dispose(); - manager.dispose(); }); test('removed from cache when model disposed', function () { const manager = accessor.textFileService.files as ITestTextFileEditorModelManager; - const model1: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random1.txt'), 'utf8', undefined); - const model2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random2.txt'), 'utf8', undefined); - const model3: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random3.txt'), 'utf8', undefined); + const model1: TextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random1.txt'), 'utf8', undefined)); + const model2: TextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random2.txt'), 'utf8', undefined)); + const model3: TextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random3.txt'), 'utf8', undefined)); manager.add(URI.file('/test.html'), model1); manager.add(URI.file('/some/other.html'), model2); @@ -270,11 +250,6 @@ suite('Files - TextFileEditorModelManager', () => { model1.dispose(); assert(!manager.get(URI.file('/test.html'))); - - model2.dispose(); - model3.dispose(); - - manager.dispose(); }); test('events', async function () { @@ -291,19 +266,19 @@ suite('Files - TextFileEditorModelManager', () => { let savedCounter = 0; let encodingCounter = 0; - manager.onDidResolve(({ model }) => { + disposables.add(manager.onDidResolve(({ model }) => { if (model.resource.toString() === resource1.toString()) { resolvedCounter++; } - }); + })); - manager.onDidRemove(resource => { + disposables.add(manager.onDidRemove(resource => { if (resource.toString() === resource1.toString() || resource.toString() === resource2.toString()) { removedCounter++; } - }); + })); - manager.onDidChangeDirty(model => { + disposables.add(manager.onDidChangeDirty(model => { if (model.resource.toString() === resource1.toString()) { if (model.isDirty()) { gotDirtyCounter++; @@ -311,25 +286,25 @@ suite('Files - TextFileEditorModelManager', () => { gotNonDirtyCounter++; } } - }); + })); - manager.onDidRevert(model => { + disposables.add(manager.onDidRevert(model => { if (model.resource.toString() === resource1.toString()) { revertedCounter++; } - }); + })); - manager.onDidSave(({ model }) => { + disposables.add(manager.onDidSave(({ model }) => { if (model.resource.toString() === resource1.toString()) { savedCounter++; } - }); + })); - manager.onDidChangeEncoding(model => { + disposables.add(manager.onDidChangeEncoding(model => { if (model.resource.toString() === resource1.toString()) { encodingCounter++; } - }); + })); const model1 = await manager.resolve(resource1, { encoding: 'utf8' }); assert.strictEqual(resolvedCounter, 1); @@ -362,8 +337,6 @@ suite('Files - TextFileEditorModelManager', () => { model2.dispose(); assert.ok(!accessor.modelService.getModel(resource1)); assert.ok(!accessor.modelService.getModel(resource2)); - - manager.dispose(); }); test('disposing model takes it out of the manager', async function () { @@ -375,7 +348,6 @@ suite('Files - TextFileEditorModelManager', () => { model.dispose(); assert.ok(!manager.get(resource)); assert.ok(!accessor.modelService.getModel(model.resource)); - manager.dispose(); }); test('canDispose with dirty model', async function () { @@ -383,7 +355,7 @@ suite('Files - TextFileEditorModelManager', () => { const resource = toResource.call(this, '/path/index_something.txt'); - const model = await manager.resolve(resource, { encoding: 'utf8' }); + const model = disposables.add(await manager.resolve(resource, { encoding: 'utf8' })); (model as TextFileEditorModel).updateTextEditorModel(createTextBufferFactory('make dirty')); const canDisposePromise = manager.canDispose(model as TextFileEditorModel); @@ -403,46 +375,40 @@ suite('Files - TextFileEditorModelManager', () => { const canDispose2 = manager.canDispose(model as TextFileEditorModel); assert.strictEqual(canDispose2, true); - - manager.dispose(); }); test('language', async function () { const languageId = 'text-file-model-manager-test'; - const registration = accessor.languageService.registerLanguage({ + disposables.add(accessor.languageService.registerLanguage({ id: languageId, - }); + })); const manager = accessor.textFileService.files as ITestTextFileEditorModelManager; - const resource = toResource.call(this, '/path/index_something.txt'); + const resource: URI = toResource.call(this, '/path/index_something.txt'); - let model = await manager.resolve(resource, { languageId: languageId }); + let model = disposables.add(await manager.resolve(resource, { languageId: languageId })); assert.strictEqual(model.textEditorModel!.getLanguageId(), languageId); model = await manager.resolve(resource, { languageId: 'text' }); assert.strictEqual(model.textEditorModel!.getLanguageId(), PLAINTEXT_LANGUAGE_ID); - - model.dispose(); - manager.dispose(); - registration.dispose(); }); test('file change events trigger reload (on a resolved model)', async () => { const manager = accessor.textFileService.files as ITestTextFileEditorModelManager; const resource = URI.file('/path/index.txt'); - await manager.resolve(resource); + disposables.add(await manager.resolve(resource)); let didResolve = false; const onDidResolve = new Promise(resolve => { - manager.onDidResolve(({ model }) => { + disposables.add(manager.onDidResolve(({ model }) => { if (model.resource.toString() === resource.toString()) { didResolve = true; resolve(); } - }); + })); }); accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.UPDATED }], false)); @@ -460,7 +426,8 @@ suite('Files - TextFileEditorModelManager', () => { let didResolve = false; let resolvedCounter = 0; const onDidResolve = new Promise(resolve => { - manager.onDidResolve(({ model }) => { + disposables.add(manager.onDidResolve(({ model }) => { + disposables.add(model); if (model.resource.toString() === resource.toString()) { resolvedCounter++; if (resolvedCounter === 2) { @@ -468,7 +435,7 @@ suite('Files - TextFileEditorModelManager', () => { resolve(); } } - }); + })); }); accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.UPDATED }], false)); @@ -476,4 +443,6 @@ suite('Files - TextFileEditorModelManager', () => { await onDidResolve; assert.strictEqual(didResolve, true); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts b/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts index 45bd9ba71fd..7730d1149a5 100644 --- a/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts +++ b/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { workbenchInstantiationService, TestServiceAccessor, ITestTextFileEditorModelManager } from 'vs/workbench/test/browser/workbenchTestServices'; -import { toResource } from 'vs/base/test/common/utils'; +import { ensureNoDisposablesAreLeakedInTestSuite, toResource } from 'vs/base/test/common/utils'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { FileOperation } from 'vs/platform/files/common/files'; @@ -13,25 +13,22 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; suite('Files - TextFileService', () => { - let disposables: DisposableStore; + const disposables = new DisposableStore(); let instantiationService: IInstantiationService; - let model: TextFileEditorModel; let accessor: TestServiceAccessor; setup(() => { - disposables = new DisposableStore(); instantiationService = workbenchInstantiationService(undefined, disposables); accessor = instantiationService.createInstance(TestServiceAccessor); + disposables.add(accessor.textFileService.files); }); teardown(() => { - model?.dispose(); - (accessor.textFileService.files).dispose(); - disposables.dispose(); + disposables.clear(); }); test('isDirty/getDirty - files and untitled', async function () { - model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined); + const model: TextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined)); (accessor.textFileService.files).add(model.resource, model); await model.resolve(); @@ -41,19 +38,16 @@ suite('Files - TextFileService', () => { assert.ok(accessor.textFileService.isDirty(model.resource)); - const untitled = await accessor.textFileService.untitled.resolve(); + const untitled = disposables.add(await accessor.textFileService.untitled.resolve()); assert.ok(!accessor.textFileService.isDirty(untitled.resource)); untitled.textEditorModel?.setValue('changed'); assert.ok(accessor.textFileService.isDirty(untitled.resource)); - - untitled.dispose(); - model.dispose(); }); test('save - file', async function () { - model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined); + const model: TextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined)); (accessor.textFileService.files).add(model.resource, model); await model.resolve(); @@ -66,7 +60,7 @@ suite('Files - TextFileService', () => { }); test('saveAll - file', async function () { - model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined); + const model: TextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined)); (accessor.textFileService.files).add(model.resource, model); await model.resolve(); @@ -79,7 +73,7 @@ suite('Files - TextFileService', () => { }); test('saveAs - file', async function () { - model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined); + const model: TextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined)); (accessor.textFileService.files).add(model.resource, model); accessor.fileDialogService.setPickFileToSave(model.resource); @@ -93,7 +87,7 @@ suite('Files - TextFileService', () => { }); test('revert - file', async function () { - model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined); + const model: TextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined)); (accessor.textFileService.files).add(model.resource, model); accessor.fileDialogService.setPickFileToSave(model.resource); @@ -106,7 +100,7 @@ suite('Files - TextFileService', () => { }); test('create does not overwrite existing model', async function () { - model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined); + const model: TextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined)); (accessor.textFileService.files).add(model.resource, model); await model.resolve(); @@ -115,103 +109,95 @@ suite('Files - TextFileService', () => { let eventCounter = 0; - const disposable1 = accessor.workingCopyFileService.addFileOperationParticipant({ + disposables.add(accessor.workingCopyFileService.addFileOperationParticipant({ participate: async files => { assert.strictEqual(files[0].target.toString(), model.resource.toString()); eventCounter++; } - }); + })); - const disposable2 = accessor.workingCopyFileService.onDidRunWorkingCopyFileOperation(e => { + disposables.add(accessor.workingCopyFileService.onDidRunWorkingCopyFileOperation(e => { assert.strictEqual(e.operation, FileOperation.CREATE); assert.strictEqual(e.files[0].target.toString(), model.resource.toString()); eventCounter++; - }); + })); await accessor.textFileService.create([{ resource: model.resource, value: 'Foo' }]); assert.ok(!accessor.textFileService.isDirty(model.resource)); assert.strictEqual(eventCounter, 2); - - disposable1.dispose(); - disposable2.dispose(); }); test('Filename Suggestion - Suggest prefix only when there are no relevant extensions', () => { - const registration = accessor.languageService.registerLanguage({ + disposables.add(accessor.languageService.registerLanguage({ id: 'plumbus0', extensions: ['.one', '.two'] - }); + })); const suggested = accessor.textFileService.suggestFilename('shleem', 'Untitled-1'); assert.strictEqual(suggested, 'Untitled-1'); - registration.dispose(); }); test('Filename Suggestion - Suggest prefix with first extension', () => { - const registration = accessor.languageService.registerLanguage({ + disposables.add(accessor.languageService.registerLanguage({ id: 'plumbus1', extensions: ['.shleem', '.gazorpazorp'], filenames: ['plumbus'] - }); + })); const suggested = accessor.textFileService.suggestFilename('plumbus1', 'Untitled-1'); assert.strictEqual(suggested, 'Untitled-1.shleem'); - registration.dispose(); }); test('Filename Suggestion - Preserve extension if it matchers', () => { - const registration = accessor.languageService.registerLanguage({ + disposables.add(accessor.languageService.registerLanguage({ id: 'plumbus2', extensions: ['.shleem', '.gazorpazorp'], - }); + })); const suggested = accessor.textFileService.suggestFilename('plumbus2', 'Untitled-1.gazorpazorp'); assert.strictEqual(suggested, 'Untitled-1.gazorpazorp'); - registration.dispose(); }); test('Filename Suggestion - Rewrite extension according to language', () => { - const registration = accessor.languageService.registerLanguage({ + disposables.add(accessor.languageService.registerLanguage({ id: 'plumbus2', extensions: ['.shleem', '.gazorpazorp'], - }); + })); const suggested = accessor.textFileService.suggestFilename('plumbus2', 'Untitled-1.foobar'); assert.strictEqual(suggested, 'Untitled-1.shleem'); - registration.dispose(); }); test('Filename Suggestion - Suggest filename if there are no extensions', () => { - const registration = accessor.languageService.registerLanguage({ + disposables.add(accessor.languageService.registerLanguage({ id: 'plumbus2', filenames: ['plumbus', 'shleem', 'gazorpazorp'] - }); + })); const suggested = accessor.textFileService.suggestFilename('plumbus2', 'Untitled-1'); assert.strictEqual(suggested, 'plumbus'); - registration.dispose(); }); test('Filename Suggestion - Preserve filename if it matches', () => { - const registration = accessor.languageService.registerLanguage({ + disposables.add(accessor.languageService.registerLanguage({ id: 'plumbus2', filenames: ['plumbus', 'shleem', 'gazorpazorp'] - }); + })); const suggested = accessor.textFileService.suggestFilename('plumbus2', 'gazorpazorp'); assert.strictEqual(suggested, 'gazorpazorp'); - registration.dispose(); }); test('Filename Suggestion - Rewrites filename according to language', () => { - const registration = accessor.languageService.registerLanguage({ + disposables.add(accessor.languageService.registerLanguage({ id: 'plumbus2', filenames: ['plumbus', 'shleem', 'gazorpazorp'] - }); + })); const suggested = accessor.textFileService.suggestFilename('plumbus2', 'foobar'); assert.strictEqual(suggested, 'plumbus'); - registration.dispose(); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/services/textfile/test/common/textFileService.io.test.ts b/src/vs/workbench/services/textfile/test/common/textFileService.io.test.ts index 3bc8637e4aa..02f4f1e4bc2 100644 --- a/src/vs/workbench/services/textfile/test/common/textFileService.io.test.ts +++ b/src/vs/workbench/services/textfile/test/common/textFileService.io.test.ts @@ -13,6 +13,7 @@ import { createTextModel } from 'vs/editor/test/common/testTextModel'; import { ITextSnapshot, DefaultEndOfLine } from 'vs/editor/common/model'; import { isWindows } from 'vs/base/common/platform'; import { createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel'; +import { DisposableStore } from 'vs/base/common/lifecycle'; export interface Params { setup(): Promise<{ @@ -40,6 +41,7 @@ export default function createSuite(params: Params) { let service: ITextFileService; let testDir = ''; const { exists, stat, readFile, detectEncodingByBOM } = params; + const disposables = new DisposableStore(); setup(async () => { const result = await params.setup(); @@ -49,6 +51,7 @@ export default function createSuite(params: Params) { teardown(async () => { await params.teardown(); + disposables.clear(); }); test('create - no encoding - content empty', async () => { @@ -165,9 +168,9 @@ export default function createSuite(params: Params) { }); function createTextModelSnapshot(text: string, preserveBOM?: boolean): ITextSnapshot { - const textModel = createTextModel(text); + const textModel = disposables.add(createTextModel(text)); const snapshot = textModel.createSnapshot(preserveBOM); - textModel.dispose(); + return snapshot; } @@ -224,7 +227,8 @@ export default function createSuite(params: Params) { const resolved = await service.readStream(resource); assert.strictEqual(resolved.encoding, encoding); - assert.strictEqual(snapshotToString(resolved.value.create(isWindows ? DefaultEndOfLine.CRLF : DefaultEndOfLine.LF).textBuffer.createSnapshot(false)), expectedContent); + const textBuffer = disposables.add(resolved.value.create(isWindows ? DefaultEndOfLine.CRLF : DefaultEndOfLine.LF).textBuffer); + assert.strictEqual(snapshotToString(textBuffer.createSnapshot(false)), expectedContent); } test('write - use encoding (cp1252)', async () => { @@ -252,18 +256,21 @@ export default function createSuite(params: Params) { async function testEncodingKeepsData(resource: URI, encoding: string, expected: string) { let resolved = await service.readStream(resource, { encoding }); - const content = snapshotToString(resolved.value.create(isWindows ? DefaultEndOfLine.CRLF : DefaultEndOfLine.LF).textBuffer.createSnapshot(false)); + const textBuffer = disposables.add(resolved.value.create(isWindows ? DefaultEndOfLine.CRLF : DefaultEndOfLine.LF).textBuffer); + const content = snapshotToString(textBuffer.createSnapshot(false)); assert.strictEqual(content, expected); await service.write(resource, content, { encoding }); resolved = await service.readStream(resource, { encoding }); - assert.strictEqual(snapshotToString(resolved.value.create(DefaultEndOfLine.CRLF).textBuffer.createSnapshot(false)), content); + const textBuffer2 = disposables.add(resolved.value.create(DefaultEndOfLine.CRLF).textBuffer); + assert.strictEqual(snapshotToString(textBuffer2.createSnapshot(false)), content); await service.write(resource, createTextModelSnapshot(content), { encoding }); resolved = await service.readStream(resource, { encoding }); - assert.strictEqual(snapshotToString(resolved.value.create(DefaultEndOfLine.CRLF).textBuffer.createSnapshot(false)), content); + const textBuffer3 = disposables.add(resolved.value.create(DefaultEndOfLine.CRLF).textBuffer); + assert.strictEqual(snapshotToString(textBuffer3.createSnapshot(false)), content); } test('write - no encoding - content as string', async () => { @@ -340,7 +347,7 @@ export default function createSuite(params: Params) { let detectedEncoding = await detectEncodingByBOM(resource.fsPath); assert.strictEqual(detectedEncoding, null); - const model = createTextModel((await readFile(resource.fsPath)).toString() + 'updates'); + const model = disposables.add(createTextModel((await readFile(resource.fsPath)).toString() + 'updates')); await service.write(resource, model.createSnapshot(), { encoding: UTF8_with_bom }); detectedEncoding = await detectEncodingByBOM(resource.fsPath); @@ -360,8 +367,6 @@ export default function createSuite(params: Params) { await service.write(resource, model.createSnapshot(), { encoding: UTF8 }); detectedEncoding = await detectEncodingByBOM(resource.fsPath); assert.strictEqual(detectedEncoding, null); - - model.dispose(); }); test('write - preserve UTF8 BOM - content as string', async () => { @@ -412,8 +417,9 @@ export default function createSuite(params: Params) { assert.strictEqual(result.size, (await stat(resource.fsPath)).size); const content = (await readFile(resource.fsPath)).toString(); + const textBuffer = disposables.add(result.value.create(DefaultEndOfLine.LF).textBuffer); assert.strictEqual( - snapshotToString(result.value.create(DefaultEndOfLine.LF).textBuffer.createSnapshot(false)), + snapshotToString(textBuffer.createSnapshot(false)), snapshotToString(createTextModelSnapshot(content, false))); } @@ -541,7 +547,8 @@ export default function createSuite(params: Params) { const result = await service.readStream(resource, { encoding }); assert.strictEqual(result.encoding, encoding); - let contents = snapshotToString(result.value.create(DefaultEndOfLine.LF).textBuffer.createSnapshot(false)); + const textBuffer = disposables.add(result.value.create(DefaultEndOfLine.LF).textBuffer); + let contents = snapshotToString(textBuffer.createSnapshot(false)); assert.strictEqual(contents.indexOf(needle), 0); assert.ok(contents.indexOf(needle, 10) > 0); @@ -557,7 +564,8 @@ export default function createSuite(params: Params) { const factory = await createTextBufferFactoryFromStream(await service.getDecodedStream(resource, bufferToStream(rawFileVSBuffer), { encoding })); - contents = snapshotToString(factory.create(DefaultEndOfLine.LF).textBuffer.createSnapshot(false)); + const textBuffer2 = disposables.add(factory.create(DefaultEndOfLine.LF).textBuffer); + contents = snapshotToString(textBuffer2.createSnapshot(false)); assert.strictEqual(contents.indexOf(needle), 0); assert.ok(contents.indexOf(needle, 10) > 0); diff --git a/src/vs/workbench/services/textfile/test/electron-sandbox/nativeTextFileService.io.test.ts b/src/vs/workbench/services/textfile/test/electron-sandbox/nativeTextFileService.io.test.ts index 707780e4c2a..f3b05c01014 100644 --- a/src/vs/workbench/services/textfile/test/electron-sandbox/nativeTextFileService.io.test.ts +++ b/src/vs/workbench/services/textfile/test/electron-sandbox/nativeTextFileService.io.test.ts @@ -22,6 +22,7 @@ import { WorkingCopyService } from 'vs/workbench/services/workingCopy/common/wor import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; import { TestInMemoryFileSystemProvider } from 'vs/workbench/test/browser/workbenchTestServices'; import { TestNativeTextFileServiceWithEncodingOverrides, workbenchInstantiationService } from 'vs/workbench/test/electron-sandbox/workbenchTestServices'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('Files - NativeTextFileService i/o', function () { const disposables = new DisposableStore(); @@ -35,18 +36,17 @@ suite('Files - NativeTextFileService i/o', function () { const instantiationService = workbenchInstantiationService(undefined, disposables); const logService = new NullLogService(); - const fileService = new FileService(logService); + const fileService = disposables.add(new FileService(logService)); - fileProvider = new TestInMemoryFileSystemProvider(); + fileProvider = disposables.add(new TestInMemoryFileSystemProvider()); disposables.add(fileService.registerProvider(Schemas.file, fileProvider)); - disposables.add(fileProvider); const collection = new ServiceCollection(); collection.set(IFileService, fileService); + collection.set(IWorkingCopyFileService, disposables.add(new WorkingCopyFileService(fileService, disposables.add(new WorkingCopyService()), instantiationService, disposables.add(new UriIdentityService(fileService))))); - collection.set(IWorkingCopyFileService, new WorkingCopyFileService(fileService, new WorkingCopyService(), instantiationService, new UriIdentityService(fileService))); - - service = instantiationService.createChild(collection).createInstance(TestNativeTextFileServiceWithEncodingOverrides); + service = disposables.add(instantiationService.createChild(collection).createInstance(TestNativeTextFileServiceWithEncodingOverrides)); + disposables.add(service.files); await fileProvider.mkdir(URI.file(testDir)); for (const fileName in files) { @@ -61,8 +61,6 @@ suite('Files - NativeTextFileService i/o', function () { }, teardown: async () => { - (service.files).dispose(); - disposables.clear(); }, @@ -107,4 +105,6 @@ suite('Files - NativeTextFileService i/o', function () { return null; // ignore errors (like file not found) } } + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/services/textfile/test/electron-sandbox/nativeTextFileService.test.ts b/src/vs/workbench/services/textfile/test/electron-sandbox/nativeTextFileService.test.ts index 1ac327fd20c..a48728e4a7a 100644 --- a/src/vs/workbench/services/textfile/test/electron-sandbox/nativeTextFileService.test.ts +++ b/src/vs/workbench/services/textfile/test/electron-sandbox/nativeTextFileService.test.ts @@ -19,7 +19,7 @@ import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentitySe import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; -import { toResource } from 'vs/base/test/common/utils'; +import { ensureNoDisposablesAreLeakedInTestSuite, toResource } from 'vs/base/test/common/utils'; suite('Files - NativeTextFileService', function () { const disposables = new DisposableStore(); @@ -31,28 +31,25 @@ suite('Files - NativeTextFileService', function () { instantiationService = workbenchInstantiationService(undefined, disposables); const logService = new NullLogService(); - const fileService = new FileService(logService); + const fileService = disposables.add(new FileService(logService)); - const fileProvider = new InMemoryFileSystemProvider(); + const fileProvider = disposables.add(new InMemoryFileSystemProvider()); disposables.add(fileService.registerProvider(Schemas.file, fileProvider)); - disposables.add(fileProvider); const collection = new ServiceCollection(); collection.set(IFileService, fileService); + collection.set(IWorkingCopyFileService, disposables.add(new WorkingCopyFileService(fileService, disposables.add(new WorkingCopyService()), instantiationService, disposables.add(new UriIdentityService(fileService))))); - collection.set(IWorkingCopyFileService, new WorkingCopyFileService(fileService, new WorkingCopyService(), instantiationService, new UriIdentityService(fileService))); - - service = instantiationService.createChild(collection).createInstance(TestNativeTextFileServiceWithEncodingOverrides); + service = disposables.add(instantiationService.createChild(collection).createInstance(TestNativeTextFileServiceWithEncodingOverrides)); + disposables.add(service.files); }); teardown(() => { - (service.files).dispose(); - disposables.clear(); }); test('shutdown joins on pending saves', async function () { - const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + const model: TextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined)); await model.resolve(); @@ -67,4 +64,6 @@ suite('Files - NativeTextFileService', function () { assert.strictEqual(pendingSaveAwaited, true); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/services/textfile/test/node/encoding/encoding.test.ts b/src/vs/workbench/services/textfile/test/node/encoding/encoding.test.ts index bcd51283ba6..b3cf625c809 100644 --- a/src/vs/workbench/services/textfile/test/node/encoding/encoding.test.ts +++ b/src/vs/workbench/services/textfile/test/node/encoding/encoding.test.ts @@ -11,6 +11,7 @@ import { newWriteableBufferStream, VSBuffer, VSBufferReadableStream, streamToBuf import { splitLines } from 'vs/base/common/strings'; import { FileAccess } from 'vs/base/common/network'; import { importAMDNodeModule } from 'vs/amdX'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; export async function detectEncodingByBOM(file: string): Promise { try { @@ -452,4 +453,6 @@ suite('Encoding', () => { assert.strictEqual(iconv.encodingExists(enc), true, enc); } }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/services/textmodelResolver/test/browser/textModelResolverService.test.ts b/src/vs/workbench/services/textmodelResolver/test/browser/textModelResolverService.test.ts index 9d7af92be98..f38d12fc2bb 100644 --- a/src/vs/workbench/services/textmodelResolver/test/browser/textModelResolverService.test.ts +++ b/src/vs/workbench/services/textmodelResolver/test/browser/textModelResolverService.test.ts @@ -10,7 +10,7 @@ import { TextResourceEditorInput } from 'vs/workbench/common/editor/textResource import { TextResourceEditorModel } from 'vs/workbench/common/editor/textResourceEditorModel'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { workbenchInstantiationService, TestServiceAccessor, ITestTextFileEditorModelManager } from 'vs/workbench/test/browser/workbenchTestServices'; -import { toResource } from 'vs/base/test/common/utils'; +import { ensureNoDisposablesAreLeakedInTestSuite, toResource } from 'vs/base/test/common/utils'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { snapshotToString } from 'vs/workbench/services/textfile/common/textfiles'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; @@ -22,25 +22,22 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; suite('Workbench - TextModelResolverService', () => { - let disposables: DisposableStore; + const disposables = new DisposableStore(); let instantiationService: IInstantiationService; let accessor: TestServiceAccessor; - let model: TextFileEditorModel; setup(() => { - disposables = new DisposableStore(); instantiationService = workbenchInstantiationService(undefined, disposables); accessor = instantiationService.createInstance(TestServiceAccessor); + disposables.add(accessor.textFileService.files); }); teardown(() => { - model?.dispose(); - (accessor.textFileService.files).dispose(); - disposables.dispose(); + disposables.clear(); }); test('resolve resource', async () => { - const disposable = accessor.textModelResolverService.registerTextModelContentProvider('test', { + disposables.add(accessor.textModelResolverService.registerTextModelContentProvider('test', { provideTextContent: async function (resource: URI): Promise { if (resource.scheme === 'test') { const modelContent = 'Hello Test'; @@ -51,12 +48,12 @@ suite('Workbench - TextModelResolverService', () => { return null; } - }); + })); const resource = URI.from({ scheme: 'test', authority: null!, path: 'thePath' }); const input = instantiationService.createInstance(TextResourceEditorInput, resource, 'The Name', 'The Description', undefined, undefined); - const model = await input.resolve(); + const model = disposables.add(await input.resolve()); assert.ok(model); assert.strictEqual(snapshotToString(((model as TextResourceEditorModel).createSnapshot()!)), 'Hello Test'); let disposed = false; @@ -70,11 +67,10 @@ suite('Workbench - TextModelResolverService', () => { await disposedPromise; assert.strictEqual(disposed, true); - disposable.dispose(); }); test('resolve file', async function () { - const textModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file_resolver.txt'), 'utf8', undefined); + const textModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file_resolver.txt'), 'utf8', undefined)); (accessor.textFileService.files).add(textModel.resource, textModel); await textModel.resolve(); @@ -98,7 +94,7 @@ suite('Workbench - TextModelResolverService', () => { }); test('resolved dirty file eventually disposes', async function () { - const textModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file_resolver.txt'), 'utf8', undefined); + const textModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file_resolver.txt'), 'utf8', undefined)); (accessor.textFileService.files).add(textModel.resource, textModel); await textModel.resolve(); @@ -123,7 +119,7 @@ suite('Workbench - TextModelResolverService', () => { }); test('resolved dirty file does not dispose when new reference created', async function () { - const textModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file_resolver.txt'), 'utf8', undefined); + const textModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file_resolver.txt'), 'utf8', undefined)); (accessor.textFileService.files).add(textModel.resource, textModel); await textModel.resolve(); @@ -156,8 +152,8 @@ suite('Workbench - TextModelResolverService', () => { test('resolve untitled', async () => { const service = accessor.untitledTextEditorService; - const untitledModel = service.create(); - const input = instantiationService.createInstance(UntitledTextEditorInput, untitledModel); + const untitledModel = disposables.add(service.create()); + const input = disposables.add(instantiationService.createInstance(UntitledTextEditorInput, untitledModel)); await input.resolve(); const ref = await accessor.textModelResolverService.createModelReference(input.resource); @@ -174,15 +170,15 @@ suite('Workbench - TextModelResolverService', () => { let resolveModel!: Function; const waitForIt = new Promise(resolve => resolveModel = resolve); - const disposable = accessor.textModelResolverService.registerTextModelContentProvider('test', { + disposables.add(accessor.textModelResolverService.registerTextModelContentProvider('test', { provideTextContent: async (resource: URI): Promise => { await waitForIt; const modelContent = 'Hello Test'; const languageSelection = accessor.languageService.createById('json'); - return accessor.modelService.createModel(modelContent, languageSelection, resource); + return disposables.add(accessor.modelService.createModel(modelContent, languageSelection, resource)); } - }); + })); const uri = URI.from({ scheme: 'test', authority: null!, path: 'thePath' }); @@ -203,12 +199,12 @@ suite('Workbench - TextModelResolverService', () => { modelRef1.dispose(); assert(!textModel.isDisposed(), 'the text model should still not be disposed'); - const p1 = new Promise(resolve => textModel.onWillDispose(resolve)); + const p1 = new Promise(resolve => disposables.add(textModel.onWillDispose(resolve))); modelRef2.dispose(); await p1; assert(textModel.isDisposed(), 'the text model should finally be disposed'); - - disposable.dispose(); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts index ea58be56b01..e82e03d208a 100644 --- a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts @@ -114,7 +114,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, @ILogService private readonly logService: ILogService, @IHostColorSchemeService private readonly hostColorService: IHostColorSchemeService, - @IUserDataInitializationService userDataInitializationService: IUserDataInitializationService, + @IUserDataInitializationService private readonly userDataInitializationService: IUserDataInitializationService, @ILanguageService languageService: ILanguageService ) { this.container = layoutService.container; @@ -180,7 +180,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { this.applyAndSetProductIconTheme(productIconData, true); } - Promise.all([extensionService.whenInstalledExtensionsRegistered(), userDataInitializationService.whenInitializationFinished()]).then(_ => { + extensionService.whenInstalledExtensionsRegistered().then(_ => { this.installConfigurationListener(); this.installPreferredSchemeListener(); this.installRegistryListeners(); @@ -209,17 +209,23 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { if (devThemes.length) { return this.setColorTheme(devThemes[0].id, ConfigurationTarget.MEMORY); } - const fallbackTheme = this.currentColorTheme.type === ColorScheme.LIGHT ? ThemeSettingDefaults.COLOR_THEME_LIGHT : ThemeSettingDefaults.COLOR_THEME_DARK; - const theme = this.colorThemeRegistry.findThemeBySettingsId(this.settings.colorTheme, fallbackTheme); const preferredColorScheme = this.getPreferredColorScheme(); const prevScheme = this.storageService.get(PERSISTED_OS_COLOR_SCHEME, PERSISTED_OS_COLOR_SCHEME_SCOPE); if (preferredColorScheme !== prevScheme) { this.storageService.store(PERSISTED_OS_COLOR_SCHEME, preferredColorScheme, PERSISTED_OS_COLOR_SCHEME_SCOPE, StorageTarget.USER); - if (preferredColorScheme && theme?.type !== preferredColorScheme) { + if (preferredColorScheme && this.currentColorTheme.type !== preferredColorScheme) { return this.applyPreferredColorTheme(preferredColorScheme); } } + let theme = this.colorThemeRegistry.findThemeBySettingsId(this.settings.colorTheme, undefined); + if (!theme) { + // If the current theme is not available, first make sure setting sync is complete + await this.userDataInitializationService.whenInitializationFinished(); + // try to get the theme again, now with a fallback to the default themes + const fallbackTheme = this.currentColorTheme.type === ColorScheme.LIGHT ? ThemeSettingDefaults.COLOR_THEME_LIGHT : ThemeSettingDefaults.COLOR_THEME_DARK; + theme = this.colorThemeRegistry.findThemeBySettingsId(this.settings.colorTheme, fallbackTheme); + } return this.setColorTheme(theme && theme.id, undefined); }; @@ -228,7 +234,12 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { if (devThemes.length) { return this.setFileIconTheme(devThemes[0].id, ConfigurationTarget.MEMORY); } - const theme = this.fileIconThemeRegistry.findThemeBySettingsId(this.settings.fileIconTheme); + let theme = this.fileIconThemeRegistry.findThemeBySettingsId(this.settings.fileIconTheme); + if (!theme) { + // If the current theme is not available, first make sure setting sync is complete + await this.userDataInitializationService.whenInitializationFinished(); + theme = this.fileIconThemeRegistry.findThemeBySettingsId(this.settings.fileIconTheme); + } return this.setFileIconTheme(theme ? theme.id : DEFAULT_FILE_ICON_THEME_ID, undefined); }; @@ -237,7 +248,12 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { if (devThemes.length) { return this.setProductIconTheme(devThemes[0].id, ConfigurationTarget.MEMORY); } - const theme = this.productIconThemeRegistry.findThemeBySettingsId(this.settings.productIconTheme); + let theme = this.productIconThemeRegistry.findThemeBySettingsId(this.settings.productIconTheme); + if (!theme) { + // If the current theme is not available, first make sure setting sync is complete + await this.userDataInitializationService.whenInitializationFinished(); + theme = this.productIconThemeRegistry.findThemeBySettingsId(this.settings.productIconTheme); + } return this.setProductIconTheme(theme ? theme.id : DEFAULT_PRODUCT_ICON_THEME_ID, undefined); }; diff --git a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.integrationTest.ts b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.integrationTest.ts index 828a72f1d0d..f64d8c7e4c8 100644 --- a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.integrationTest.ts +++ b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.integrationTest.ts @@ -5,27 +5,27 @@ import * as assert from 'assert'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { UntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; import { workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench/test/browser/workbenchTestServices'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { CancellationToken } from 'vs/base/common/cancellation'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('Untitled text editors', () => { - let disposables: DisposableStore; + const disposables = new DisposableStore(); + let instantiationService: IInstantiationService; let accessor: TestServiceAccessor; setup(() => { - disposables = new DisposableStore(); instantiationService = workbenchInstantiationService(undefined, disposables); accessor = instantiationService.createInstance(TestServiceAccessor); + disposables.add(accessor.untitledTextEditorService); }); teardown(() => { - (accessor.untitledTextEditorService as UntitledTextEditorService).dispose(); - disposables.dispose(); + disposables.clear(); }); test('backup and restore (simple)', async function () { @@ -39,24 +39,21 @@ suite('Untitled text editors', () => { async function testBackupAndRestore(content: string) { const service = accessor.untitledTextEditorService; - const originalInput = instantiationService.createInstance(UntitledTextEditorInput, service.create()); - const restoredInput = instantiationService.createInstance(UntitledTextEditorInput, service.create()); + const originalInput = disposables.add(instantiationService.createInstance(UntitledTextEditorInput, service.create())); + const restoredInput = disposables.add(instantiationService.createInstance(UntitledTextEditorInput, service.create())); - const originalModel = await originalInput.resolve(); + const originalModel = disposables.add(await originalInput.resolve()); originalModel.textEditorModel?.setValue(content); const backup = await originalModel.backup(CancellationToken.None); const modelRestoredIdentifier = { typeId: originalModel.typeId, resource: restoredInput.resource }; await accessor.workingCopyBackupService.backup(modelRestoredIdentifier, backup.content); - const restoredModel = await restoredInput.resolve(); + const restoredModel = disposables.add(await restoredInput.resolve()); assert.strictEqual(restoredModel.textEditorModel?.getValue(), content); assert.strictEqual(restoredModel.isDirty(), true); - - originalInput.dispose(); - originalModel.dispose(); - restoredInput.dispose(); - restoredModel.dispose(); } + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts index 935b78a3dfd..c70d755430b 100644 --- a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts +++ b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts @@ -21,22 +21,22 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { isReadable, isReadableStream } from 'vs/base/common/stream'; import { readableToBuffer, streamToBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer'; import { LanguageDetectionLanguageEventSource } from 'vs/workbench/services/languageDetection/common/languageDetectionWorkerService'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('Untitled text editors', () => { - let disposables: DisposableStore; + const disposables = new DisposableStore(); let instantiationService: IInstantiationService; let accessor: TestServiceAccessor; setup(() => { - disposables = new DisposableStore(); instantiationService = workbenchInstantiationService(undefined, disposables); accessor = instantiationService.createInstance(TestServiceAccessor); + disposables.add(accessor.untitledTextEditorService as UntitledTextEditorService); }); teardown(() => { - (accessor.untitledTextEditorService as UntitledTextEditorService).dispose(); - disposables.dispose(); + disposables.clear(); }); test('basics', async () => { @@ -135,13 +135,13 @@ suite('Untitled text editors', () => { const file = URI.file(join('C:\\', '/foo/file.txt')); let onDidChangeDirtyModel: IUntitledTextEditorModel | undefined = undefined; - const listener = service.onDidChangeDirty(model => { + disposables.add(service.onDidChangeDirty(model => { onDidChangeDirtyModel = model; - }); + })); - const model = service.create({ associatedResource: file }); + const model = disposables.add(service.create({ associatedResource: file })); assert.ok(accessor.untitledTextEditorService.isUntitledWithAssociatedResource(model.resource)); - const untitled = instantiationService.createInstance(UntitledTextEditorInput, model); + const untitled = disposables.add(instantiationService.createInstance(UntitledTextEditorInput, model)); assert.ok(untitled.isDirty()); assert.strictEqual(model, onDidChangeDirtyModel); @@ -149,32 +149,28 @@ suite('Untitled text editors', () => { assert.ok(resolvedModel.hasAssociatedFilePath); assert.strictEqual(untitled.isDirty(), true); - - untitled.dispose(); - listener.dispose(); }); test('no longer dirty when content gets empty (not with associated resource)', async () => { const service = accessor.untitledTextEditorService; const workingCopyService = accessor.workingCopyService; - const input = instantiationService.createInstance(UntitledTextEditorInput, service.create()); + const input = disposables.add(instantiationService.createInstance(UntitledTextEditorInput, service.create())); // dirty - const model = await input.resolve(); + const model = disposables.add(await input.resolve()); model.textEditorModel?.setValue('foo bar'); assert.ok(model.isDirty()); assert.ok(workingCopyService.isDirty(model.resource, model.typeId)); model.textEditorModel?.setValue(''); assert.ok(!model.isDirty()); assert.ok(!workingCopyService.isDirty(model.resource, model.typeId)); - input.dispose(); - model.dispose(); }); test('via create options', async () => { const service = accessor.untitledTextEditorService; - const model1 = await instantiationService.createInstance(UntitledTextEditorInput, service.create()).resolve(); + const input1 = disposables.add(instantiationService.createInstance(UntitledTextEditorInput, service.create())); + const model1 = disposables.add(await input1.resolve()); model1.textEditorModel!.setValue('foo bar'); assert.ok(model1.isDirty()); @@ -182,47 +178,42 @@ suite('Untitled text editors', () => { model1.textEditorModel!.setValue(''); assert.ok(!model1.isDirty()); - const model2 = await instantiationService.createInstance(UntitledTextEditorInput, service.create({ initialValue: 'Hello World' })).resolve(); + const input2 = disposables.add(instantiationService.createInstance(UntitledTextEditorInput, service.create({ initialValue: 'Hello World' }))); + const model2 = disposables.add(await input2.resolve()); assert.strictEqual(snapshotToString(model2.createSnapshot()!), 'Hello World'); - const input = instantiationService.createInstance(UntitledTextEditorInput, service.create()); + const input = disposables.add(instantiationService.createInstance(UntitledTextEditorInput, disposables.add(service.create()))); - const model3 = await instantiationService.createInstance(UntitledTextEditorInput, service.create({ untitledResource: input.resource })).resolve(); + const input3 = disposables.add(instantiationService.createInstance(UntitledTextEditorInput, service.create({ untitledResource: input.resource }))); + const model3 = disposables.add(await input3.resolve()); assert.strictEqual(model3.resource.toString(), input.resource.toString()); const file = URI.file(join('C:\\', '/foo/file44.txt')); - const model4 = await instantiationService.createInstance(UntitledTextEditorInput, service.create({ associatedResource: file })).resolve(); + const input4 = disposables.add(instantiationService.createInstance(UntitledTextEditorInput, service.create({ associatedResource: file }))); + const model4 = disposables.add(await input4.resolve()); assert.ok(model4.hasAssociatedFilePath); assert.ok(model4.isDirty()); - - model1.dispose(); - model2.dispose(); - model3.dispose(); - model4.dispose(); - input.dispose(); }); test('associated path remains dirty when content gets empty', async () => { const service = accessor.untitledTextEditorService; const file = URI.file(join('C:\\', '/foo/file.txt')); - const input = instantiationService.createInstance(UntitledTextEditorInput, service.create({ associatedResource: file })); + const input = disposables.add(instantiationService.createInstance(UntitledTextEditorInput, service.create({ associatedResource: file }))); // dirty - const model = await input.resolve(); + const model = disposables.add(await input.resolve()); model.textEditorModel?.setValue('foo bar'); assert.ok(model.isDirty()); model.textEditorModel?.setValue(''); assert.ok(model.isDirty()); - input.dispose(); - model.dispose(); }); test('initial content is dirty', async () => { const service = accessor.untitledTextEditorService; const workingCopyService = accessor.workingCopyService; - const untitled = instantiationService.createInstance(UntitledTextEditorInput, service.create({ initialValue: 'Hello World' })); + const untitled = disposables.add(instantiationService.createInstance(UntitledTextEditorInput, service.create({ initialValue: 'Hello World' }))); assert.ok(untitled.isDirty()); const backup = (await untitled.model.backup(CancellationToken.None)).content; @@ -237,12 +228,9 @@ suite('Untitled text editors', () => { } // dirty - const model = await untitled.resolve(); + const model = disposables.add(await untitled.resolve()); assert.ok(model.isDirty()); assert.strictEqual(workingCopyService.dirtyCount, 1); - - untitled.dispose(); - model.dispose(); }); test('created with files.defaultLanguage setting', () => { @@ -251,13 +239,11 @@ suite('Untitled text editors', () => { config.setUserConfiguration('files', { 'defaultLanguage': defaultLanguage }); const service = accessor.untitledTextEditorService; - const input = service.create(); + const input = disposables.add(service.create()); assert.strictEqual(input.getLanguageId(), defaultLanguage); config.setUserConfiguration('files', { 'defaultLanguage': undefined }); - - input.dispose(); }); test('created with files.defaultLanguage setting (${activeEditorLanguage})', async () => { @@ -267,14 +253,12 @@ suite('Untitled text editors', () => { accessor.editorService.activeTextEditorLanguageId = 'typescript'; const service = accessor.untitledTextEditorService; - const model = service.create(); + const model = disposables.add(service.create()); assert.strictEqual(model.getLanguageId(), 'typescript'); config.setUserConfiguration('files', { 'defaultLanguage': undefined }); accessor.editorService.activeTextEditorLanguageId = undefined; - - model.dispose(); }); test('created with language overrides files.defaultLanguage setting', () => { @@ -284,94 +268,81 @@ suite('Untitled text editors', () => { config.setUserConfiguration('files', { 'defaultLanguage': defaultLanguage }); const service = accessor.untitledTextEditorService; - const input = service.create({ languageId: language }); + const input = disposables.add(service.create({ languageId: language })); assert.strictEqual(input.getLanguageId(), language); config.setUserConfiguration('files', { 'defaultLanguage': undefined }); - - input.dispose(); }); test('can change language afterwards', async () => { const languageId = 'untitled-input-test'; - const registration = accessor.languageService.registerLanguage({ + disposables.add(accessor.languageService.registerLanguage({ id: languageId, - }); + })); const service = accessor.untitledTextEditorService; - const input = instantiationService.createInstance(UntitledTextEditorInput, service.create({ languageId: languageId })); + const input = disposables.add(instantiationService.createInstance(UntitledTextEditorInput, service.create({ languageId: languageId }))); assert.strictEqual(input.getLanguageId(), languageId); - const model = await input.resolve(); + const model = disposables.add(await input.resolve()); assert.strictEqual(model.getLanguageId(), languageId); input.setLanguageId(PLAINTEXT_LANGUAGE_ID); assert.strictEqual(input.getLanguageId(), PLAINTEXT_LANGUAGE_ID); - - input.dispose(); - model.dispose(); - registration.dispose(); }); test('remembers that language was set explicitly', async () => { const language = 'untitled-input-test'; - const registration = accessor.languageService.registerLanguage({ + disposables.add(accessor.languageService.registerLanguage({ id: language, - }); + })); const service = accessor.untitledTextEditorService; - const model = service.create(); - const input = instantiationService.createInstance(UntitledTextEditorInput, model); + const model = disposables.add(service.create()); + const input = disposables.add(instantiationService.createInstance(UntitledTextEditorInput, model)); assert.ok(!input.model.hasLanguageSetExplicitly); input.setLanguageId(PLAINTEXT_LANGUAGE_ID); assert.ok(input.model.hasLanguageSetExplicitly); assert.strictEqual(input.getLanguageId(), PLAINTEXT_LANGUAGE_ID); - - input.dispose(); - model.dispose(); - registration.dispose(); }); // Issue #159202 test('remembers that language was set explicitly if set by another source (i.e. ModelService)', async () => { const language = 'untitled-input-test'; - const registration = accessor.languageService.registerLanguage({ + disposables.add(accessor.languageService.registerLanguage({ id: language, - }); + })); const service = accessor.untitledTextEditorService; - const model = service.create(); - const input = instantiationService.createInstance(UntitledTextEditorInput, model); - await input.resolve(); + const model = disposables.add(service.create()); + const input = disposables.add(instantiationService.createInstance(UntitledTextEditorInput, model)); + disposables.add(await input.resolve()); assert.ok(!input.model.hasLanguageSetExplicitly); model.textEditorModel!.setLanguage(accessor.languageService.createById(language)); assert.ok(input.model.hasLanguageSetExplicitly); assert.strictEqual(model.getLanguageId(), language); - - model.dispose(); - registration.dispose(); }); test('Language is not set explicitly if set by language detection source', async () => { const language = 'untitled-input-test'; - const registration = accessor.languageService.registerLanguage({ + disposables.add(accessor.languageService.registerLanguage({ id: language, - }); + })); const service = accessor.untitledTextEditorService; - const model = service.create(); - const input = instantiationService.createInstance(UntitledTextEditorInput, model); + const model = disposables.add(service.create()); + const input = disposables.add(instantiationService.createInstance(UntitledTextEditorInput, model)); await input.resolve(); assert.ok(!input.model.hasLanguageSetExplicitly); @@ -382,61 +353,54 @@ suite('Untitled text editors', () => { assert.ok(!input.model.hasLanguageSetExplicitly); assert.strictEqual(model.getLanguageId(), language); - - model.dispose(); - registration.dispose(); }); test('service#onDidChangeEncoding', async () => { const service = accessor.untitledTextEditorService; - const input = instantiationService.createInstance(UntitledTextEditorInput, service.create()); + const input = disposables.add(instantiationService.createInstance(UntitledTextEditorInput, service.create())); let counter = 0; - service.onDidChangeEncoding(model => { + disposables.add(service.onDidChangeEncoding(model => { counter++; assert.strictEqual(model.resource.toString(), input.resource.toString()); - }); + })); // encoding - const model = await input.resolve(); + const model = disposables.add(await input.resolve()); await model.setEncoding('utf16'); assert.strictEqual(counter, 1); - input.dispose(); - model.dispose(); }); test('service#onDidChangeLabel', async () => { const service = accessor.untitledTextEditorService; - const input = instantiationService.createInstance(UntitledTextEditorInput, service.create()); + const input = disposables.add(instantiationService.createInstance(UntitledTextEditorInput, service.create())); let counter = 0; - service.onDidChangeLabel(model => { + disposables.add(service.onDidChangeLabel(model => { counter++; assert.strictEqual(model.resource.toString(), input.resource.toString()); - }); + })); // label - const model = await input.resolve(); + const model = disposables.add(await input.resolve()); model.textEditorModel?.setValue('Foo Bar'); assert.strictEqual(counter, 1); - input.dispose(); - model.dispose(); }); test('service#onWillDispose', async () => { const service = accessor.untitledTextEditorService; - const input = instantiationService.createInstance(UntitledTextEditorInput, service.create()); + const input = disposables.add(instantiationService.createInstance(UntitledTextEditorInput, service.create())); let counter = 0; - service.onWillDispose(model => { + disposables.add(service.onWillDispose(model => { counter++; assert.strictEqual(model.resource.toString(), input.resource.toString()); - }); + })); - const model = await input.resolve(); + const model = disposables.add(await input.resolve()); assert.strictEqual(counter, 0); model.dispose(); assert.strictEqual(counter, 1); @@ -444,9 +408,9 @@ suite('Untitled text editors', () => { test('service#getValue', async () => { - // This function is used for the untitledocumentData API const service = accessor.untitledTextEditorService; - const model1 = await instantiationService.createInstance(UntitledTextEditorInput, service.create()).resolve(); + const input1 = disposables.add(instantiationService.createInstance(UntitledTextEditorInput, service.create())); + const model1 = disposables.add(await input1.resolve()); model1.textEditorModel!.setValue('foo bar'); assert.strictEqual(service.getValue(model1.resource), 'foo bar'); @@ -458,12 +422,12 @@ suite('Untitled text editors', () => { test('model#onDidChangeContent', async function () { const service = accessor.untitledTextEditorService; - const input = instantiationService.createInstance(UntitledTextEditorInput, service.create()); + const input = disposables.add(instantiationService.createInstance(UntitledTextEditorInput, service.create())); let counter = 0; - const model = await input.resolve(); - model.onDidChangeContent(() => counter++); + const model = disposables.add(await input.resolve()); + disposables.add(model.onDidChangeContent(() => counter++)); model.textEditorModel?.setValue('foo'); @@ -477,19 +441,16 @@ suite('Untitled text editors', () => { model.textEditorModel?.setValue('foo'); assert.strictEqual(counter, 4, 'Dirty model should trigger event'); - - input.dispose(); - model.dispose(); }); test('model#onDidRevert and input disposed when reverted', async function () { const service = accessor.untitledTextEditorService; - const input = instantiationService.createInstance(UntitledTextEditorInput, service.create()); + const input = disposables.add(instantiationService.createInstance(UntitledTextEditorInput, service.create())); let counter = 0; - const model = await input.resolve(); - model.onDidRevert(() => counter++); + const model = disposables.add(await input.resolve()); + disposables.add(model.onDidRevert(() => counter++)); model.textEditorModel?.setValue('foo'); @@ -501,12 +462,12 @@ suite('Untitled text editors', () => { test('model#onDidChangeName and input name', async function () { const service = accessor.untitledTextEditorService; - const input = instantiationService.createInstance(UntitledTextEditorInput, service.create()); + const input = disposables.add(instantiationService.createInstance(UntitledTextEditorInput, service.create())); let counter = 0; - let model = await input.resolve(); - model.onDidChangeName(() => counter++); + let model = disposables.add(await input.resolve()); + disposables.add(model.onDidChangeName(() => counter++)); model.textEditorModel?.setValue('foo'); assert.strictEqual(input.getName(), 'foo'); @@ -572,23 +533,20 @@ suite('Untitled text editors', () => { input.dispose(); model.dispose(); - const inputWithContents = instantiationService.createInstance(UntitledTextEditorInput, service.create({ initialValue: 'Foo' })); - model = await inputWithContents.resolve(); + const inputWithContents = disposables.add(instantiationService.createInstance(UntitledTextEditorInput, service.create({ initialValue: 'Foo' }))); + model = disposables.add(await inputWithContents.resolve()); assert.strictEqual(inputWithContents.getName(), 'Foo'); - - inputWithContents.dispose(); - model.dispose(); }); test('model#onDidChangeDirty', async function () { const service = accessor.untitledTextEditorService; - const input = instantiationService.createInstance(UntitledTextEditorInput, service.create()); + const input = disposables.add(instantiationService.createInstance(UntitledTextEditorInput, service.create())); let counter = 0; - const model = await input.resolve(); - model.onDidChangeDirty(() => counter++); + const model = disposables.add(await input.resolve()); + disposables.add(model.onDidChangeDirty(() => counter++)); model.textEditorModel?.setValue('foo'); @@ -596,19 +554,16 @@ suite('Untitled text editors', () => { model.textEditorModel?.setValue('bar'); assert.strictEqual(counter, 1, 'Another change does not fire event'); - - input.dispose(); - model.dispose(); }); test('model#onDidChangeEncoding', async function () { const service = accessor.untitledTextEditorService; - const input = instantiationService.createInstance(UntitledTextEditorInput, service.create()); + const input = disposables.add(instantiationService.createInstance(UntitledTextEditorInput, service.create())); let counter = 0; - const model = await input.resolve(); - model.onDidChangeEncoding(() => counter++); + const model = disposables.add(await input.resolve()); + disposables.add(model.onDidChangeEncoding(() => counter++)); await model.setEncoding('utf16'); @@ -616,8 +571,7 @@ suite('Untitled text editors', () => { await model.setEncoding('utf16'); assert.strictEqual(counter, 1, 'Another change to same encoding does not fire event'); - - input.dispose(); - model.dispose(); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts index 71cbb86e4d7..e5587cb3cda 100644 --- a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts +++ b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts @@ -175,15 +175,13 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat this._register(this.authenticationService.onDidChangeDeclaredProviders(() => this.updateAuthenticationProviders())); - this._register( + this._register(Event.filter( Event.any( - Event.filter( - Event.any( - this.authenticationService.onDidRegisterAuthenticationProvider, - this.authenticationService.onDidUnregisterAuthenticationProvider, - ), info => this.isSupportedAuthenticationProviderId(info.id)), - Event.filter(this.userDataSyncAccountService.onTokenFailed, isSuccessive => !isSuccessive)) - (() => this.update())); + this.authenticationService.onDidRegisterAuthenticationProvider, + this.authenticationService.onDidUnregisterAuthenticationProvider, + ), info => this.isSupportedAuthenticationProviderId(info.id))(() => this.update())); + + this._register(Event.filter(this.userDataSyncAccountService.onTokenFailed, isSuccessive => !isSuccessive)(() => this.update('token failure'))); this._register(Event.filter(this.authenticationService.onDidChangeSessions, e => this.isSupportedAuthenticationProviderId(e.providerId))(({ event }) => this.onDidChangeSessions(event))); this._register(this.storageService.onDidChangeValue(StorageScope.APPLICATION, UserDataSyncWorkbenchService.CACHED_SESSION_STORAGE_KEY, this._register(new DisposableStore()))(() => this.onDidChangeStorage())); @@ -205,7 +203,11 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat })); } - private async update(): Promise { + private async update(reason?: string): Promise { + + if (reason) { + this.logService.info(`Settings Sync: Updating due to ${reason}`); + } this.updateAuthenticationProviders(); await this.updateCurrentAccount(); @@ -611,20 +613,20 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat private async onDidAuthFailure(): Promise { this.telemetryService.publicLog2<{}, { owner: 'sandy081'; comment: 'Report when there are successive auth failures during settings sync' }>('sync/successiveAuthFailures'); this.currentSessionId = undefined; - await this.update(); + await this.update('auth failure'); } private onDidChangeSessions(e: AuthenticationSessionsChangeEvent): void { if (this.currentSessionId && e.removed.find(session => session.id === this.currentSessionId)) { this.currentSessionId = undefined; } - this.update(); + this.update('change in sessions'); } private onDidChangeStorage(): void { if (this.currentSessionId !== this.getStoredCachedSessionId() /* This checks if current window changed the value or not */) { this._cachedCurrentSessionId = null; - this.update(); + this.update('change in storage'); } } diff --git a/src/vs/workbench/services/views/test/browser/viewContainerModel.test.ts b/src/vs/workbench/services/views/test/browser/viewContainerModel.test.ts index 57723675e03..9a39dda2611 100644 --- a/src/vs/workbench/services/views/test/browser/viewContainerModel.test.ts +++ b/src/vs/workbench/services/views/test/browser/viewContainerModel.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; import { IViewsRegistry, IViewDescriptor, IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainerLocation, IViewContainerModel, IViewDescriptorService, ViewContainer } from 'vs/workbench/common/views'; -import { IDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { move } from 'vs/base/common/arrays'; import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -19,6 +19,7 @@ import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storag import { Event } from 'vs/base/common/event'; import { getViewsStateStorageId } from 'vs/workbench/services/views/common/viewContainerModel'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; const ViewContainerRegistry = Registry.as(ViewContainerExtensions.ViewContainersRegistry); const ViewsRegistry = Registry.as(ViewContainerExtensions.ViewsRegistry); @@ -43,22 +44,20 @@ class ViewDescriptorSequence { suite('ViewContainerModel', () => { let container: ViewContainer; - let disposableStore: DisposableStore; + const disposableStore = ensureNoDisposablesAreLeakedInTestSuite(); let contextKeyService: IContextKeyService; let viewDescriptorService: IViewDescriptorService; let storageService: IStorageService; setup(() => { - disposableStore = new DisposableStore(); const instantiationService: TestInstantiationService = workbenchInstantiationService(undefined, disposableStore); - contextKeyService = instantiationService.createInstance(ContextKeyService); + contextKeyService = disposableStore.add(instantiationService.createInstance(ContextKeyService)); instantiationService.stub(IContextKeyService, contextKeyService); storageService = instantiationService.get(IStorageService); - viewDescriptorService = instantiationService.createInstance(ViewDescriptorService); + viewDescriptorService = disposableStore.add(instantiationService.createInstance(ViewDescriptorService)); }); teardown(() => { - disposableStore.dispose(); ViewsRegistry.deregisterViews(ViewsRegistry.getViews(container), container); ViewContainerRegistry.deregisterViewContainer(container); }); @@ -380,7 +379,7 @@ suite('ViewContainerModel', () => { assert.strictEqual(target.elements.length, 0); const targetEvent = sinon.spy(); - testObject.onDidRemoveVisibleViewDescriptors(targetEvent); + disposableStore.add(testObject.onDidRemoveVisibleViewDescriptors(targetEvent)); key.set(false); await new Promise(c => setTimeout(c, 30)); assert.ok(!targetEvent.called, 'remove event should not be called since it is already hidden'); @@ -406,7 +405,7 @@ suite('ViewContainerModel', () => { assert.strictEqual(target.elements.length, 0); const targetEvent = sinon.spy(); - testObject.onDidAddVisibleViewDescriptors(targetEvent); + disposableStore.add(testObject.onDidAddVisibleViewDescriptors(targetEvent)); testObject.setVisible('view1', true); assert.ok(!targetEvent.called, 'add event should not be called since it is already visible'); assert.strictEqual(testObject.visibleViewDescriptors.length, 0); @@ -433,7 +432,7 @@ suite('ViewContainerModel', () => { assert.strictEqual(target.elements.length, 0); const targetEvent = sinon.spy(); - testObject.onDidAddVisibleViewDescriptors(targetEvent); + disposableStore.add(testObject.onDidAddVisibleViewDescriptors(targetEvent)); testObject.setVisible('view1', false); assert.ok(!targetEvent.called, 'add event should not be called since it is disabled'); assert.strictEqual(testObject.visibleViewDescriptors.length, 0); @@ -464,7 +463,7 @@ suite('ViewContainerModel', () => { assert.strictEqual(target.elements.length, 0); const targetEvent = sinon.spy(); - testObject.onDidAddVisibleViewDescriptors(targetEvent); + disposableStore.add(testObject.onDidAddVisibleViewDescriptors(targetEvent)); testObject.setVisible('view1', true); assert.ok(!targetEvent.called, 'add event should not be called since it is disabled'); assert.strictEqual(testObject.visibleViewDescriptors.length, 0); @@ -543,8 +542,8 @@ suite('ViewContainerModel', () => { assert.strictEqual(target.elements.length, 0); const targetEvent = sinon.spy(); - testObject.onDidAddVisibleViewDescriptors(targetEvent); - Event.once(testObject.onDidChangeActiveViewDescriptors)(() => testObject.setVisible('view1', true)); + disposableStore.add(testObject.onDidAddVisibleViewDescriptors(targetEvent)); + disposableStore.add(Event.once(testObject.onDidChangeActiveViewDescriptors)(() => testObject.setVisible('view1', true))); key.set(true); await new Promise(c => setTimeout(c, 30)); assert.strictEqual(targetEvent.callCount, 1); @@ -573,8 +572,8 @@ suite('ViewContainerModel', () => { assert.strictEqual(target.elements.length, 0); const targetEvent = sinon.spy(); - testObject.onDidAddVisibleViewDescriptors(targetEvent); - Event.once(testObject.onDidChangeActiveViewDescriptors)(() => testObject.setVisible('view1', false)); + disposableStore.add(testObject.onDidAddVisibleViewDescriptors(targetEvent)); + disposableStore.add(Event.once(testObject.onDidChangeActiveViewDescriptors)(() => testObject.setVisible('view1', false))); key.set(true); await new Promise(c => setTimeout(c, 30)); assert.strictEqual(targetEvent.callCount, 0); @@ -631,10 +630,10 @@ suite('ViewContainerModel', () => { ViewsRegistry.registerViews([viewDescriptor1, viewDescriptor2, viewDescriptor3], container); const remomveEvent = sinon.spy(); - testObject.onDidRemoveVisibleViewDescriptors(remomveEvent); + disposableStore.add(testObject.onDidRemoveVisibleViewDescriptors(remomveEvent)); const addEvent = sinon.spy(); - testObject.onDidAddVisibleViewDescriptors(addEvent); + disposableStore.add(testObject.onDidAddVisibleViewDescriptors(addEvent)); storageService.store(getViewsStateStorageId('test.state'), JSON.stringify([{ id: viewDescriptor1.id, @@ -691,10 +690,10 @@ suite('ViewContainerModel', () => { testObject.setVisible(viewDescriptor3.id, false); const removeEvent = sinon.spy(); - testObject.onDidRemoveVisibleViewDescriptors(removeEvent); + disposableStore.add(testObject.onDidRemoveVisibleViewDescriptors(removeEvent)); const addEvent = sinon.spy(); - testObject.onDidAddVisibleViewDescriptors(addEvent); + disposableStore.add(testObject.onDidAddVisibleViewDescriptors(addEvent)); storageService.store(getViewsStateStorageId('test.state'), JSON.stringify([{ id: viewDescriptor1.id, @@ -764,10 +763,10 @@ suite('ViewContainerModel', () => { testObject.setVisible(viewDescriptor1.id, false); const removeEvent = sinon.spy(); - testObject.onDidRemoveVisibleViewDescriptors(removeEvent); + disposableStore.add(testObject.onDidRemoveVisibleViewDescriptors(removeEvent)); const addEvent = sinon.spy(); - testObject.onDidAddVisibleViewDescriptors(addEvent); + disposableStore.add(testObject.onDidAddVisibleViewDescriptors(addEvent)); storageService.store(getViewsStateStorageId('test.state'), JSON.stringify([{ id: viewDescriptor1.id, diff --git a/src/vs/workbench/services/views/test/browser/viewDescriptorService.test.ts b/src/vs/workbench/services/views/test/browser/viewDescriptorService.test.ts index 4a1c167c07f..481b0c25bd5 100644 --- a/src/vs/workbench/services/views/test/browser/viewDescriptorService.test.ts +++ b/src/vs/workbench/services/views/test/browser/viewDescriptorService.test.ts @@ -13,10 +13,10 @@ import { ViewDescriptorService } from 'vs/workbench/services/views/browser/viewD import { assertIsDefined } from 'vs/base/common/types'; import { ContextKeyService } from 'vs/platform/contextkey/browser/contextKeyService'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { DisposableStore } from 'vs/base/common/lifecycle'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { generateUuid } from 'vs/base/common/uuid'; import { compare } from 'vs/base/common/strings'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; const ViewsRegistry = Registry.as(ViewContainerExtensions.ViewsRegistry); const ViewContainersRegistry = Registry.as(ViewContainerExtensions.ViewContainersRegistry); @@ -26,12 +26,12 @@ const panelContainer = ViewContainersRegistry.registerViewContainer({ id: `${vie suite('ViewDescriptorService', () => { - const disposables = new DisposableStore(); + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); let instantiationService: TestInstantiationService; setup(() => { disposables.add(instantiationService = workbenchInstantiationService(undefined, disposables)); - instantiationService.stub(IContextKeyService, instantiationService.createInstance(ContextKeyService)); + instantiationService.stub(IContextKeyService, disposables.add(instantiationService.createInstance(ContextKeyService))); }); teardown(() => { @@ -40,7 +40,6 @@ suite('ViewDescriptorService', () => { ViewsRegistry.deregisterViews(ViewsRegistry.getViews(viewContainer), viewContainer); } } - disposables.clear(); }); function aViewDescriptorService(): ViewDescriptorService { @@ -222,7 +221,6 @@ suite('ViewDescriptorService', () => { let expectedSequence = ''; let actualSequence = ''; - const disposables = []; const containerMoveString = (view: IViewDescriptor, from: ViewContainer, to: ViewContainer) => { return `Moved ${view.id} from ${from.id} to ${to.id}\n`; @@ -231,13 +229,13 @@ suite('ViewDescriptorService', () => { const locationMoveString = (view: IViewDescriptor, from: ViewContainerLocation, to: ViewContainerLocation) => { return `Moved ${view.id} from ${from === ViewContainerLocation.Sidebar ? 'Sidebar' : 'Panel'} to ${to === ViewContainerLocation.Sidebar ? 'Sidebar' : 'Panel'}\n`; }; - disposables.push(testObject.onDidChangeContainer(({ views, from, to }) => { + disposables.add(testObject.onDidChangeContainer(({ views, from, to }) => { views.forEach(view => { actualSequence += containerMoveString(view, from, to); }); })); - disposables.push(testObject.onDidChangeLocation(({ views, from, to }) => { + disposables.add(testObject.onDidChangeLocation(({ views, from, to }) => { views.forEach(view => { actualSequence += locationMoveString(view, from, to); }); diff --git a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts index 00ab0443125..6332060450e 100644 --- a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts @@ -199,7 +199,7 @@ export class FileWorkingCopyManager extend const handle = this.notificationService.notify({ id: `${hash(this.resource.toString())}`, severity: Severity.Error, message, actions: { primary: primaryActions } }); // Remove automatically when we get saved/reverted - const listener = Event.once(Event.any(this.onDidSave, this.onDidRevert))(() => handle.close()); - Event.once(handle.onDidClose)(() => listener.dispose()); + const listener = this._register(Event.once(Event.any(this.onDidSave, this.onDidRevert))(() => handle.close())); + this._register(Event.once(handle.onDidClose)(() => listener.dispose())); } private updateLastResolvedFileStat(newFileStat: IFileStatWithMetadata): void { diff --git a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts index d08c9d07a14..cfdce4134e3 100644 --- a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts @@ -208,9 +208,9 @@ export class StoredFileWorkingCopyManager // Lifecycle if (isWeb) { - this.lifecycleService.onBeforeShutdown(event => event.veto(this.onBeforeShutdownWeb(), 'veto.fileWorkingCopyManager')); + this._register(this.lifecycleService.onBeforeShutdown(event => event.veto(this.onBeforeShutdownWeb(), 'veto.fileWorkingCopyManager'))); } else { - this.lifecycleService.onWillShutdown(event => event.join(this.onWillShutdownDesktop(), { id: 'join.fileWorkingCopyManager', label: localize('join.fileWorkingCopyManager', "Saving working copies") })); + this._register(this.lifecycleService.onWillShutdown(event => event.join(this.onWillShutdownDesktop(), { id: 'join.fileWorkingCopyManager', label: localize('join.fileWorkingCopyManager', "Saving working copies") }))); } } diff --git a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopySaveParticipant.ts b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopySaveParticipant.ts index 11bddb67625..86177563f33 100644 --- a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopySaveParticipant.ts +++ b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopySaveParticipant.ts @@ -61,6 +61,9 @@ export class StoredFileWorkingCopySaveParticipant extends Disposable { // undoStop after participation workingCopy.model?.pushStackElement(); + + // Cleanup + cts.dispose(); }, () => { // user cancel cts.dispose(true); diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyBackupService.ts b/src/vs/workbench/services/workingCopy/common/workingCopyBackupService.ts index f9ec56ad11a..16715e8b21d 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyBackupService.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyBackupService.ts @@ -116,7 +116,7 @@ export class WorkingCopyBackupsModel { } } -export abstract class WorkingCopyBackupService implements IWorkingCopyBackupService { +export abstract class WorkingCopyBackupService extends Disposable implements IWorkingCopyBackupService { declare readonly _serviceBrand: undefined; @@ -127,7 +127,9 @@ export abstract class WorkingCopyBackupService implements IWorkingCopyBackupServ @IFileService protected fileService: IFileService, @ILogService private readonly logService: ILogService ) { - this.impl = this.initialize(backupWorkspaceHome); + super(); + + this.impl = this._register(this.initialize(backupWorkspaceHome)); } private initialize(backupWorkspaceHome: URI | undefined): WorkingCopyBackupServiceImpl | InMemoryWorkingCopyBackupService { @@ -533,13 +535,15 @@ class WorkingCopyBackupServiceImpl extends Disposable implements IWorkingCopyBac } } -export class InMemoryWorkingCopyBackupService implements IWorkingCopyBackupService { +export class InMemoryWorkingCopyBackupService extends Disposable implements IWorkingCopyBackupService { declare readonly _serviceBrand: undefined; private backups = new ResourceMap<{ typeId: string; content: VSBuffer; meta?: IWorkingCopyBackupMeta }>(); - constructor() { } + constructor() { + super(); + } async hasBackups(): Promise { return this.backups.size > 0; diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyBackupTracker.ts b/src/vs/workbench/services/workingCopy/common/workingCopyBackupTracker.ts index 8775ac71554..8e0f773f30c 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyBackupTracker.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyBackupTracker.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; -import { Disposable, IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { IWorkingCopy, IWorkingCopyIdentifier, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; import { ILogService } from 'vs/platform/log/common/log'; @@ -56,8 +56,8 @@ export abstract class WorkingCopyBackupTracker extends Disposable { this._register(this.workingCopyService.onDidChangeContent(workingCopy => this.onDidChangeContent(workingCopy))); // Lifecycle - this.lifecycleService.onBeforeShutdown(event => (event as InternalBeforeShutdownEvent).finalVeto(() => this.onFinalBeforeShutdown(event.reason), 'veto.backups')); - this.lifecycleService.onWillShutdown(() => this.onWillShutdown()); + this._register(this.lifecycleService.onBeforeShutdown(event => (event as InternalBeforeShutdownEvent).finalVeto(() => this.onFinalBeforeShutdown(event.reason), 'veto.backups'))); + this._register(this.lifecycleService.onWillShutdown(() => this.onWillShutdown())); // Once a handler registers, restore backups this._register(this.workingCopyEditorService.onDidRegisterHandler(handler => this.restoreBackups(handler))); @@ -103,8 +103,8 @@ export abstract class WorkingCopyBackupTracker extends Disposable { // A map of scheduled pending backup operations for working copies // Given https://github.com/microsoft/vscode/issues/158038, we explicitly // do not store `IWorkingCopy` but the identifier in the map, since it - // looks like GC is not runnin for the working copy otherwise. - protected readonly pendingBackupOperations = new Map(); + // looks like GC is not running for the working copy otherwise. + protected readonly pendingBackupOperations = new Map void }>(); private suspended = false; @@ -206,17 +206,22 @@ export abstract class WorkingCopyBackupTracker extends Disposable { // Clear disposable unless we got canceled which would // indicate another operation has started meanwhile if (!cts.token.isCancellationRequested) { - this.pendingBackupOperations.delete(workingCopyIdentifier); + this.doClearPendingBackupOperation(workingCopyIdentifier); } }, this.getBackupScheduleDelay(workingCopy)); // Keep in map for disposal as needed - this.pendingBackupOperations.set(workingCopyIdentifier, toDisposable(() => { - this.logService.trace(`[backup tracker] clearing pending backup creation`, workingCopy.resource.toString(), workingCopy.typeId); + this.pendingBackupOperations.set(workingCopyIdentifier, { + cancel: () => { + this.logService.trace(`[backup tracker] clearing pending backup creation`, workingCopy.resource.toString(), workingCopy.typeId); - cts.dispose(true); - clearTimeout(handle); - })); + cts.cancel(); + }, + disposable: toDisposable(() => { + cts.dispose(); + clearTimeout(handle); + }) + }); } protected getBackupScheduleDelay(workingCopy: IWorkingCopy): number { @@ -247,11 +252,14 @@ export abstract class WorkingCopyBackupTracker extends Disposable { this.doDiscardBackup(workingCopyIdentifier, cts); // Keep in map for disposal as needed - this.pendingBackupOperations.set(workingCopyIdentifier, toDisposable(() => { - this.logService.trace(`[backup tracker] clearing pending backup discard`, workingCopy.resource.toString(), workingCopy.typeId); + this.pendingBackupOperations.set(workingCopyIdentifier, { + cancel: () => { + this.logService.trace(`[backup tracker] clearing pending backup discard`, workingCopy.resource.toString(), workingCopy.typeId); - cts.dispose(true); - })); + cts.cancel(); + }, + disposable: cts + }); } private async doDiscardBackup(workingCopyIdentifier: IWorkingCopyIdentifier, cts: CancellationTokenSource) { @@ -267,7 +275,7 @@ export abstract class WorkingCopyBackupTracker extends Disposable { // Clear disposable unless we got canceled which would // indicate another operation has started meanwhile if (!cts.token.isCancellationRequested) { - this.pendingBackupOperations.delete(workingCopyIdentifier); + this.doClearPendingBackupOperation(workingCopyIdentifier); } } @@ -287,14 +295,29 @@ export abstract class WorkingCopyBackupTracker extends Disposable { } if (workingCopyIdentifier) { - dispose(this.pendingBackupOperations.get(workingCopyIdentifier)); - this.pendingBackupOperations.delete(workingCopyIdentifier); + this.doClearPendingBackupOperation(workingCopyIdentifier, { cancel: true }); } } + private doClearPendingBackupOperation(workingCopyIdentifier: IWorkingCopyIdentifier, options?: { cancel: boolean }): void { + const pendingBackupOperation = this.pendingBackupOperations.get(workingCopyIdentifier); + if (!pendingBackupOperation) { + return; + } + + if (options?.cancel) { + pendingBackupOperation.cancel(); + } + + pendingBackupOperation.disposable.dispose(); + + this.pendingBackupOperations.delete(workingCopyIdentifier); + } + protected cancelBackupOperations(): void { - for (const [, disposable] of this.pendingBackupOperations) { - dispose(disposable); + for (const [, operation] of this.pendingBackupOperations) { + operation.cancel(); + operation.disposable.dispose(); } this.pendingBackupOperations.clear(); diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyHistoryService.ts b/src/vs/workbench/services/workingCopy/common/workingCopyHistoryService.ts index a689d217c5e..56e3fd5e9cd 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyHistoryService.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyHistoryService.ts @@ -814,7 +814,7 @@ export class NativeWorkingCopyHistoryService extends WorkingCopyHistoryService { if (!this.isRemotelyStored) { // Local: persist all on shutdown - this.lifecycleService.onWillShutdown(e => this.onWillShutdown(e)); + this._register(this.lifecycleService.onWillShutdown(e => this.onWillShutdown(e))); // Local: schedule persist on change this._register(Event.any(this.onDidAddEntry, this.onDidChangeEntry, this.onDidReplaceEntry, this.onDidRemoveEntry)(() => this.onDidChangeModels())); diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyService.ts b/src/vs/workbench/services/workingCopy/common/workingCopyService.ts index a5f58b91345..6b3fb90c562 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyService.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyService.ts @@ -7,7 +7,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { Event, Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; -import { Disposable, IDisposable, toDisposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, toDisposable, DisposableStore, DisposableMap } from 'vs/base/common/lifecycle'; import { ResourceMap } from 'vs/base/common/map'; import { IWorkingCopy, IWorkingCopyIdentifier, IWorkingCopySaveEvent as IBaseWorkingCopySaveEvent } from 'vs/workbench/services/workingCopy/common/workingCopy'; @@ -167,6 +167,7 @@ export class WorkingCopyService extends Disposable implements IWorkingCopyServic private _workingCopies = new Set(); private readonly mapResourceToWorkingCopies = new ResourceMap>(); + private readonly mapWorkingCopyToListeners = this._register(new DisposableMap()); registerWorkingCopy(workingCopy: IWorkingCopy): IDisposable { let workingCopiesForResource = this.mapResourceToWorkingCopies.get(workingCopy.resource); @@ -189,6 +190,7 @@ export class WorkingCopyService extends Disposable implements IWorkingCopyServic disposables.add(workingCopy.onDidChangeContent(() => this._onDidChangeContent.fire(workingCopy))); disposables.add(workingCopy.onDidChangeDirty(() => this._onDidChangeDirty.fire(workingCopy))); disposables.add(workingCopy.onDidSave(e => this._onDidSave.fire({ workingCopy, ...e }))); + this.mapWorkingCopyToListeners.set(workingCopy, disposables); // Send some initial events this._onDidRegister.fire(workingCopy); @@ -197,8 +199,9 @@ export class WorkingCopyService extends Disposable implements IWorkingCopyServic } return toDisposable(() => { + + // Unregister working copy this.unregisterWorkingCopy(workingCopy); - dispose(disposables); // Signal as event this._onDidUnregister.fire(workingCopy); @@ -221,6 +224,9 @@ export class WorkingCopyService extends Disposable implements IWorkingCopyServic if (workingCopy.isDirty()) { this._onDidChangeDirty.fire(workingCopy); } + + // Remove all listeners associated to working copy + this.mapWorkingCopyToListeners.deleteAndDispose(workingCopy); } has(identifier: IWorkingCopyIdentifier): boolean; diff --git a/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupService.ts b/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupService.ts index 1aa006ca6e6..5e597eaa2bc 100644 --- a/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupService.ts +++ b/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupService.ts @@ -34,7 +34,7 @@ export class NativeWorkingCopyBackupService extends WorkingCopyBackupService { // Lifecycle: ensure to prolong the shutdown for as long // as pending backup operations have not finished yet. // Otherwise, we risk writing partial backups to disk. - this.lifecycleService.onWillShutdown(event => event.join(this.joinBackups(), { id: 'join.workingCopyBackups', label: localize('join.workingCopyBackups', "Backup working copies") })); + this._register(this.lifecycleService.onWillShutdown(event => event.join(this.joinBackups(), { id: 'join.workingCopyBackups', label: localize('join.workingCopyBackups', "Backup working copies") }))); } } diff --git a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts index 307ed8eb379..16185733f61 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts @@ -15,24 +15,24 @@ import { IFileWorkingCopyManager, FileWorkingCopyManager } from 'vs/workbench/se import { TestUntitledFileWorkingCopyModel, TestUntitledFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test'; import { UntitledFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopy'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('FileWorkingCopyManager', () => { - let disposables: DisposableStore; + const disposables = new DisposableStore(); let instantiationService: IInstantiationService; let accessor: TestServiceAccessor; let manager: IFileWorkingCopyManager; setup(() => { - disposables = new DisposableStore(); instantiationService = workbenchInstantiationService(undefined, disposables); accessor = instantiationService.createInstance(TestServiceAccessor); accessor.fileService.registerProvider(Schemas.file, new TestInMemoryFileSystemProvider()); accessor.fileService.registerProvider(Schemas.vscodeRemote, new TestInMemoryFileSystemProvider()); - manager = new FileWorkingCopyManager( + manager = disposables.add(new FileWorkingCopyManager( 'testFileWorkingCopyType', new TestStoredFileWorkingCopyModelFactory(), new TestUntitledFileWorkingCopyModelFactory(), @@ -41,19 +41,18 @@ suite('FileWorkingCopyManager', () => { accessor.filesConfigurationService, accessor.workingCopyService, accessor.notificationService, accessor.workingCopyEditorService, accessor.editorService, accessor.elevatedFileService, accessor.pathService, accessor.environmentService, accessor.dialogService, accessor.decorationsService - ); + )); }); teardown(() => { - manager.dispose(); - disposables.dispose(); + disposables.clear(); }); test('onDidCreate, get, workingCopies', async () => { let createCounter = 0; - manager.onDidCreate(e => { + disposables.add(manager.onDidCreate(e => { createCounter++; - }); + })); const fileUri = URI.file('/test.html'); @@ -68,29 +67,23 @@ suite('FileWorkingCopyManager', () => { assert.strictEqual(manager.get(fileWorkingCopy.resource), fileWorkingCopy); assert.strictEqual(manager.get(untitledFileWorkingCopy.resource), untitledFileWorkingCopy); - const sameFileWorkingCopy = await manager.resolve(fileUri); - const sameUntitledFileWorkingCopy = await manager.resolve({ untitledResource: untitledFileWorkingCopy.resource }); + const sameFileWorkingCopy = disposables.add(await manager.resolve(fileUri)); + const sameUntitledFileWorkingCopy = disposables.add(await manager.resolve({ untitledResource: untitledFileWorkingCopy.resource })); assert.strictEqual(sameFileWorkingCopy, fileWorkingCopy); assert.strictEqual(sameUntitledFileWorkingCopy, untitledFileWorkingCopy); assert.strictEqual(manager.workingCopies.length, 2); assert.strictEqual(createCounter, 2); - - fileWorkingCopy.dispose(); - untitledFileWorkingCopy.dispose(); }); test('resolve', async () => { - const fileWorkingCopy = await manager.resolve(URI.file('/test.html')); + const fileWorkingCopy = disposables.add(await manager.resolve(URI.file('/test.html'))); assert.ok(fileWorkingCopy instanceof StoredFileWorkingCopy); assert.strictEqual(await manager.stored.resolve(fileWorkingCopy.resource), fileWorkingCopy); - const untitledFileWorkingCopy = await manager.resolve(); + const untitledFileWorkingCopy = disposables.add(await manager.resolve()); assert.ok(untitledFileWorkingCopy instanceof UntitledFileWorkingCopy); assert.strictEqual(await manager.untitled.resolve({ untitledResource: untitledFileWorkingCopy.resource }), untitledFileWorkingCopy); assert.strictEqual(await manager.resolve(untitledFileWorkingCopy.resource), untitledFileWorkingCopy); - - fileWorkingCopy.dispose(); - untitledFileWorkingCopy.dispose(); }); test('destroy', async () => { @@ -186,14 +179,14 @@ suite('FileWorkingCopyManager', () => { async function testSaveAsFile(source: URI, target: URI, resolveSource: boolean, resolveTarget: boolean) { let sourceWorkingCopy: IStoredFileWorkingCopy | undefined = undefined; if (resolveSource) { - sourceWorkingCopy = await manager.resolve(source); + sourceWorkingCopy = disposables.add(await manager.resolve(source)); sourceWorkingCopy.model?.updateContents('hello world'); assert.ok(sourceWorkingCopy.isDirty()); } let targetWorkingCopy: IStoredFileWorkingCopy | undefined = undefined; if (resolveTarget) { - targetWorkingCopy = await manager.resolve(target); + targetWorkingCopy = disposables.add(await manager.resolve(target)); targetWorkingCopy.model?.updateContents('hello world'); assert.ok(targetWorkingCopy.isDirty()); } @@ -224,10 +217,12 @@ suite('FileWorkingCopyManager', () => { if (resolveTarget) { assert.strictEqual(targetWorkingCopy?.isDirty(), false); } + + result?.dispose(); } test('saveAs - untitled (without associated resource)', async () => { - const workingCopy = await manager.resolve(); + const workingCopy = disposables.add(await manager.resolve()); workingCopy.model?.updateContents('Simple Save As'); const target = URI.file('simple/file.txt'); @@ -240,11 +235,11 @@ suite('FileWorkingCopyManager', () => { assert.strictEqual(manager.untitled.get(workingCopy.resource), undefined); - workingCopy.dispose(); + result?.dispose(); }); test('saveAs - untitled (with associated resource)', async () => { - const workingCopy = await manager.resolve({ associatedResource: { path: '/some/associated.txt' } }); + const workingCopy = disposables.add(await manager.resolve({ associatedResource: { path: '/some/associated.txt' } })); workingCopy.model?.updateContents('Simple Save As with associated resource'); const target = URI.from({ scheme: Schemas.file, path: '/some/associated.txt' }); @@ -258,11 +253,11 @@ suite('FileWorkingCopyManager', () => { assert.strictEqual(manager.untitled.get(workingCopy.resource), undefined); - workingCopy.dispose(); + result?.dispose(); }); test('saveAs - untitled (target exists and is resolved)', async () => { - const workingCopy = await manager.resolve(); + const workingCopy = disposables.add(await manager.resolve()); workingCopy.model?.updateContents('Simple Save As'); const target = URI.file('simple/file.txt'); @@ -276,6 +271,8 @@ suite('FileWorkingCopyManager', () => { assert.strictEqual(manager.untitled.get(workingCopy.resource), undefined); - workingCopy.dispose(); + result?.dispose(); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/services/workingCopy/test/browser/resourceWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/resourceWorkingCopy.test.ts index b7f6501ab42..d4d9097c020 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/resourceWorkingCopy.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/resourceWorkingCopy.test.ts @@ -15,6 +15,7 @@ import { ResourceWorkingCopy } from 'vs/workbench/services/workingCopy/common/re import { WorkingCopyCapabilities, IWorkingCopyBackup } from 'vs/workbench/services/workingCopy/common/workingCopy'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('ResourceWorkingCopy', function () { @@ -32,7 +33,7 @@ suite('ResourceWorkingCopy', function () { } - let disposables: DisposableStore; + const disposables = new DisposableStore(); const resource = URI.file('test/resource'); let instantiationService: IInstantiationService; let accessor: TestServiceAccessor; @@ -43,16 +44,14 @@ suite('ResourceWorkingCopy', function () { } setup(() => { - disposables = new DisposableStore(); instantiationService = workbenchInstantiationService(undefined, disposables); accessor = instantiationService.createInstance(TestServiceAccessor); - workingCopy = createWorkingCopy(); + workingCopy = disposables.add(createWorkingCopy()); }); teardown(() => { - workingCopy.dispose(); - disposables.dispose(); + disposables.clear(); }); test('orphaned tracking', async () => { @@ -75,18 +74,19 @@ suite('ResourceWorkingCopy', function () { }); }); - test('dispose, isDisposed', async () => { assert.strictEqual(workingCopy.isDisposed(), false); let disposedEvent = false; - workingCopy.onWillDispose(() => { + disposables.add(workingCopy.onWillDispose(() => { disposedEvent = true; - }); + })); workingCopy.dispose(); assert.strictEqual(workingCopy.isDisposed(), true); assert.strictEqual(disposedEvent, true); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts index df3106ac319..17cfba00f84 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts @@ -18,6 +18,7 @@ import { SaveReason, SaveSourceRegistry } from 'vs/workbench/common/editor'; import { Promises, timeout } from 'vs/base/common/async'; import { consumeReadable, consumeStream, isReadableStream } from 'vs/base/common/stream'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; export class TestStoredFileWorkingCopyModel extends Disposable implements IStoredFileWorkingCopyModel { @@ -129,43 +130,36 @@ suite('StoredFileWorkingCopy (with custom save)', function () { const factory = new TestStoredFileWorkingCopyModelWithCustomSaveFactory(); - let disposables: DisposableStore; - const resource = URI.file('test/resource'); + const disposables = new DisposableStore(); + let instantiationService: IInstantiationService; let accessor: TestServiceAccessor; let workingCopy: StoredFileWorkingCopy; - function createWorkingCopy(uri: URI = resource) { - const workingCopy: StoredFileWorkingCopy = new StoredFileWorkingCopy('testStoredFileWorkingCopyType', uri, basename(uri), factory, options => workingCopy.resolve(options), accessor.fileService, accessor.logService, accessor.workingCopyFileService, accessor.filesConfigurationService, accessor.workingCopyBackupService, accessor.workingCopyService, accessor.notificationService, accessor.workingCopyEditorService, accessor.editorService, accessor.elevatedFileService); - - return workingCopy; - } - setup(() => { - disposables = new DisposableStore(); instantiationService = workbenchInstantiationService(undefined, disposables); accessor = instantiationService.createInstance(TestServiceAccessor); - workingCopy = createWorkingCopy(); + const resource = URI.file('test/resource'); + workingCopy = disposables.add(new StoredFileWorkingCopy('testStoredFileWorkingCopyType', resource, basename(resource), factory, options => workingCopy.resolve(options), accessor.fileService, accessor.logService, accessor.workingCopyFileService, accessor.filesConfigurationService, accessor.workingCopyBackupService, accessor.workingCopyService, accessor.notificationService, accessor.workingCopyEditorService, accessor.editorService, accessor.elevatedFileService)); }); teardown(() => { - workingCopy.dispose(); - disposables.dispose(); + disposables.clear(); }); test('save (custom implemented)', async () => { let savedCounter = 0; let lastSaveEvent: IStoredFileWorkingCopySaveEvent | undefined = undefined; - workingCopy.onDidSave(e => { + disposables.add(workingCopy.onDidSave(e => { savedCounter++; lastSaveEvent = e; - }); + })); let saveErrorCounter = 0; - workingCopy.onDidSaveError(() => { + disposables.add(workingCopy.onDidSaveError(() => { saveErrorCounter++; - }); + })); // unresolved await workingCopy.save(); @@ -194,13 +188,15 @@ suite('StoredFileWorkingCopy (with custom save)', function () { assert.strictEqual(saveErrorCounter, 1); assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.ERROR), true); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); suite('StoredFileWorkingCopy', function () { const factory = new TestStoredFileWorkingCopyModelFactory(); - let disposables: DisposableStore; + const disposables = new DisposableStore(); const resource = URI.file('test/resource'); let instantiationService: IInstantiationService; let accessor: TestServiceAccessor; @@ -213,16 +209,20 @@ suite('StoredFileWorkingCopy', function () { } setup(() => { - disposables = new DisposableStore(); instantiationService = workbenchInstantiationService(undefined, disposables); accessor = instantiationService.createInstance(TestServiceAccessor); - workingCopy = createWorkingCopy(); + workingCopy = disposables.add(createWorkingCopy()); }); teardown(() => { workingCopy.dispose(); - disposables.dispose(); + + for (const workingCopy of accessor.workingCopyService.workingCopies) { + (workingCopy as StoredFileWorkingCopy).dispose(); + } + + disposables.clear(); }); test('registers with working copy service', async () => { @@ -262,19 +262,19 @@ suite('StoredFileWorkingCopy', function () { assert.strictEqual(workingCopy.isResolved(), true); let changeDirtyCounter = 0; - workingCopy.onDidChangeDirty(() => { + disposables.add(workingCopy.onDidChangeDirty(() => { changeDirtyCounter++; - }); + })); let contentChangeCounter = 0; - workingCopy.onDidChangeContent(() => { + disposables.add(workingCopy.onDidChangeContent(() => { contentChangeCounter++; - }); + })); let savedCounter = 0; - workingCopy.onDidSave(() => { + disposables.add(workingCopy.onDidSave(() => { savedCounter++; - }); + })); // Dirty from: Model content change workingCopy.model?.updateContents('hello dirty'); @@ -344,9 +344,9 @@ suite('StoredFileWorkingCopy', function () { test('resolve (without backup)', async () => { let onDidResolveCounter = 0; - workingCopy.onDidResolve(() => { + disposables.add(workingCopy.onDidResolve(() => { onDidResolveCounter++; - }); + })); // resolve from file await workingCopy.resolve(); @@ -482,7 +482,7 @@ suite('StoredFileWorkingCopy', function () { test('resolve (FILE_NOT_MODIFIED_SINCE still updates readonly state)', async () => { let readonlyChangeCounter = 0; - workingCopy.onDidChangeReadonly(() => readonlyChangeCounter++); + disposables.add(workingCopy.onDidChangeReadonly(() => readonlyChangeCounter++)); await workingCopy.resolve(); @@ -547,15 +547,15 @@ suite('StoredFileWorkingCopy', function () { test('save (no errors) - simple', async () => { let savedCounter = 0; let lastSaveEvent: IStoredFileWorkingCopySaveEvent | undefined = undefined; - workingCopy.onDidSave(e => { + disposables.add(workingCopy.onDidSave(e => { savedCounter++; lastSaveEvent = e; - }); + })); let saveErrorCounter = 0; - workingCopy.onDidSaveError(() => { + disposables.add(workingCopy.onDidSaveError(() => { saveErrorCounter++; - }); + })); // unresolved await workingCopy.save(); @@ -579,15 +579,15 @@ suite('StoredFileWorkingCopy', function () { test('save (no errors) - save reason', async () => { let savedCounter = 0; let lastSaveEvent: IStoredFileWorkingCopySaveEvent | undefined = undefined; - workingCopy.onDidSave(e => { + disposables.add(workingCopy.onDidSave(e => { savedCounter++; lastSaveEvent = e; - }); + })); let saveErrorCounter = 0; - workingCopy.onDidSaveError(() => { + disposables.add(workingCopy.onDidSaveError(() => { saveErrorCounter++; - }); + })); // save reason await workingCopy.resolve(); @@ -605,14 +605,14 @@ suite('StoredFileWorkingCopy', function () { test('save (no errors) - multiple', async () => { let savedCounter = 0; - workingCopy.onDidSave(e => { + disposables.add(workingCopy.onDidSave(e => { savedCounter++; - }); + })); let saveErrorCounter = 0; - workingCopy.onDidSaveError(() => { + disposables.add(workingCopy.onDidSaveError(() => { saveErrorCounter++; - }); + })); // multiple saves in parallel are fine and result // in a single save when content does not change @@ -631,14 +631,14 @@ suite('StoredFileWorkingCopy', function () { test('save (no errors) - multiple, cancellation', async () => { let savedCounter = 0; - workingCopy.onDidSave(e => { + disposables.add(workingCopy.onDidSave(e => { savedCounter++; - }); + })); let saveErrorCounter = 0; - workingCopy.onDidSaveError(() => { + disposables.add(workingCopy.onDidSaveError(() => { saveErrorCounter++; - }); + })); // multiple saves in parallel are fine and result // in just one save operation (the second one @@ -657,14 +657,14 @@ suite('StoredFileWorkingCopy', function () { test('save (no errors) - not forced but not dirty', async () => { let savedCounter = 0; - workingCopy.onDidSave(e => { + disposables.add(workingCopy.onDidSave(e => { savedCounter++; - }); + })); let saveErrorCounter = 0; - workingCopy.onDidSaveError(() => { + disposables.add(workingCopy.onDidSaveError(() => { saveErrorCounter++; - }); + })); // no save when not forced and not dirty await workingCopy.resolve(); @@ -676,14 +676,14 @@ suite('StoredFileWorkingCopy', function () { test('save (no errors) - forced but not dirty', async () => { let savedCounter = 0; - workingCopy.onDidSave(e => { + disposables.add(workingCopy.onDidSave(e => { savedCounter++; - }); + })); let saveErrorCounter = 0; - workingCopy.onDidSaveError(() => { + disposables.add(workingCopy.onDidSaveError(() => { saveErrorCounter++; - }); + })); // save when forced even when not dirty await workingCopy.resolve(); @@ -696,14 +696,14 @@ suite('StoredFileWorkingCopy', function () { test('save (no errors) - save clears orphaned', async () => { runWithFakedTimers({}, async () => { let savedCounter = 0; - workingCopy.onDidSave(e => { + disposables.add(workingCopy.onDidSave(e => { savedCounter++; - }); + })); let saveErrorCounter = 0; - workingCopy.onDidSaveError(() => { + disposables.add(workingCopy.onDidSaveError(() => { saveErrorCounter++; - }); + })); await workingCopy.resolve(); @@ -726,14 +726,14 @@ suite('StoredFileWorkingCopy', function () { test('save (errors)', async () => { let savedCounter = 0; - workingCopy.onDidSave(reason => { + disposables.add(workingCopy.onDidSave(reason => { savedCounter++; - }); + })); let saveErrorCounter = 0; - workingCopy.onDidSaveError(() => { + disposables.add(workingCopy.onDidSaveError(() => { saveErrorCounter++; - }); + })); await workingCopy.resolve(); @@ -904,9 +904,9 @@ suite('StoredFileWorkingCopy', function () { workingCopy.model?.updateContents('hello revert'); let revertedCounter = 0; - workingCopy.onDidRevert(() => { + disposables.add(workingCopy.onDidRevert(() => { revertedCounter++; - }); + })); // revert: soft await workingCopy.revert({ soft: true }); @@ -1000,14 +1000,14 @@ suite('StoredFileWorkingCopy', function () { assert.strictEqual(workingCopy.isDisposed(), false); let disposedEvent = false; - workingCopy.onWillDispose(() => { + disposables.add(workingCopy.onWillDispose(() => { disposedEvent = true; - }); + })); let disposedModelEvent = false; - workingCopy.model.onWillDispose(() => { + disposables.add(workingCopy.model.onWillDispose(() => { disposedModelEvent = true; - }); + })); workingCopy.dispose(); @@ -1026,13 +1026,15 @@ suite('StoredFileWorkingCopy', function () { accessor.fileService.readonly = false; let readonlyEvent = false; - workingCopy.onDidChangeReadonly(() => { + disposables.add(workingCopy.onDidChangeReadonly(() => { readonlyEvent = true; - }); + })); await workingCopy.resolve(); assert.strictEqual(workingCopy.isReadonly(), false); assert.strictEqual(readonlyEvent, true); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopyManager.test.ts index cfb69f33e2e..54366a2df21 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopyManager.test.ts @@ -17,33 +17,36 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { isWeb } from 'vs/base/common/platform'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('StoredFileWorkingCopyManager', () => { - let disposables: DisposableStore; + const disposables = new DisposableStore(); let instantiationService: IInstantiationService; let accessor: TestServiceAccessor; let manager: IStoredFileWorkingCopyManager; setup(() => { - disposables = new DisposableStore(); instantiationService = workbenchInstantiationService(undefined, disposables); accessor = instantiationService.createInstance(TestServiceAccessor); - manager = new StoredFileWorkingCopyManager( + manager = disposables.add(new StoredFileWorkingCopyManager( 'testStoredFileWorkingCopyType', new TestStoredFileWorkingCopyModelFactory(), accessor.fileService, accessor.lifecycleService, accessor.labelService, accessor.logService, accessor.workingCopyFileService, accessor.workingCopyBackupService, accessor.uriIdentityService, accessor.filesConfigurationService, accessor.workingCopyService, accessor.notificationService, accessor.workingCopyEditorService, accessor.editorService, accessor.elevatedFileService - ); + )); }); teardown(() => { - manager.dispose(); - disposables.dispose(); + for (const workingCopy of manager.workingCopies) { + workingCopy.dispose(); + } + + disposables.clear(); }); test('resolve', async () => { @@ -92,19 +95,19 @@ suite('StoredFileWorkingCopyManager', () => { test('resolve (async)', async () => { const resource = URI.file('/path/index.txt'); - await manager.resolve(resource); + disposables.add(await manager.resolve(resource)); let didResolve = false; let onDidResolve = new Promise(resolve => { - manager.onDidResolve(({ model }) => { + disposables.add(manager.onDidResolve(({ model }) => { if (model?.resource.toString() === resource.toString()) { didResolve = true; resolve(); } - }); + })); }); - manager.resolve(resource, { reload: { async: true } }); + const resolve = manager.resolve(resource, { reload: { async: true } }); await onDidResolve; @@ -113,12 +116,12 @@ suite('StoredFileWorkingCopyManager', () => { didResolve = false; onDidResolve = new Promise(resolve => { - manager.onDidResolve(({ model }) => { + disposables.add(manager.onDidResolve(({ model }) => { if (model?.resource.toString() === resource.toString()) { didResolve = true; resolve(); } - }); + })); }); manager.resolve(resource, { reload: { async: true, force: true } }); @@ -126,6 +129,8 @@ suite('StoredFileWorkingCopyManager', () => { await onDidResolve; assert.strictEqual(didResolve, true); + + disposables.add(await resolve); }); test('resolve (sync)', async () => { @@ -134,18 +139,18 @@ suite('StoredFileWorkingCopyManager', () => { await manager.resolve(resource); let didResolve = false; - manager.onDidResolve(({ model }) => { + disposables.add(manager.onDidResolve(({ model }) => { if (model?.resource.toString() === resource.toString()) { didResolve = true; } - }); + })); - await manager.resolve(resource, { reload: { async: false } }); + disposables.add(await manager.resolve(resource, { reload: { async: false } })); assert.strictEqual(didResolve, true); didResolve = false; - await manager.resolve(resource, { reload: { async: false, force: true } }); + disposables.add(await manager.resolve(resource, { reload: { async: false, force: true } })); assert.strictEqual(didResolve, true); }); @@ -172,7 +177,7 @@ suite('StoredFileWorkingCopyManager', () => { test('resolve (sync) - model not disposed when error and model existed before', async () => { const resource = URI.file('/path/index.txt'); - await manager.resolve(resource); + disposables.add(await manager.resolve(resource)); accessor.fileService.readShouldThrowError = new FileOperationError('fail', FileOperationResult.FILE_OTHER_ERROR); @@ -274,23 +279,23 @@ suite('StoredFileWorkingCopyManager', () => { let savedCounter = 0; let saveErrorCounter = 0; - manager.onDidCreate(() => { + disposables.add(manager.onDidCreate(() => { createdCounter++; - }); + })); - manager.onDidRemove(resource => { + disposables.add(manager.onDidRemove(resource => { if (resource.toString() === resource1.toString() || resource.toString() === resource2.toString()) { removedCounter++; } - }); + })); - manager.onDidResolve(workingCopy => { + disposables.add(manager.onDidResolve(workingCopy => { if (workingCopy.resource.toString() === resource1.toString()) { resolvedCounter++; } - }); + })); - manager.onDidChangeDirty(workingCopy => { + disposables.add(manager.onDidChangeDirty(workingCopy => { if (workingCopy.resource.toString() === resource1.toString()) { if (workingCopy.isDirty()) { gotDirtyCounter++; @@ -298,36 +303,36 @@ suite('StoredFileWorkingCopyManager', () => { gotNonDirtyCounter++; } } - }); + })); - manager.onDidRevert(workingCopy => { + disposables.add(manager.onDidRevert(workingCopy => { if (workingCopy.resource.toString() === resource1.toString()) { revertedCounter++; } - }); + })); let lastSaveEvent: IStoredFileWorkingCopySaveEvent | undefined = undefined; - manager.onDidSave((e) => { + disposables.add(manager.onDidSave((e) => { if (e.workingCopy.resource.toString() === resource1.toString()) { lastSaveEvent = e; savedCounter++; } - }); + })); - manager.onDidSaveError(workingCopy => { + disposables.add(manager.onDidSaveError(workingCopy => { if (workingCopy.resource.toString() === resource1.toString()) { saveErrorCounter++; } - }); + })); - const workingCopy1 = await manager.resolve(resource1); + const workingCopy1 = disposables.add(await manager.resolve(resource1)); assert.strictEqual(resolvedCounter, 1); assert.strictEqual(createdCounter, 1); accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource: resource1, type: FileChangeType.DELETED }], false)); accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource: resource1, type: FileChangeType.ADDED }], false)); - const workingCopy2 = await manager.resolve(resource2); + const workingCopy2 = disposables.add(await manager.resolve(resource2)); assert.strictEqual(resolvedCounter, 2); assert.strictEqual(createdCounter, 2); @@ -386,6 +391,10 @@ suite('StoredFileWorkingCopyManager', () => { // dispose does not remove from working copy service, only `destroy` should assert.strictEqual(accessor.workingCopyService.workingCopies.length, 3); + + disposables.add(await firstPromise); + disposables.add(await secondPromise); + disposables.add(await thirdPromise); }); test('destroy', async () => { @@ -416,9 +425,9 @@ suite('StoredFileWorkingCopyManager', () => { const workingCopy = await manager.resolve(resource); let saved = false; - workingCopy.onDidSave(() => { + disposables.add(workingCopy.onDidSave(() => { saved = true; - }); + })); workingCopy.model?.updateContents('hello create'); assert.strictEqual(workingCopy.isDirty(), true); @@ -441,9 +450,9 @@ suite('StoredFileWorkingCopyManager', () => { workingCopy.model?.setThrowOnSnapshot(); let unexpectedSave = false; - workingCopy.onDidSave(() => { + disposables.add(workingCopy.onDidSave(() => { unexpectedSave = true; - }); + })); workingCopy.model?.updateContents('hello create'); assert.strictEqual(workingCopy.isDirty(), true); @@ -468,12 +477,12 @@ suite('StoredFileWorkingCopyManager', () => { let didResolve = false; const onDidResolve = new Promise(resolve => { - manager.onDidResolve(({ model }) => { + disposables.add(manager.onDidResolve(({ model }) => { if (model?.resource.toString() === resource.toString()) { didResolve = true; resolve(); } - }); + })); }); accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.UPDATED }], false)); @@ -491,7 +500,7 @@ suite('StoredFileWorkingCopyManager', () => { let didResolve = false; let resolvedCounter = 0; const onDidResolve = new Promise(resolve => { - manager.onDidResolve(({ model }) => { + disposables.add(manager.onDidResolve(({ model }) => { if (model?.resource.toString() === resource.toString()) { resolvedCounter++; if (resolvedCounter === 2) { @@ -499,7 +508,7 @@ suite('StoredFileWorkingCopyManager', () => { resolve(); } } - }); + })); }); accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.UPDATED }], false)); @@ -512,19 +521,19 @@ suite('StoredFileWorkingCopyManager', () => { test('file system provider change triggers working copy resolve', async () => { const resource = URI.file('/path/index.txt'); - await manager.resolve(resource); + disposables.add(await manager.resolve(resource)); let didResolve = false; const onDidResolve = new Promise(resolve => { - manager.onDidResolve(({ model }) => { + disposables.add(manager.onDidResolve(({ model }) => { if (model?.resource.toString() === resource.toString()) { didResolve = true; resolve(); } - }); + })); }); - accessor.fileService.fireFileSystemProviderCapabilitiesChangeEvent({ provider: new InMemoryFileSystemProvider(), scheme: resource.scheme }); + accessor.fileService.fireFileSystemProviderCapabilitiesChangeEvent({ provider: disposables.add(new InMemoryFileSystemProvider()), scheme: resource.scheme }); await onDidResolve; @@ -622,10 +631,10 @@ suite('StoredFileWorkingCopyManager', () => { const resource1 = URI.file('/path/index_something1.txt'); const resource2 = URI.file('/path/index_something2.txt'); - const workingCopy1 = await manager.resolve(resource1); + const workingCopy1 = disposables.add(await manager.resolve(resource1)); workingCopy1.model?.updateContents('make dirty'); - const workingCopy2 = await manager.resolve(resource2); + const workingCopy2 = disposables.add(await manager.resolve(resource2)); workingCopy2.model?.updateContents('make dirty'); let saved1 = false; @@ -647,4 +656,6 @@ suite('StoredFileWorkingCopyManager', () => { assert.strictEqual(saved1, true); assert.strictEqual(saved2, true); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts index c98fad63dac..94e3c941233 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts @@ -12,6 +12,7 @@ import { Schemas } from 'vs/base/common/network'; import { basename } from 'vs/base/common/resources'; import { consumeReadable, consumeStream, isReadable, isReadableStream } from 'vs/base/common/stream'; import { URI } from 'vs/base/common/uri'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IUntitledFileWorkingCopyModel, IUntitledFileWorkingCopyModelContentChangedEvent, IUntitledFileWorkingCopyModelFactory, UntitledFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopy'; import { TestServiceAccessor, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; @@ -90,14 +91,14 @@ suite('UntitledFileWorkingCopy', () => { const factory = new TestUntitledFileWorkingCopyModelFactory(); - let disposables: DisposableStore; + const disposables = new DisposableStore(); const resource = URI.from({ scheme: Schemas.untitled, path: 'Untitled-1' }); let instantiationService: IInstantiationService; let accessor: TestServiceAccessor; let workingCopy: UntitledFileWorkingCopy; function createWorkingCopy(uri: URI = resource, hasAssociatedFilePath = false, initialValue = '') { - return new UntitledFileWorkingCopy( + return disposables.add(new UntitledFileWorkingCopy( 'testUntitledWorkingCopyType', uri, basename(uri), @@ -109,20 +110,18 @@ suite('UntitledFileWorkingCopy', () => { accessor.workingCopyService, accessor.workingCopyBackupService, accessor.logService - ); + )); } setup(() => { - disposables = new DisposableStore(); instantiationService = workbenchInstantiationService(undefined, disposables); accessor = instantiationService.createInstance(TestServiceAccessor); - workingCopy = createWorkingCopy(); + workingCopy = disposables.add(createWorkingCopy()); }); teardown(() => { - workingCopy.dispose(); - disposables.dispose(); + disposables.clear(); }); test('registers with working copy service', async () => { @@ -137,14 +136,14 @@ suite('UntitledFileWorkingCopy', () => { assert.strictEqual(workingCopy.isDirty(), false); let changeDirtyCounter = 0; - workingCopy.onDidChangeDirty(() => { + disposables.add(workingCopy.onDidChangeDirty(() => { changeDirtyCounter++; - }); + })); let contentChangeCounter = 0; - workingCopy.onDidChangeContent(() => { + disposables.add(workingCopy.onDidChangeContent(() => { contentChangeCounter++; - }); + })); await workingCopy.resolve(); assert.strictEqual(workingCopy.isResolved(), true); @@ -191,14 +190,14 @@ suite('UntitledFileWorkingCopy', () => { test('revert', async () => { let revertCounter = 0; - workingCopy.onDidRevert(() => { + disposables.add(workingCopy.onDidRevert(() => { revertCounter++; - }); + })); let disposeCounter = 0; - workingCopy.onWillDispose(() => { + disposables.add(workingCopy.onWillDispose(() => { disposeCounter++; - }); + })); await workingCopy.resolve(); @@ -214,9 +213,9 @@ suite('UntitledFileWorkingCopy', () => { test('dispose', async () => { let disposeCounter = 0; - workingCopy.onWillDispose(() => { + disposables.add(workingCopy.onWillDispose(() => { disposeCounter++; - }); + })); await workingCopy.resolve(); workingCopy.dispose(); @@ -259,9 +258,9 @@ suite('UntitledFileWorkingCopy', () => { workingCopy = createWorkingCopy(resource, false, 'Hello Initial'); let contentChangeCounter = 0; - workingCopy.onDidChangeContent(() => { + disposables.add(workingCopy.onDidChangeContent(() => { contentChangeCounter++; - }); + })); assert.strictEqual(workingCopy.isDirty(), true); @@ -321,9 +320,9 @@ suite('UntitledFileWorkingCopy', () => { workingCopy = createWorkingCopy(); let contentChangeCounter = 0; - workingCopy.onDidChangeContent(() => { + disposables.add(workingCopy.onDidChangeContent(() => { contentChangeCounter++; - }); + })); await workingCopy.resolve(); @@ -331,4 +330,6 @@ suite('UntitledFileWorkingCopy', () => { assert.strictEqual(workingCopy.model?.contents, 'Hello Backup'); assert.strictEqual(contentChangeCounter, 1); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts index c32b141d5f6..cfac100bd5b 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts @@ -8,6 +8,7 @@ import { bufferToStream, VSBuffer } from 'vs/base/common/buffer'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { FileWorkingCopyManager, IFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; import { NO_TYPE_ID, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; @@ -17,21 +18,20 @@ import { TestInMemoryFileSystemProvider, TestServiceAccessor, workbenchInstantia suite('UntitledFileWorkingCopyManager', () => { - let disposables: DisposableStore; + const disposables = new DisposableStore(); let instantiationService: IInstantiationService; let accessor: TestServiceAccessor; let manager: IFileWorkingCopyManager; setup(() => { - disposables = new DisposableStore(); instantiationService = workbenchInstantiationService(undefined, disposables); accessor = instantiationService.createInstance(TestServiceAccessor); - accessor.fileService.registerProvider(Schemas.file, new TestInMemoryFileSystemProvider()); - accessor.fileService.registerProvider(Schemas.vscodeRemote, new TestInMemoryFileSystemProvider()); + disposables.add(accessor.fileService.registerProvider(Schemas.file, disposables.add(new TestInMemoryFileSystemProvider()))); + disposables.add(accessor.fileService.registerProvider(Schemas.vscodeRemote, disposables.add(new TestInMemoryFileSystemProvider()))); - manager = new FileWorkingCopyManager( + manager = disposables.add(new FileWorkingCopyManager( 'testUntitledFileWorkingCopyType', new TestStoredFileWorkingCopyModelFactory(), new TestUntitledFileWorkingCopyModelFactory(), @@ -40,29 +40,32 @@ suite('UntitledFileWorkingCopyManager', () => { accessor.filesConfigurationService, accessor.workingCopyService, accessor.notificationService, accessor.workingCopyEditorService, accessor.editorService, accessor.elevatedFileService, accessor.pathService, accessor.environmentService, accessor.dialogService, accessor.decorationsService - ); + )); }); teardown(() => { - manager.dispose(); - disposables.dispose(); + for (const workingCopy of [...manager.untitled.workingCopies, ...manager.stored.workingCopies]) { + workingCopy.dispose(); + } + + disposables.clear(); }); test('basics', async () => { let createCounter = 0; - manager.untitled.onDidCreate(e => { + disposables.add(manager.untitled.onDidCreate(e => { createCounter++; - }); + })); let disposeCounter = 0; - manager.untitled.onWillDispose(e => { + disposables.add(manager.untitled.onWillDispose(e => { disposeCounter++; - }); + })); let dirtyCounter = 0; - manager.untitled.onDidChangeDirty(e => { + disposables.add(manager.untitled.onDidChangeDirty(e => { dirtyCounter++; - }); + })); assert.strictEqual(accessor.workingCopyService.workingCopies.length, 0); assert.strictEqual(manager.untitled.workingCopies.length, 0); @@ -124,9 +127,9 @@ suite('UntitledFileWorkingCopyManager', () => { test('dirty - scratchpads are never dirty', async () => { let dirtyCounter = 0; - manager.untitled.onDidChangeDirty(e => { + disposables.add(manager.untitled.onDidChangeDirty(e => { dirtyCounter++; - }); + })); const workingCopy1 = await manager.resolve({ untitledResource: URI.from({ scheme: Schemas.untitled, path: `/myscratchpad` }), @@ -151,9 +154,9 @@ suite('UntitledFileWorkingCopyManager', () => { test('resolve - with initial value', async () => { let dirtyCounter = 0; - manager.untitled.onDidChangeDirty(e => { + disposables.add(manager.untitled.onDidChangeDirty(e => { dirtyCounter++; - }); + })); const workingCopy1 = await manager.untitled.resolve({ contents: { value: bufferToStream(VSBuffer.fromString('Hello World')) } }); @@ -176,9 +179,9 @@ suite('UntitledFileWorkingCopyManager', () => { test('resolve - with initial value but markDirty: false', async () => { let dirtyCounter = 0; - manager.untitled.onDidChangeDirty(e => { + disposables.add(manager.untitled.onDidChangeDirty(e => { dirtyCounter++; - }); + })); const workingCopy = await manager.untitled.resolve({ contents: { value: bufferToStream(VSBuffer.fromString('Hello World')), markModified: false } }); @@ -194,15 +197,15 @@ suite('UntitledFileWorkingCopyManager', () => { const untitled1 = await manager.untitled.resolve(); untitled1.dispose(); - const untitled1Again = await manager.untitled.resolve(); + const untitled1Again = disposables.add(await manager.untitled.resolve()); assert.strictEqual(untitled1.resource.toString(), untitled1Again.resource.toString()); }); test('resolve - existing', async () => { let createCounter = 0; - manager.untitled.onDidCreate(e => { + disposables.add(manager.untitled.onDidCreate(e => { createCounter++; - }); + })); const workingCopy1 = await manager.untitled.resolve(); assert.strictEqual(createCounter, 1); @@ -307,7 +310,7 @@ suite('UntitledFileWorkingCopyManager', () => { test('manager with different types produce different URIs', async () => { try { - manager = new FileWorkingCopyManager( + manager = disposables.add(new FileWorkingCopyManager( 'someOtherUntitledTypeId', new TestStoredFileWorkingCopyModelFactory(), new TestUntitledFileWorkingCopyModelFactory(), @@ -316,10 +319,10 @@ suite('UntitledFileWorkingCopyManager', () => { accessor.filesConfigurationService, accessor.workingCopyService, accessor.notificationService, accessor.workingCopyEditorService, accessor.editorService, accessor.elevatedFileService, accessor.pathService, accessor.environmentService, accessor.dialogService, accessor.decorationsService - ); + )); - const untitled1OriginalType = await manager.untitled.resolve(); - const untitled1OtherType = await manager.untitled.resolve(); + const untitled1OriginalType = disposables.add(await manager.untitled.resolve()); + const untitled1OtherType = disposables.add(await manager.untitled.resolve()); assert.notStrictEqual(untitled1OriginalType.resource.toString(), untitled1OtherType.resource.toString()); } finally { @@ -329,7 +332,7 @@ suite('UntitledFileWorkingCopyManager', () => { test('manager without typeId produces backwards compatible URIs', async () => { try { - manager = new FileWorkingCopyManager( + manager = disposables.add(new FileWorkingCopyManager( NO_TYPE_ID, new TestStoredFileWorkingCopyModelFactory(), new TestUntitledFileWorkingCopyModelFactory(), @@ -338,9 +341,9 @@ suite('UntitledFileWorkingCopyManager', () => { accessor.filesConfigurationService, accessor.workingCopyService, accessor.notificationService, accessor.workingCopyEditorService, accessor.editorService, accessor.elevatedFileService, accessor.pathService, accessor.environmentService, accessor.dialogService, accessor.decorationsService - ); + )); - const result = await manager.untitled.resolve(); + const result = disposables.add(await manager.untitled.resolve()); assert.strictEqual(result.resource.scheme, Schemas.untitled); assert.ok(result.resource.path.length > 0); assert.strictEqual(result.resource.query, ''); @@ -350,4 +353,6 @@ suite('UntitledFileWorkingCopyManager', () => { manager.destroy(); } }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/services/workingCopy/test/browser/untitledScratchpadWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/untitledScratchpadWorkingCopy.test.ts index b3f94c4ab9c..a04a7c4951a 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/untitledScratchpadWorkingCopy.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/untitledScratchpadWorkingCopy.test.ts @@ -11,6 +11,7 @@ import { Schemas } from 'vs/base/common/network'; import { basename } from 'vs/base/common/resources'; import { consumeReadable, consumeStream, isReadable, isReadableStream } from 'vs/base/common/stream'; import { URI } from 'vs/base/common/uri'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IUntitledFileWorkingCopyModelFactory, UntitledFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopy'; import { TestUntitledFileWorkingCopyModel } from 'vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test'; @@ -27,14 +28,14 @@ suite('UntitledScratchpadWorkingCopy', () => { const factory = new TestUntitledFileWorkingCopyModelFactory(); - let disposables: DisposableStore; + const disposables = new DisposableStore(); const resource = URI.from({ scheme: Schemas.untitled, path: 'Untitled-1' }); let instantiationService: IInstantiationService; let accessor: TestServiceAccessor; let workingCopy: UntitledFileWorkingCopy; function createWorkingCopy(uri: URI = resource, hasAssociatedFilePath = false, initialValue = '') { - return new UntitledFileWorkingCopy( + return disposables.add(new UntitledFileWorkingCopy( 'testUntitledWorkingCopyType', uri, basename(uri), @@ -46,20 +47,18 @@ suite('UntitledScratchpadWorkingCopy', () => { accessor.workingCopyService, accessor.workingCopyBackupService, accessor.logService - ); + )); } setup(() => { - disposables = new DisposableStore(); instantiationService = workbenchInstantiationService(undefined, disposables); accessor = instantiationService.createInstance(TestServiceAccessor); - workingCopy = createWorkingCopy(); + workingCopy = disposables.add(createWorkingCopy()); }); teardown(() => { - workingCopy.dispose(); - disposables.dispose(); + disposables.clear(); }); test('registers with working copy service', async () => { @@ -74,14 +73,14 @@ suite('UntitledScratchpadWorkingCopy', () => { assert.strictEqual(workingCopy.isDirty(), false); let changeDirtyCounter = 0; - workingCopy.onDidChangeDirty(() => { + disposables.add(workingCopy.onDidChangeDirty(() => { changeDirtyCounter++; - }); + })); let contentChangeCounter = 0; - workingCopy.onDidChangeContent(() => { + disposables.add(workingCopy.onDidChangeContent(() => { contentChangeCounter++; - }); + })); await workingCopy.resolve(); assert.strictEqual(workingCopy.isResolved(), true); @@ -130,14 +129,14 @@ suite('UntitledScratchpadWorkingCopy', () => { test('revert', async () => { let revertCounter = 0; - workingCopy.onDidRevert(() => { + disposables.add(workingCopy.onDidRevert(() => { revertCounter++; - }); + })); let disposeCounter = 0; - workingCopy.onWillDispose(() => { + disposables.add(workingCopy.onWillDispose(() => { disposeCounter++; - }); + })); await workingCopy.resolve(); @@ -153,9 +152,9 @@ suite('UntitledScratchpadWorkingCopy', () => { test('dispose', async () => { let disposeCounter = 0; - workingCopy.onWillDispose(() => { + disposables.add(workingCopy.onWillDispose(() => { disposeCounter++; - }); + })); await workingCopy.resolve(); workingCopy.dispose(); @@ -198,9 +197,9 @@ suite('UntitledScratchpadWorkingCopy', () => { workingCopy = createWorkingCopy(resource, false, 'Hello Initial'); let contentChangeCounter = 0; - workingCopy.onDidChangeContent(() => { + disposables.add(workingCopy.onDidChangeContent(() => { contentChangeCounter++; - }); + })); assert.strictEqual(workingCopy.isModified(), true); @@ -260,9 +259,9 @@ suite('UntitledScratchpadWorkingCopy', () => { workingCopy = createWorkingCopy(); let contentChangeCounter = 0; - workingCopy.onDidChangeContent(() => { + disposables.add(workingCopy.onDidChangeContent(() => { contentChangeCounter++; - }); + })); await workingCopy.resolve(); @@ -270,4 +269,6 @@ suite('UntitledScratchpadWorkingCopy', () => { assert.strictEqual(workingCopy.model?.contents, 'Hello Backup'); assert.strictEqual(contentChangeCounter, 1); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/services/workingCopy/test/browser/workingCopyBackupTracker.test.ts b/src/vs/workbench/services/workingCopy/test/browser/workingCopyBackupTracker.test.ts index eab01d63a3b..13bdd16574b 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/workingCopyBackupTracker.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/workingCopyBackupTracker.test.ts @@ -11,7 +11,7 @@ import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editor import { EditorService } from 'vs/workbench/services/editor/browser/editorService'; import { IUntitledTextResourceEditorInput } from 'vs/workbench/common/editor'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; -import { toResource } from 'vs/base/test/common/utils'; +import { ensureNoDisposablesAreLeakedInTestSuite, toResource } from 'vs/base/test/common/utils'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { IWorkingCopyBackup } from 'vs/workbench/services/workingCopy/common/workingCopy'; @@ -19,18 +19,16 @@ import { ILogService } from 'vs/platform/log/common/log'; import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; -import { createEditorPart, InMemoryTestWorkingCopyBackupService, registerTestResourceEditor, TestServiceAccessor, toTypedWorkingCopyId, toUntypedWorkingCopyId, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { createEditorPart, InMemoryTestWorkingCopyBackupService, registerTestResourceEditor, TestServiceAccessor, toTypedWorkingCopyId, toUntypedWorkingCopyId, workbenchInstantiationService, workbenchTeardown } from 'vs/workbench/test/browser/workbenchTestServices'; import { TestWorkingCopy } from 'vs/workbench/test/common/workbenchTestServices'; import { CancellationToken } from 'vs/base/common/cancellation'; import { timeout } from 'vs/base/common/async'; import { BrowserWorkingCopyBackupTracker } from 'vs/workbench/services/workingCopy/browser/workingCopyBackupTracker'; -import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { IWorkingCopyEditorHandler, IWorkingCopyEditorService } from 'vs/workbench/services/workingCopy/common/workingCopyEditorService'; import { bufferToReadable, VSBuffer } from 'vs/base/common/buffer'; import { isWindows } from 'vs/base/common/platform'; import { Schemas } from 'vs/base/common/network'; -import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; -import { TestWorkspaceTrustRequestService } from 'vs/workbench/services/workspaces/test/common/testWorkspaceTrustService'; suite('WorkingCopyBackupTracker (browser)', function () { let accessor: TestServiceAccessor; @@ -40,7 +38,9 @@ suite('WorkingCopyBackupTracker (browser)', function () { disposables.add(registerTestResourceEditor()); }); - teardown(() => { + teardown(async () => { + await workbenchTeardown(accessor.instantiationService); + disposables.clear(); }); @@ -85,10 +85,8 @@ suite('WorkingCopyBackupTracker (browser)', function () { } } - async function createTracker(): Promise<{ accessor: TestServiceAccessor; part: EditorPart; tracker: TestWorkingCopyBackupTracker; workingCopyBackupService: InMemoryTestWorkingCopyBackupService; instantiationService: IInstantiationService; cleanup: () => void }> { - const disposables = new DisposableStore(); - - const workingCopyBackupService = new InMemoryTestWorkingCopyBackupService(); + async function createTracker(): Promise<{ accessor: TestServiceAccessor; part: EditorPart; tracker: TestWorkingCopyBackupTracker; workingCopyBackupService: InMemoryTestWorkingCopyBackupService; instantiationService: IInstantiationService }> { + const workingCopyBackupService = disposables.add(new InMemoryTestWorkingCopyBackupService()); const instantiationService = workbenchInstantiationService(undefined, disposables); instantiationService.stub(IWorkingCopyBackupService, workingCopyBackupService); @@ -97,24 +95,21 @@ suite('WorkingCopyBackupTracker (browser)', function () { disposables.add(registerTestResourceEditor()); - instantiationService.stub(IWorkspaceTrustRequestService, new TestWorkspaceTrustRequestService(false)); - - const editorService: EditorService = instantiationService.createInstance(EditorService); + const editorService: EditorService = disposables.add(instantiationService.createInstance(EditorService)); instantiationService.stub(IEditorService, editorService); accessor = instantiationService.createInstance(TestServiceAccessor); const tracker = disposables.add(instantiationService.createInstance(TestWorkingCopyBackupTracker)); - return { accessor, part, tracker, workingCopyBackupService: workingCopyBackupService, instantiationService, cleanup: () => disposables.dispose() }; + return { accessor, part, tracker, workingCopyBackupService: workingCopyBackupService, instantiationService }; } async function untitledBackupTest(untitled: IUntitledTextResourceEditorInput = { resource: undefined }): Promise { - const { accessor, cleanup, workingCopyBackupService } = await createTracker(); + const { accessor, workingCopyBackupService } = await createTracker(); - const untitledTextEditor = (await accessor.editorService.openEditor(untitled))?.input as UntitledTextEditorInput; - - const untitledTextModel = await untitledTextEditor.resolve(); + const untitledTextEditor = disposables.add((await accessor.editorService.openEditor(untitled))?.input as UntitledTextEditorInput); + const untitledTextModel = disposables.add(await untitledTextEditor.resolve()); if (!untitled?.contents) { untitledTextModel.textEditorModel?.setValue('Super Good'); @@ -129,8 +124,6 @@ suite('WorkingCopyBackupTracker (browser)', function () { await workingCopyBackupService.joinDiscardBackup(); assert.strictEqual(workingCopyBackupService.hasBackupSync(untitledTextModel), false); - - cleanup(); } test('Track backups (untitled)', function () { @@ -142,14 +135,14 @@ suite('WorkingCopyBackupTracker (browser)', function () { }); test('Track backups (custom)', async function () { - const { accessor, tracker, cleanup, workingCopyBackupService } = await createTracker(); + const { accessor, tracker, workingCopyBackupService } = await createTracker(); class TestBackupWorkingCopy extends TestWorkingCopy { constructor(resource: URI) { super(resource); - accessor.workingCopyService.registerWorkingCopy(this); + disposables.add(accessor.workingCopyService.registerWorkingCopy(this)); } readonly backupDelay = 10; @@ -162,7 +155,7 @@ suite('WorkingCopyBackupTracker (browser)', function () { } const resource = toResource.call(this, '/path/custom.txt'); - const customWorkingCopy = new TestBackupWorkingCopy(resource); + const customWorkingCopy = disposables.add(new TestBackupWorkingCopy(resource)); // Normal customWorkingCopy.setDirty(true); @@ -188,29 +181,22 @@ suite('WorkingCopyBackupTracker (browser)', function () { assert.strictEqual(tracker.pendingBackupOperationCount, 1); await workingCopyBackupService.joinDiscardBackup(); assert.strictEqual(workingCopyBackupService.hasBackupSync(customWorkingCopy), false); - - customWorkingCopy.dispose(); - cleanup(); }); - async function restoreBackupsInit(): Promise<[TestWorkingCopyBackupTracker, TestServiceAccessor, IDisposable]> { + async function restoreBackupsInit(): Promise<[TestWorkingCopyBackupTracker, TestServiceAccessor]> { const fooFile = URI.file(isWindows ? 'c:\\Foo' : '/Foo'); const barFile = URI.file(isWindows ? 'c:\\Bar' : '/Bar'); const untitledFile1 = URI.from({ scheme: Schemas.untitled, path: 'Untitled-1' }); const untitledFile2 = URI.from({ scheme: Schemas.untitled, path: 'Untitled-2' }); - const disposables = new DisposableStore(); - - const workingCopyBackupService = new InMemoryTestWorkingCopyBackupService(); + const workingCopyBackupService = disposables.add(new InMemoryTestWorkingCopyBackupService()); const instantiationService = workbenchInstantiationService(undefined, disposables); instantiationService.stub(IWorkingCopyBackupService, workingCopyBackupService); const part = await createEditorPart(instantiationService, disposables); instantiationService.stub(IEditorGroupsService, part); - instantiationService.stub(IWorkspaceTrustRequestService, new TestWorkspaceTrustRequestService(false)); - - const editorService: EditorService = instantiationService.createInstance(EditorService); + const editorService: EditorService = disposables.add(instantiationService.createInstance(EditorService)); instantiationService.stub(IEditorService, editorService); accessor = instantiationService.createInstance(TestServiceAccessor); @@ -230,11 +216,11 @@ suite('WorkingCopyBackupTracker (browser)', function () { accessor.lifecycleService.phase = LifecyclePhase.Restored; - return [tracker, accessor, disposables]; + return [tracker, accessor]; } test('Restore backups (basics, some handled)', async function () { - const [tracker, accessor, disposables] = await restoreBackupsInit(); + const [tracker, accessor] = await restoreBackupsInit(); assert.strictEqual(tracker.getUnrestoredBackups().size, 0); @@ -256,7 +242,7 @@ suite('WorkingCopyBackupTracker (browser)', function () { createEditor: workingCopy => { createEditorCounter++; - return accessor.instantiationService.createInstance(TestUntitledTextEditorInput, accessor.untitledTextEditorService.create({ initialValue: 'foo' })); + return disposables.add(accessor.instantiationService.createInstance(TestUntitledTextEditorInput, accessor.untitledTextEditorService.create({ initialValue: 'foo' }))); } }); @@ -272,12 +258,10 @@ suite('WorkingCopyBackupTracker (browser)', function () { assert.ok(editor instanceof TestUntitledTextEditorInput); assert.strictEqual(editor.resolved, true); } - - dispose(disposables); }); test('Restore backups (basics, none handled)', async function () { - const [tracker, accessor, disposables] = await restoreBackupsInit(); + const [tracker, accessor] = await restoreBackupsInit(); await tracker.testRestoreBackups({ handles: workingCopy => false, @@ -287,12 +271,10 @@ suite('WorkingCopyBackupTracker (browser)', function () { assert.strictEqual(accessor.editorService.count, 0); assert.strictEqual(tracker.getUnrestoredBackups().size, 4); - - dispose(disposables); }); test('Restore backups (basics, error case)', async function () { - const [tracker, , disposables] = await restoreBackupsInit(); + const [tracker] = await restoreBackupsInit(); try { await tracker.testRestoreBackups({ @@ -305,12 +287,10 @@ suite('WorkingCopyBackupTracker (browser)', function () { } assert.strictEqual(tracker.getUnrestoredBackups().size, 4); - - dispose(disposables); }); test('Restore backups (multiple handlers)', async function () { - const [tracker, accessor, disposables] = await restoreBackupsInit(); + const [tracker, accessor] = await restoreBackupsInit(); const firstHandler = tracker.testRestoreBackups({ handles: workingCopy => { @@ -320,7 +300,7 @@ suite('WorkingCopyBackupTracker (browser)', function () { return false; }, createEditor: workingCopy => { - return accessor.instantiationService.createInstance(TestUntitledTextEditorInput, accessor.untitledTextEditorService.create({ initialValue: 'foo' })); + return disposables.add(accessor.instantiationService.createInstance(TestUntitledTextEditorInput, accessor.untitledTextEditorService.create({ initialValue: 'foo' }))); } }); @@ -332,7 +312,7 @@ suite('WorkingCopyBackupTracker (browser)', function () { return false; }, createEditor: workingCopy => { - return accessor.instantiationService.createInstance(TestUntitledTextEditorInput, accessor.untitledTextEditorService.create({ initialValue: 'foo' })); + return disposables.add(accessor.instantiationService.createInstance(TestUntitledTextEditorInput, accessor.untitledTextEditorService.create({ initialValue: 'foo' }))); } }); @@ -346,20 +326,18 @@ suite('WorkingCopyBackupTracker (browser)', function () { assert.ok(editor instanceof TestUntitledTextEditorInput); assert.strictEqual(editor.resolved, true); } - - dispose(disposables); }); test('Restore backups (editors already opened)', async function () { - const [tracker, accessor, disposables] = await restoreBackupsInit(); + const [tracker, accessor] = await restoreBackupsInit(); assert.strictEqual(tracker.getUnrestoredBackups().size, 0); let handlesCounter = 0; let isOpenCounter = 0; - const editor1 = accessor.instantiationService.createInstance(TestUntitledTextEditorInput, accessor.untitledTextEditorService.create({ initialValue: 'foo' })); - const editor2 = accessor.instantiationService.createInstance(TestUntitledTextEditorInput, accessor.untitledTextEditorService.create({ initialValue: 'foo' })); + const editor1 = disposables.add(accessor.instantiationService.createInstance(TestUntitledTextEditorInput, accessor.untitledTextEditorService.create({ initialValue: 'foo' }))); + const editor2 = disposables.add(accessor.instantiationService.createInstance(TestUntitledTextEditorInput, accessor.untitledTextEditorService.create({ initialValue: 'foo' }))); await accessor.editorService.openEditors([{ editor: editor1 }, { editor: editor2 }]); @@ -396,7 +374,7 @@ suite('WorkingCopyBackupTracker (browser)', function () { assert.strictEqual(editor.resolved, true); } } - - dispose(disposables); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/services/workingCopy/test/browser/workingCopyEditorService.test.ts b/src/vs/workbench/services/workingCopy/test/browser/workingCopyEditorService.test.ts index 26521dee61a..11bad8713a6 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/workingCopyEditorService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/workingCopyEditorService.test.ts @@ -6,12 +6,11 @@ import * as assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; -import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { EditorService } from 'vs/workbench/services/editor/browser/editorService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { IWorkingCopyEditorHandler, WorkingCopyEditorService } from 'vs/workbench/services/workingCopy/common/workingCopyEditorService'; -import { TestWorkspaceTrustRequestService } from 'vs/workbench/services/workspaces/test/common/testWorkspaceTrustService'; import { createEditorPart, registerTestResourceEditor, TestEditorService, TestServiceAccessor, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; import { TestWorkingCopy } from 'vs/workbench/test/common/workbenchTestServices'; @@ -28,12 +27,12 @@ suite('WorkingCopyEditorService', () => { }); test('registry - basics', () => { - const service = new WorkingCopyEditorService(new TestEditorService()); + const service = disposables.add(new WorkingCopyEditorService(new TestEditorService())); let handlerEvent: IWorkingCopyEditorHandler | undefined = undefined; - service.onDidRegisterHandler(handler => { + disposables.add(service.onDidRegisterHandler(handler => { handlerEvent = handler; - }); + })); const editorHandler: IWorkingCopyEditorHandler = { handles: workingCopy => false, @@ -41,11 +40,9 @@ suite('WorkingCopyEditorService', () => { createEditor: workingCopy => { throw new Error(); } }; - const disposable = service.registerHandler(editorHandler); + disposables.add(service.registerHandler(editorHandler)); assert.strictEqual(handlerEvent, editorHandler); - - disposable.dispose(); }); test('findEditor', async () => { @@ -55,14 +52,13 @@ suite('WorkingCopyEditorService', () => { const part = await createEditorPart(instantiationService, disposables); instantiationService.stub(IEditorGroupsService, part); - instantiationService.stub(IWorkspaceTrustRequestService, new TestWorkspaceTrustRequestService(false)); - const editorService = instantiationService.createInstance(EditorService); + const editorService = disposables.add(instantiationService.createInstance(EditorService)); const accessor = instantiationService.createInstance(TestServiceAccessor); - const service = new WorkingCopyEditorService(editorService); + const service = disposables.add(new WorkingCopyEditorService(editorService)); const resource = URI.parse('custom://some/folder/custom.txt'); - const testWorkingCopy = new TestWorkingCopy(resource, false, 'testWorkingCopyTypeId1'); + const testWorkingCopy = disposables.add(new TestWorkingCopy(resource, false, 'testWorkingCopyTypeId1')); assert.strictEqual(service.findEditor(testWorkingCopy), undefined); @@ -74,8 +70,8 @@ suite('WorkingCopyEditorService', () => { disposables.add(service.registerHandler(editorHandler)); - const editor1 = instantiationService.createInstance(UntitledTextEditorInput, accessor.untitledTextEditorService.create({ initialValue: 'foo' })); - const editor2 = instantiationService.createInstance(UntitledTextEditorInput, accessor.untitledTextEditorService.create({ initialValue: 'foo' })); + const editor1 = disposables.add(instantiationService.createInstance(UntitledTextEditorInput, accessor.untitledTextEditorService.create({ initialValue: 'foo' }))); + const editor2 = disposables.add(instantiationService.createInstance(UntitledTextEditorInput, accessor.untitledTextEditorService.create({ initialValue: 'foo' }))); await editorService.openEditors([{ editor: editor1 }, { editor: editor2 }]); @@ -83,4 +79,6 @@ suite('WorkingCopyEditorService', () => { disposables.dispose(); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/services/workingCopy/test/browser/workingCopyFileService.test.ts b/src/vs/workbench/services/workingCopy/test/browser/workingCopyFileService.test.ts index ab966104874..e63425a837c 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/workingCopyFileService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/workingCopyFileService.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { toResource } from 'vs/base/test/common/utils'; +import { ensureNoDisposablesAreLeakedInTestSuite, toResource } from 'vs/base/test/common/utils'; import { workbenchInstantiationService, TestServiceAccessor, ITestTextFileEditorModelManager } from 'vs/workbench/test/browser/workbenchTestServices'; import { URI } from 'vs/base/common/uri'; import { FileOperation } from 'vs/platform/files/common/files'; @@ -20,19 +20,18 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; suite('WorkingCopyFileService', () => { - let disposables: DisposableStore; + const disposables = new DisposableStore(); let instantiationService: IInstantiationService; let accessor: TestServiceAccessor; setup(() => { - disposables = new DisposableStore(); instantiationService = workbenchInstantiationService(undefined, disposables); accessor = instantiationService.createInstance(TestServiceAccessor); + disposables.add(accessor.textFileService.files); }); teardown(() => { - (accessor.textFileService.files).dispose(); - disposables.dispose(); + disposables.clear(); }); test('create - dirty file', async function () { @@ -172,12 +171,12 @@ suite('WorkingCopyFileService', () => { }); test('registerWorkingCopyProvider', async function () { - const model1 = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file-1.txt'), 'utf8', undefined); + const model1: TextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file-1.txt'), 'utf8', undefined)); (accessor.textFileService.files).add(model1.resource, model1); await model1.resolve(); model1.textEditorModel!.setValue('foo'); - const testWorkingCopy = new TestWorkingCopy(toResource.call(this, '/path/file-2.txt'), true); + const testWorkingCopy: TestWorkingCopy = disposables.add(new TestWorkingCopy(toResource.call(this, '/path/file-2.txt'), true)); const registration = accessor.workingCopyFileService.registerWorkingCopyProvider(() => { return [model1, testWorkingCopy]; }); @@ -192,8 +191,6 @@ suite('WorkingCopyFileService', () => { dirty = accessor.workingCopyFileService.getDirty(model1.resource); assert.strictEqual(dirty.length, 1, 'Should have unregistered our provider'); assert.strictEqual(dirty[0], model1); - - model1.dispose(); }); test('createFolder', async function () { @@ -202,7 +199,7 @@ suite('WorkingCopyFileService', () => { const resource = toResource.call(this, '/path/folder'); - const participant = accessor.workingCopyFileService.addFileOperationParticipant({ + disposables.add(accessor.workingCopyFileService.addFileOperationParticipant({ participate: async (files, operation) => { assert.strictEqual(files.length, 1); const file = files[0]; @@ -210,45 +207,41 @@ suite('WorkingCopyFileService', () => { assert.strictEqual(operation, FileOperation.CREATE); eventCounter++; } - }); + })); - const listener1 = accessor.workingCopyFileService.onWillRunWorkingCopyFileOperation(e => { + disposables.add(accessor.workingCopyFileService.onWillRunWorkingCopyFileOperation(e => { assert.strictEqual(e.files.length, 1); const file = e.files[0]; assert.strictEqual(file.target.toString(), resource.toString()); assert.strictEqual(e.operation, FileOperation.CREATE); correlationId = e.correlationId; eventCounter++; - }); + })); - const listener2 = accessor.workingCopyFileService.onDidRunWorkingCopyFileOperation(e => { + disposables.add(accessor.workingCopyFileService.onDidRunWorkingCopyFileOperation(e => { assert.strictEqual(e.files.length, 1); const file = e.files[0]; assert.strictEqual(file.target.toString(), resource.toString()); assert.strictEqual(e.operation, FileOperation.CREATE); assert.strictEqual(e.correlationId, correlationId); eventCounter++; - }); + })); await accessor.workingCopyFileService.createFolder([{ resource }], CancellationToken.None); assert.strictEqual(eventCounter, 3); - - participant.dispose(); - listener1.dispose(); - listener2.dispose(); }); test('cancellation of participants', async function () { const resource = toResource.call(this, '/path/folder'); let canceled = false; - const participant = accessor.workingCopyFileService.addFileOperationParticipant({ + disposables.add(accessor.workingCopyFileService.addFileOperationParticipant({ participate: async (files, operation, info, t, token) => { await timeout(0); canceled = token.isCancellationRequested; } - }); + })); // Create let cts = new CancellationTokenSource(); @@ -289,8 +282,6 @@ suite('WorkingCopyFileService', () => { await promise; assert.strictEqual(canceled, true); canceled = false; - - participant.dispose(); }); async function testEventsMoveOrCopy(files: ICopyOperation[], move?: boolean): Promise { @@ -491,7 +482,7 @@ suite('WorkingCopyFileService', () => { let eventCounter = 0; let correlationId: number | undefined = undefined; - const participant = accessor.workingCopyFileService.addFileOperationParticipant({ + disposables.add(accessor.workingCopyFileService.addFileOperationParticipant({ participate: async (files, operation) => { assert.strictEqual(files.length, 1); const file = files[0]; @@ -499,34 +490,32 @@ suite('WorkingCopyFileService', () => { assert.strictEqual(operation, FileOperation.CREATE); eventCounter++; } - }); + })); - const listener1 = accessor.workingCopyFileService.onWillRunWorkingCopyFileOperation(e => { + disposables.add(accessor.workingCopyFileService.onWillRunWorkingCopyFileOperation(e => { assert.strictEqual(e.files.length, 1); const file = e.files[0]; assert.strictEqual(file.target.toString(), model.resource.toString()); assert.strictEqual(e.operation, FileOperation.CREATE); correlationId = e.correlationId; eventCounter++; - }); + })); - const listener2 = accessor.workingCopyFileService.onDidRunWorkingCopyFileOperation(e => { + disposables.add(accessor.workingCopyFileService.onDidRunWorkingCopyFileOperation(e => { assert.strictEqual(e.files.length, 1); const file = e.files[0]; assert.strictEqual(file.target.toString(), model.resource.toString()); assert.strictEqual(e.operation, FileOperation.CREATE); assert.strictEqual(e.correlationId, correlationId); eventCounter++; - }); + })); await accessor.workingCopyFileService.create([{ resource, contents }], CancellationToken.None); assert.ok(!accessor.workingCopyService.isDirty(model.resource)); model.dispose(); assert.strictEqual(eventCounter, 3); - - participant.dispose(); - listener1.dispose(); - listener2.dispose(); } + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts b/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts index 81f856c6d9d..384e3b17955 100644 --- a/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts @@ -8,26 +8,34 @@ import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCo import { URI } from 'vs/base/common/uri'; import { TestWorkingCopy } from 'vs/workbench/test/common/workbenchTestServices'; import { IWorkingCopySaveEvent, WorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { DisposableStore } from 'vs/base/common/lifecycle'; suite('WorkingCopyService', () => { + const disposables = new DisposableStore(); + + teardown(() => { + disposables.clear(); + }); + test('registry - basics', () => { - const service = new WorkingCopyService(); + const service = disposables.add(new WorkingCopyService()); const onDidChangeDirty: IWorkingCopy[] = []; - service.onDidChangeDirty(copy => onDidChangeDirty.push(copy)); + disposables.add(service.onDidChangeDirty(copy => onDidChangeDirty.push(copy))); const onDidChangeContent: IWorkingCopy[] = []; - service.onDidChangeContent(copy => onDidChangeContent.push(copy)); + disposables.add(service.onDidChangeContent(copy => onDidChangeContent.push(copy))); const onDidSave: IWorkingCopySaveEvent[] = []; - service.onDidSave(copy => onDidSave.push(copy)); + disposables.add(service.onDidSave(copy => onDidSave.push(copy))); const onDidRegister: IWorkingCopy[] = []; - service.onDidRegister(copy => onDidRegister.push(copy)); + disposables.add(service.onDidRegister(copy => onDidRegister.push(copy))); const onDidUnregister: IWorkingCopy[] = []; - service.onDidUnregister(copy => onDidUnregister.push(copy)); + disposables.add(service.onDidUnregister(copy => onDidUnregister.push(copy))); assert.strictEqual(service.hasDirty, false); assert.strictEqual(service.dirtyCount, 0); @@ -40,7 +48,7 @@ suite('WorkingCopyService', () => { assert.strictEqual(service.has({ resource: resource1, typeId: 'testWorkingCopyType' }), false); assert.strictEqual(service.get({ resource: resource1, typeId: 'testWorkingCopyType' }), undefined); assert.strictEqual(service.getAll(resource1), undefined); - const copy1 = new TestWorkingCopy(resource1); + const copy1 = disposables.add(new TestWorkingCopy(resource1)); const unregister1 = service.registerWorkingCopy(copy1); assert.strictEqual(service.workingCopies.length, 1); @@ -100,7 +108,7 @@ suite('WorkingCopyService', () => { // resource 2 const resource2 = URI.file('/some/folder/file-dirty.txt'); - const copy2 = new TestWorkingCopy(resource2, true); + const copy2 = disposables.add(new TestWorkingCopy(resource2, true)); const unregister2 = service.registerWorkingCopy(copy2); assert.strictEqual(onDidRegister.length, 2); @@ -128,33 +136,33 @@ suite('WorkingCopyService', () => { }); test('registry - multiple copies on same resource throws (same type ID)', () => { - const service = new WorkingCopyService(); + const service = disposables.add(new WorkingCopyService()); const resource = URI.parse('custom://some/folder/custom.txt'); - const copy1 = new TestWorkingCopy(resource); - service.registerWorkingCopy(copy1); + const copy1 = disposables.add(new TestWorkingCopy(resource)); + disposables.add(service.registerWorkingCopy(copy1)); - const copy2 = new TestWorkingCopy(resource); + const copy2 = disposables.add(new TestWorkingCopy(resource)); assert.throws(() => service.registerWorkingCopy(copy2)); }); test('registry - multiple copies on same resource is supported (different type ID)', () => { - const service = new WorkingCopyService(); + const service = disposables.add(new WorkingCopyService()); const resource = URI.parse('custom://some/folder/custom.txt'); const typeId1 = 'testWorkingCopyTypeId1'; - let copy1 = new TestWorkingCopy(resource, false, typeId1); + let copy1 = disposables.add(new TestWorkingCopy(resource, false, typeId1)); let dispose1 = service.registerWorkingCopy(copy1); const typeId2 = 'testWorkingCopyTypeId2'; - const copy2 = new TestWorkingCopy(resource, false, typeId2); + const copy2 = disposables.add(new TestWorkingCopy(resource, false, typeId2)); const dispose2 = service.registerWorkingCopy(copy2); const typeId3 = 'testWorkingCopyTypeId3'; - const copy3 = new TestWorkingCopy(resource, false, typeId3); + const copy3 = disposables.add(new TestWorkingCopy(resource, false, typeId3)); const dispose3 = service.registerWorkingCopy(copy3); const copies = service.getAll(resource); @@ -196,7 +204,7 @@ suite('WorkingCopyService', () => { assert.strictEqual(service.isDirty(resource, typeId3), false); dispose1.dispose(); - copy1 = new TestWorkingCopy(resource, false, typeId1); + copy1 = disposables.add(new TestWorkingCopy(resource, false, typeId1)); dispose1 = service.registerWorkingCopy(copy1); dispose1.dispose(); @@ -205,4 +213,6 @@ suite('WorkingCopyService', () => { assert.strictEqual(service.workingCopies.length, 0); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupService.test.ts b/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupService.test.ts index 8c381b4318b..92da56329ee 100644 --- a/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupService.test.ts @@ -30,6 +30,8 @@ import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFil import { generateUuid } from 'vs/base/common/uuid'; import { INativeWindowConfiguration } from 'vs/platform/window/common/window'; import product from 'vs/platform/product/common/product'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { DisposableStore } from 'vs/base/common/lifecycle'; const homeDir = URI.file('home').with({ scheme: Schemas.inMemory }); const tmpDir = URI.file('tmp').with({ scheme: Schemas.inMemory }); @@ -170,6 +172,8 @@ suite('WorkingCopyBackupService', () => { let service: NodeTestWorkingCopyBackupService; let fileService: IFileService; + const disposables = new DisposableStore(); + const workspaceResource = URI.file(isWindows ? 'c:\\workspace' : '/workspace'); const fooFile = URI.file(isWindows ? 'c:\\Foo' : '/Foo'); const customFile = URI.parse('customScheme://some/path'); @@ -184,7 +188,7 @@ suite('WorkingCopyBackupService', () => { workspacesJsonPath = joinPath(backupHome, 'workspaces.json'); workspaceBackupPath = joinPath(backupHome, hash(workspaceResource.fsPath).toString(16)); - service = new NodeTestWorkingCopyBackupService(testDir, workspaceBackupPath); + service = disposables.add(new NodeTestWorkingCopyBackupService(testDir, workspaceBackupPath)); fileService = service._fileService; await fileService.createFolder(backupHome); @@ -192,6 +196,10 @@ suite('WorkingCopyBackupService', () => { return fileService.writeFile(workspacesJsonPath, VSBuffer.fromString('')); }); + teardown(() => { + disposables.clear(); + }); + suite('hashIdentifier', () => { test('should correctly hash the identifier for untitled scheme URIs', () => { const uri = URI.from({ scheme: Schemas.untitled, path: 'Untitled-1' }); @@ -1296,4 +1304,6 @@ suite('WorkingCopyBackupService', () => { assert.ok(backups.every(backup => backup.typeId === '')); }); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupTracker.test.ts b/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupTracker.test.ts index 8b53c58a362..9d17566ae2d 100644 --- a/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupTracker.test.ts +++ b/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupTracker.test.ts @@ -16,7 +16,7 @@ import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editor import { EditorService } from 'vs/workbench/services/editor/browser/editorService'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { DisposableStore } from 'vs/base/common/lifecycle'; -import { toResource } from 'vs/base/test/common/utils'; +import { ensureNoDisposablesAreLeakedInTestSuite, toResource } from 'vs/base/test/common/utils'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { ILogService } from 'vs/platform/log/common/log'; @@ -28,7 +28,7 @@ import { INativeHostService } from 'vs/platform/native/common/native'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { createEditorPart, registerTestFileEditor, TestBeforeShutdownEvent, TestEnvironmentService, TestFilesConfigurationService, TestFileService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { createEditorPart, registerTestFileEditor, TestBeforeShutdownEvent, TestEnvironmentService, TestFilesConfigurationService, TestFileService, workbenchTeardown } from 'vs/workbench/test/browser/workbenchTestServices'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -82,8 +82,9 @@ suite('WorkingCopyBackupTracker (native)', function () { override dispose() { super.dispose(); - for (const [_, disposable] of this.pendingBackupOperations) { - disposable.dispose(); + for (const [_, pending] of this.pendingBackupOperations) { + pending.cancel(); + pending.disposable.dispose(); } } @@ -113,11 +114,10 @@ suite('WorkingCopyBackupTracker (native)', function () { let workspaceBackupPath: URI; let accessor: TestServiceAccessor; - let disposables: DisposableStore; + + const disposables = new DisposableStore(); setup(async () => { - disposables = new DisposableStore(); - testDir = URI.file(join(generateUuid(), 'vsctests', 'workingcopybackuptracker')).with({ scheme: Schemas.inMemory }); backupHome = joinPath(testDir, 'Backups'); const workspacesJsonPath = joinPath(backupHome, 'workspaces.json'); @@ -137,8 +137,8 @@ suite('WorkingCopyBackupTracker (native)', function () { return accessor.fileService.writeFile(workspacesJsonPath, VSBuffer.fromString('')); }); - teardown(async () => { - disposables.dispose(); + teardown(() => { + disposables.clear(); }); async function createTracker(autoSaveEnabled = false): Promise<{ accessor: TestServiceAccessor; part: EditorPart; tracker: TestWorkingCopyBackupTracker; instantiationService: IInstantiationService; cleanup: () => Promise }> { @@ -150,19 +150,19 @@ suite('WorkingCopyBackupTracker (native)', function () { } instantiationService.stub(IConfigurationService, configurationService); - instantiationService.stub(IFilesConfigurationService, new TestFilesConfigurationService( + instantiationService.stub(IFilesConfigurationService, disposables.add(new TestFilesConfigurationService( instantiationService.createInstance(MockContextKeyService), configurationService, new TestContextService(TestWorkspace), TestEnvironmentService, - new UriIdentityService(new TestFileService()), - new TestFileService() - )); + disposables.add(new UriIdentityService(disposables.add(new TestFileService()))), + disposables.add(new TestFileService()) + ))); const part = await createEditorPart(instantiationService, disposables); instantiationService.stub(IEditorGroupsService, part); - const editorService: EditorService = instantiationService.createInstance(EditorService); + const editorService: EditorService = disposables.add(instantiationService.createInstance(EditorService)); instantiationService.stub(IEditorService, editorService); accessor = instantiationService.createInstance(TestServiceAccessor); @@ -170,8 +170,9 @@ suite('WorkingCopyBackupTracker (native)', function () { const tracker = instantiationService.createInstance(TestWorkingCopyBackupTracker); const cleanup = async () => { - // File changes could also schedule some backup operations so we need to wait for them before finishing the test - await accessor.workingCopyBackupService.waitForAllBackups(); + await accessor.workingCopyBackupService.waitForAllBackups(); // File changes could also schedule some backup operations so we need to wait for them before finishing the test + + await workbenchTeardown(instantiationService); part.dispose(); tracker.dispose(); @@ -356,7 +357,7 @@ suite('WorkingCopyBackupTracker (native)', function () { constructor(resource: URI) { super(resource); - accessor.workingCopyService.registerWorkingCopy(this); + this._register(accessor.workingCopyService.registerWorkingCopy(this)); } override async backup(token: CancellationToken): Promise { @@ -365,7 +366,7 @@ suite('WorkingCopyBackupTracker (native)', function () { } const resource = toResource.call(this, '/path/custom.txt'); - const customWorkingCopy = new TestBackupWorkingCopy(resource); + const customWorkingCopy = disposables.add(new TestBackupWorkingCopy(resource)); customWorkingCopy.setDirty(true); const event = new TestBeforeShutdownEvent(); @@ -389,7 +390,7 @@ suite('WorkingCopyBackupTracker (native)', function () { constructor(resource: URI) { super(resource); - accessor.workingCopyService.registerWorkingCopy(this); + this._register(accessor.workingCopyService.registerWorkingCopy(this)); } override capabilities = WorkingCopyCapabilities.Untitled | WorkingCopyCapabilities.Scratchpad; @@ -408,7 +409,7 @@ suite('WorkingCopyBackupTracker (native)', function () { } const resource = toResource.call(this, '/path/custom.txt'); - new TestBackupWorkingCopy(resource); + disposables.add(new TestBackupWorkingCopy(resource)); const event = new TestBeforeShutdownEvent(); event.reason = ShutdownReason.QUIT; @@ -716,7 +717,7 @@ suite('WorkingCopyBackupTracker (native)', function () { constructor(resource: URI) { super(resource); - accessor.workingCopyService.registerWorkingCopy(this); + this._register(accessor.workingCopyService.registerWorkingCopy(this)); } override capabilities = WorkingCopyCapabilities.Untitled | WorkingCopyCapabilities.Scratchpad; @@ -747,7 +748,7 @@ suite('WorkingCopyBackupTracker (native)', function () { accessor.fileDialogService.setConfirmResult(ConfirmResult.CANCEL); const resource = toResource.call(this, '/path/custom.txt'); - new TestBackupWorkingCopy(resource); + disposables.add(new TestBackupWorkingCopy(resource)); const event = new TestBeforeShutdownEvent(); event.reason = shutdownReason; @@ -761,4 +762,6 @@ suite('WorkingCopyBackupTracker (native)', function () { await cleanup(); } }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyHistoryService.test.ts b/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyHistoryService.test.ts index 9d9094116bc..ff4f48a4af9 100644 --- a/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyHistoryService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyHistoryService.test.ts @@ -23,6 +23,8 @@ import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFil import { generateUuid } from 'vs/base/common/uuid'; import { join } from 'vs/base/common/path'; import { VSBuffer } from 'vs/base/common/buffer'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { DisposableStore } from 'vs/base/common/lifecycle'; export class TestWorkingCopyHistoryService extends NativeWorkingCopyHistoryService { @@ -30,24 +32,20 @@ export class TestWorkingCopyHistoryService extends NativeWorkingCopyHistoryServi readonly _configurationService: TestConfigurationService; readonly _lifecycleService: TestLifecycleService; - constructor(fileService?: IFileService) { + constructor(disposables: DisposableStore, fileService?: IFileService) { const environmentService = TestEnvironmentService; const logService = new NullLogService(); if (!fileService) { - fileService = new FileService(logService); - fileService.registerProvider(Schemas.inMemory, new InMemoryFileSystemProvider()); - fileService.registerProvider(Schemas.vscodeUserData, new InMemoryFileSystemProvider()); + fileService = disposables.add(new FileService(logService)); + disposables.add(fileService.registerProvider(Schemas.inMemory, disposables.add(new InMemoryFileSystemProvider()))); + disposables.add(fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new InMemoryFileSystemProvider()))); } const remoteAgentService = new TestRemoteAgentService(); - - const uriIdentityService = new UriIdentityService(fileService); - - const labelService = new LabelService(environmentService, new TestContextService(), new TestPathService(), new TestRemoteAgentService(), new TestStorageService(), new TestLifecycleService()); - - const lifecycleService = new TestLifecycleService(); - + const uriIdentityService = disposables.add(new UriIdentityService(fileService)); + const lifecycleService = disposables.add(new TestLifecycleService()); + const labelService = disposables.add(new LabelService(environmentService, new TestContextService(), new TestPathService(), new TestRemoteAgentService(), disposables.add(new TestStorageService()), lifecycleService)); const configurationService = new TestConfigurationService(); super(fileService, remoteAgentService, environmentService, uriIdentityService, labelService, lifecycleService, logService, configurationService); @@ -60,6 +58,8 @@ export class TestWorkingCopyHistoryService extends NativeWorkingCopyHistoryServi suite('WorkingCopyHistoryService', () => { + const disposables = new DisposableStore(); + let testDir: URI; let historyHome: URI; let workHome: URI; @@ -84,7 +84,7 @@ suite('WorkingCopyHistoryService', () => { historyHome = joinPath(testDir, 'User', 'History'); workHome = joinPath(testDir, 'work'); - service = new TestWorkingCopyHistoryService(); + service = disposables.add(new TestWorkingCopyHistoryService(disposables)); fileService = service._fileService; await fileService.createFolder(historyHome); @@ -118,15 +118,15 @@ suite('WorkingCopyHistoryService', () => { } teardown(() => { - service.dispose(); + disposables.clear(); }); test('addEntry', async () => { const addEvents: IWorkingCopyHistoryEvent[] = []; - service.onDidAddEntry(e => addEvents.push(e)); + disposables.add(service.onDidAddEntry(e => addEvents.push(e))); - const workingCopy1 = new TestWorkingCopy(testFile1Path); - const workingCopy2 = new TestWorkingCopy(testFile2Path); + const workingCopy1 = disposables.add(new TestWorkingCopy(testFile1Path)); + const workingCopy2 = disposables.add(new TestWorkingCopy(testFile2Path)); // Add Entry works @@ -164,7 +164,7 @@ suite('WorkingCopyHistoryService', () => { // Invalid working copies are ignored - const workingCopy3 = new TestWorkingCopy(testFile2Path.with({ scheme: 'unsupported' })); + const workingCopy3 = disposables.add(new TestWorkingCopy(testFile2Path.with({ scheme: 'unsupported' }))); const entry3A = await addEntry({ resource: workingCopy3.resource }, CancellationToken.None, false); assert.ok(!entry3A); @@ -173,9 +173,9 @@ suite('WorkingCopyHistoryService', () => { test('renameEntry', async () => { const changeEvents: IWorkingCopyHistoryEvent[] = []; - service.onDidChangeEntry(e => changeEvents.push(e)); + disposables.add(service.onDidChangeEntry(e => changeEvents.push(e))); - const workingCopy1 = new TestWorkingCopy(testFile1Path); + const workingCopy1 = disposables.add(new TestWorkingCopy(testFile1Path)); const entry = await addEntry({ resource: workingCopy1.resource }, CancellationToken.None); await addEntry({ resource: workingCopy1.resource }, CancellationToken.None); @@ -200,7 +200,7 @@ suite('WorkingCopyHistoryService', () => { // Resolve from file service fresh and verify again service.dispose(); - service = new TestWorkingCopyHistoryService(fileService); + service = disposables.add(new TestWorkingCopyHistoryService(disposables, fileService)); entries = await service.getEntries(workingCopy1.resource, CancellationToken.None); assert.strictEqual(entries.length, 3); @@ -209,9 +209,9 @@ suite('WorkingCopyHistoryService', () => { test('removeEntry', async () => { const removeEvents: IWorkingCopyHistoryEvent[] = []; - service.onDidRemoveEntry(e => removeEvents.push(e)); + disposables.add(service.onDidRemoveEntry(e => removeEvents.push(e))); - const workingCopy1 = new TestWorkingCopy(testFile1Path); + const workingCopy1 = disposables.add(new TestWorkingCopy(testFile1Path)); await addEntry({ resource: workingCopy1.resource }, CancellationToken.None); const entry2 = await addEntry({ resource: workingCopy1.resource }, CancellationToken.None); @@ -242,14 +242,14 @@ suite('WorkingCopyHistoryService', () => { // Resolve from file service fresh and verify again service.dispose(); - service = new TestWorkingCopyHistoryService(fileService); + service = disposables.add(new TestWorkingCopyHistoryService(disposables, fileService)); entries = await service.getEntries(workingCopy1.resource, CancellationToken.None); assert.strictEqual(entries.length, 3); }); test('removeEntry - deletes history entries folder when last entry removed', async () => { - const workingCopy1 = new TestWorkingCopy(testFile1Path); + const workingCopy1 = disposables.add(new TestWorkingCopy(testFile1Path)); let entry = await addEntry({ resource: workingCopy1.resource }, CancellationToken.None); @@ -261,7 +261,7 @@ suite('WorkingCopyHistoryService', () => { // Resolve from file service fresh and verify again service.dispose(); - service = new TestWorkingCopyHistoryService(fileService); + service = disposables.add(new TestWorkingCopyHistoryService(disposables, fileService)); assert.strictEqual((await fileService.exists(dirname(entry.location))), true); @@ -278,17 +278,17 @@ suite('WorkingCopyHistoryService', () => { // Resolve from file service fresh and verify again service.dispose(); - service = new TestWorkingCopyHistoryService(fileService); + service = disposables.add(new TestWorkingCopyHistoryService(disposables, fileService)); assert.strictEqual((await fileService.exists(dirname(entry.location))), false); }); test('removeAll', async () => { let removed = false; - service.onDidRemoveEntries(() => removed = true); + disposables.add(service.onDidRemoveEntries(() => removed = true)); - const workingCopy1 = new TestWorkingCopy(testFile1Path); - const workingCopy2 = new TestWorkingCopy(testFile2Path); + const workingCopy1 = disposables.add(new TestWorkingCopy(testFile1Path)); + const workingCopy2 = disposables.add(new TestWorkingCopy(testFile2Path)); await addEntry({ resource: workingCopy1.resource }, CancellationToken.None); await addEntry({ resource: workingCopy1.resource }, CancellationToken.None); @@ -317,7 +317,7 @@ suite('WorkingCopyHistoryService', () => { // Resolve from file service fresh and verify again service.dispose(); - service = new TestWorkingCopyHistoryService(fileService); + service = disposables.add(new TestWorkingCopyHistoryService(disposables, fileService)); entries = await service.getEntries(workingCopy1.resource, CancellationToken.None); assert.strictEqual(entries.length, 0); @@ -326,8 +326,8 @@ suite('WorkingCopyHistoryService', () => { }); test('getEntries - simple', async () => { - const workingCopy1 = new TestWorkingCopy(testFile1Path); - const workingCopy2 = new TestWorkingCopy(testFile2Path); + const workingCopy1 = disposables.add(new TestWorkingCopy(testFile1Path)); + const workingCopy2 = disposables.add(new TestWorkingCopy(testFile2Path)); let entries = await service.getEntries(workingCopy1.resource, CancellationToken.None); assert.strictEqual(entries.length, 0); @@ -355,8 +355,8 @@ suite('WorkingCopyHistoryService', () => { }); test('getEntries - metadata preserved when stored', async () => { - const workingCopy1 = new TestWorkingCopy(testFile1Path); - const workingCopy2 = new TestWorkingCopy(testFile2Path); + const workingCopy1 = disposables.add(new TestWorkingCopy(testFile1Path)); + const workingCopy2 = disposables.add(new TestWorkingCopy(testFile2Path)); const entry1 = await addEntry({ resource: workingCopy1.resource, source: 'test-source' }, CancellationToken.None); const entry2 = await addEntry({ resource: workingCopy2.resource }, CancellationToken.None); @@ -370,7 +370,7 @@ suite('WorkingCopyHistoryService', () => { // Resolve from file service fresh and verify again service.dispose(); - service = new TestWorkingCopyHistoryService(fileService); + service = disposables.add(new TestWorkingCopyHistoryService(disposables, fileService)); let entries = await service.getEntries(workingCopy1.resource, CancellationToken.None); assert.strictEqual(entries.length, 1); @@ -383,7 +383,7 @@ suite('WorkingCopyHistoryService', () => { }); test('getEntries - corrupt meta.json is no problem', async () => { - const workingCopy1 = new TestWorkingCopy(testFile1Path); + const workingCopy1 = disposables.add(new TestWorkingCopy(testFile1Path)); const entry1 = await addEntry({ resource: workingCopy1.resource }, CancellationToken.None); @@ -395,7 +395,7 @@ suite('WorkingCopyHistoryService', () => { // Resolve from file service fresh and verify again service.dispose(); - service = new TestWorkingCopyHistoryService(fileService); + service = disposables.add(new TestWorkingCopyHistoryService(disposables, fileService)); const metaFile = joinPath(dirname(entry1.location), 'entries.json'); assert.ok((await fileService.exists(metaFile))); @@ -407,7 +407,7 @@ suite('WorkingCopyHistoryService', () => { }); test('getEntries - missing entries from meta.json is no problem', async () => { - const workingCopy1 = new TestWorkingCopy(testFile1Path); + const workingCopy1 = disposables.add(new TestWorkingCopy(testFile1Path)); const entry1 = await addEntry({ resource: workingCopy1.resource }, CancellationToken.None); const entry2 = await addEntry({ resource: workingCopy1.resource }, CancellationToken.None); @@ -420,7 +420,7 @@ suite('WorkingCopyHistoryService', () => { // Resolve from file service fresh and verify again service.dispose(); - service = new TestWorkingCopyHistoryService(fileService); + service = disposables.add(new TestWorkingCopyHistoryService(disposables, fileService)); await fileService.del(entry1.location); @@ -430,7 +430,7 @@ suite('WorkingCopyHistoryService', () => { }); test('getEntries - in-memory and on-disk entries are merged', async () => { - const workingCopy1 = new TestWorkingCopy(testFile1Path); + const workingCopy1 = disposables.add(new TestWorkingCopy(testFile1Path)); const entry1 = await addEntry({ resource: workingCopy1.resource, source: 'test-source' }, CancellationToken.None); const entry2 = await addEntry({ resource: workingCopy1.resource, source: 'other-source' }, CancellationToken.None); @@ -443,7 +443,7 @@ suite('WorkingCopyHistoryService', () => { // Resolve from file service fresh and verify again service.dispose(); - service = new TestWorkingCopyHistoryService(fileService); + service = disposables.add(new TestWorkingCopyHistoryService(disposables, fileService)); const entry3 = await addEntry({ resource: workingCopy1.resource, source: 'test-source' }, CancellationToken.None); const entry4 = await addEntry({ resource: workingCopy1.resource, source: 'other-source' }, CancellationToken.None); @@ -457,7 +457,7 @@ suite('WorkingCopyHistoryService', () => { }); test('getEntries - configured max entries respected', async () => { - const workingCopy1 = new TestWorkingCopy(testFile1Path); + const workingCopy1 = disposables.add(new TestWorkingCopy(testFile1Path)); await addEntry({ resource: workingCopy1.resource }, CancellationToken.None); await addEntry({ resource: workingCopy1.resource }, CancellationToken.None); @@ -483,8 +483,8 @@ suite('WorkingCopyHistoryService', () => { }); test('getAll', async () => { - const workingCopy1 = new TestWorkingCopy(testFile1Path); - const workingCopy2 = new TestWorkingCopy(testFile2Path); + const workingCopy1 = disposables.add(new TestWorkingCopy(testFile1Path)); + const workingCopy2 = disposables.add(new TestWorkingCopy(testFile2Path)); let resources = await service.getAll(CancellationToken.None); assert.strictEqual(resources.length, 0); @@ -510,9 +510,9 @@ suite('WorkingCopyHistoryService', () => { // Resolve from file service fresh and verify again service.dispose(); - service = new TestWorkingCopyHistoryService(fileService); + service = disposables.add(new TestWorkingCopyHistoryService(disposables, fileService)); - const workingCopy3 = new TestWorkingCopy(testFile3Path); + const workingCopy3 = disposables.add(new TestWorkingCopy(testFile3Path)); await addEntry({ resource: workingCopy3.resource, source: 'test-source' }, CancellationToken.None); resources = await service.getAll(CancellationToken.None); @@ -525,7 +525,7 @@ suite('WorkingCopyHistoryService', () => { }); test('getAll - ignores resource when no entries exist', async () => { - const workingCopy1 = new TestWorkingCopy(testFile1Path); + const workingCopy1 = disposables.add(new TestWorkingCopy(testFile1Path)); const entry = await addEntry({ resource: workingCopy1.resource, source: 'test-source' }, CancellationToken.None); @@ -545,7 +545,7 @@ suite('WorkingCopyHistoryService', () => { // Resolve from file service fresh and verify again service.dispose(); - service = new TestWorkingCopyHistoryService(fileService); + service = disposables.add(new TestWorkingCopyHistoryService(disposables, fileService)); resources = await service.getAll(CancellationToken.None); assert.strictEqual(resources.length, 0); @@ -563,7 +563,7 @@ suite('WorkingCopyHistoryService', () => { } test('entries cleaned up on shutdown', async () => { - const workingCopy1 = new TestWorkingCopy(testFile1Path); + const workingCopy1 = disposables.add(new TestWorkingCopy(testFile1Path)); const entry1 = await addEntry({ resource: workingCopy1.resource, source: 'test-source' }, CancellationToken.None); const entry2 = await addEntry({ resource: workingCopy1.resource, source: 'other-source' }, CancellationToken.None); @@ -585,7 +585,7 @@ suite('WorkingCopyHistoryService', () => { // Resolve from file service fresh and verify again service.dispose(); - service = new TestWorkingCopyHistoryService(fileService); + service = disposables.add(new TestWorkingCopyHistoryService(disposables, fileService)); let entries = await service.getEntries(workingCopy1.resource, CancellationToken.None); assert.strictEqual(entries.length, 2); @@ -608,7 +608,7 @@ suite('WorkingCopyHistoryService', () => { // Resolve from file service fresh and verify again service.dispose(); - service = new TestWorkingCopyHistoryService(fileService); + service = disposables.add(new TestWorkingCopyHistoryService(disposables, fileService)); entries = await service.getEntries(workingCopy1.resource, CancellationToken.None); assert.strictEqual(entries.length, 3); @@ -619,9 +619,9 @@ suite('WorkingCopyHistoryService', () => { test('entries are merged when source is same', async () => { let replaced: IWorkingCopyHistoryEntry | undefined = undefined; - service.onDidReplaceEntry(e => replaced = e.entry); + disposables.add(service.onDidReplaceEntry(e => replaced = e.entry)); - const workingCopy1 = new TestWorkingCopy(testFile1Path); + const workingCopy1 = disposables.add(new TestWorkingCopy(testFile1Path)); service._configurationService.setUserConfiguration('workbench.localHistory.mergeWindow', 1); @@ -648,7 +648,7 @@ suite('WorkingCopyHistoryService', () => { }); test('move entries (file rename)', async () => { - const workingCopy = new TestWorkingCopy(testFile1Path); + const workingCopy = disposables.add(new TestWorkingCopy(testFile1Path)); const entry1 = await addEntry({ resource: workingCopy.resource, source: 'test-source' }, CancellationToken.None); const entry2 = await addEntry({ resource: workingCopy.resource, source: 'test-source' }, CancellationToken.None); @@ -695,8 +695,8 @@ suite('WorkingCopyHistoryService', () => { }); test('entries moved (folder rename)', async () => { - const workingCopy1 = new TestWorkingCopy(testFile1Path); - const workingCopy2 = new TestWorkingCopy(testFile2Path); + const workingCopy1 = disposables.add(new TestWorkingCopy(testFile1Path)); + const workingCopy2 = disposables.add(new TestWorkingCopy(testFile2Path)); const entry1A = await addEntry({ resource: workingCopy1.resource, source: 'test-source' }, CancellationToken.None); const entry2A = await addEntry({ resource: workingCopy1.resource, source: 'test-source' }, CancellationToken.None); @@ -782,4 +782,6 @@ suite('WorkingCopyHistoryService', () => { } } }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyHistoryTracker.test.ts b/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyHistoryTracker.test.ts index 5bbe9b30267..ef04d7041db 100644 --- a/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyHistoryTracker.test.ts +++ b/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyHistoryTracker.test.ts @@ -5,14 +5,14 @@ import * as assert from 'assert'; import { Event } from 'vs/base/common/event'; -import { TestContextService, TestStorageService, TestWorkingCopy } from 'vs/workbench/test/common/workbenchTestServices'; +import { TestContextService, TestWorkingCopy } from 'vs/workbench/test/common/workbenchTestServices'; import { randomPath } from 'vs/base/common/extpath'; import { join } from 'vs/base/common/path'; import { URI } from 'vs/base/common/uri'; import { WorkingCopyHistoryTracker } from 'vs/workbench/services/workingCopy/common/workingCopyHistoryTracker'; import { WorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; -import { TestEnvironmentService, TestFileService, TestLifecycleService, TestPathService, TestRemoteAgentService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestFileService, TestPathService } from 'vs/workbench/test/browser/workbenchTestServices'; import { DeferredPromise } from 'vs/base/common/async'; import { IFileService } from 'vs/platform/files/common/files'; import { Schemas } from 'vs/base/common/network'; @@ -25,43 +25,9 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { IWorkingCopyHistoryEntry, IWorkingCopyHistoryEntryDescriptor } from 'vs/workbench/services/workingCopy/common/workingCopyHistory'; import { assertIsDefined } from 'vs/base/common/types'; import { VSBuffer } from 'vs/base/common/buffer'; -import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; -import { IDisposable } from 'vs/base/common/lifecycle'; -import { NativeWorkingCopyHistoryService } from 'vs/workbench/services/workingCopy/common/workingCopyHistoryService'; -import { NullLogService } from 'vs/platform/log/common/log'; -import { FileService } from 'vs/platform/files/common/fileService'; -import { LabelService } from 'vs/workbench/services/label/common/labelService'; - -class TestWorkingCopyHistoryService extends NativeWorkingCopyHistoryService { - - readonly _fileService: IFileService; - readonly _configurationService: TestConfigurationService; - readonly _lifecycleService: TestLifecycleService; - - constructor(testDir: URI | string) { - const environmentService = TestEnvironmentService; - const logService = new NullLogService(); - const fileService = new FileService(logService); - - fileService.registerProvider(Schemas.vscodeUserData, new InMemoryFileSystemProvider()); - - const remoteAgentService = new TestRemoteAgentService(); - - const uriIdentityService = new UriIdentityService(fileService); - - const labelService = new LabelService(environmentService, new TestContextService(), new TestPathService(), new TestRemoteAgentService(), new TestStorageService(), new TestLifecycleService()); - - const lifecycleService = new TestLifecycleService(); - - const configurationService = new TestConfigurationService(); - - super(fileService, remoteAgentService, environmentService, uriIdentityService, labelService, lifecycleService, logService, configurationService); - - this._fileService = fileService; - this._configurationService = configurationService; - this._lifecycleService = lifecycleService; - } -} +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { TestWorkingCopyHistoryService } from 'vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyHistoryService.test'; suite('WorkingCopyHistoryTracker', () => { @@ -73,13 +39,14 @@ suite('WorkingCopyHistoryTracker', () => { let workingCopyService: WorkingCopyService; let fileService: IFileService; let configurationService: TestConfigurationService; - let inMemoryFileSystemDisposable: IDisposable; let tracker: WorkingCopyHistoryTracker; let testFile1Path: URI; let testFile2Path: URI; + const disposables = new DisposableStore(); + const testFile1PathContents = 'Hello Foo'; const testFile2PathContents = [ 'Lorem ipsum ', @@ -104,14 +71,12 @@ suite('WorkingCopyHistoryTracker', () => { historyHome = joinPath(testDir, 'User', 'History'); workHome = joinPath(testDir, 'work'); - workingCopyHistoryService = new TestWorkingCopyHistoryService(testDir); - workingCopyService = new WorkingCopyService(); + workingCopyHistoryService = disposables.add(new TestWorkingCopyHistoryService(disposables)); + workingCopyService = disposables.add(new WorkingCopyService()); fileService = workingCopyHistoryService._fileService; configurationService = workingCopyHistoryService._configurationService; - inMemoryFileSystemDisposable = fileService.registerProvider(Schemas.inMemory, new InMemoryFileSystemProvider()); - - tracker = createTracker(); + tracker = disposables.add(createTracker()); await fileService.createFolder(historyHome); await fileService.createFolder(workHome); @@ -127,7 +92,7 @@ suite('WorkingCopyHistoryTracker', () => { return new WorkingCopyHistoryTracker( workingCopyService, workingCopyHistoryService, - new UriIdentityService(new TestFileService()), + disposables.add(new UriIdentityService(disposables.add(new TestFileService()))), new TestPathService(undefined, Schemas.file), configurationService, new UndoRedoService(new TestDialogService(), new TestNotificationService()), @@ -137,28 +102,23 @@ suite('WorkingCopyHistoryTracker', () => { } teardown(async () => { - workingCopyHistoryService.dispose(); - workingCopyService.dispose(); - tracker.dispose(); - await fileService.del(testDir, { recursive: true }); - - inMemoryFileSystemDisposable.dispose(); + disposables.clear(); }); test('history entry added on save', async () => { - const workingCopy1 = new TestWorkingCopy(testFile1Path); - const workingCopy2 = new TestWorkingCopy(testFile2Path); + const workingCopy1 = disposables.add(new TestWorkingCopy(testFile1Path)); + const workingCopy2 = disposables.add(new TestWorkingCopy(testFile2Path)); const stat1 = await fileService.resolve(workingCopy1.resource, { resolveMetadata: true }); const stat2 = await fileService.resolve(workingCopy2.resource, { resolveMetadata: true }); - workingCopyService.registerWorkingCopy(workingCopy1); - workingCopyService.registerWorkingCopy(workingCopy2); + disposables.add(workingCopyService.registerWorkingCopy(workingCopy1)); + disposables.add(workingCopyService.registerWorkingCopy(workingCopy2)); const saveResult = new DeferredPromise(); let addedCounter = 0; - workingCopyHistoryService.onDidAddEntry(e => { + disposables.add(workingCopyHistoryService.onDidAddEntry(e => { if (isEqual(e.entry.workingCopy.resource, workingCopy1.resource) || isEqual(e.entry.workingCopy.resource, workingCopy2.resource)) { addedCounter++; @@ -166,7 +126,7 @@ suite('WorkingCopyHistoryTracker', () => { saveResult.complete(); } } - }); + })); await workingCopy1.save(undefined, stat1); await workingCopy2.save(undefined, stat2); @@ -185,7 +145,7 @@ suite('WorkingCopyHistoryTracker', () => { // Recreate to apply settings tracker.dispose(); - tracker = createTracker(); + tracker = disposables.add(createTracker()); return assertNoLocalHistoryEntryAddedWithSettingsConfigured(); }); @@ -197,17 +157,17 @@ suite('WorkingCopyHistoryTracker', () => { }); async function assertNoLocalHistoryEntryAddedWithSettingsConfigured(): Promise { - const workingCopy1 = new TestWorkingCopy(testFile1Path); - const workingCopy2 = new TestWorkingCopy(testFile2Path); + const workingCopy1 = disposables.add(new TestWorkingCopy(testFile1Path)); + const workingCopy2 = disposables.add(new TestWorkingCopy(testFile2Path)); const stat1 = await fileService.resolve(workingCopy1.resource, { resolveMetadata: true }); const stat2 = await fileService.resolve(workingCopy2.resource, { resolveMetadata: true }); - workingCopyService.registerWorkingCopy(workingCopy1); - workingCopyService.registerWorkingCopy(workingCopy2); + disposables.add(workingCopyService.registerWorkingCopy(workingCopy1)); + disposables.add(workingCopyService.registerWorkingCopy(workingCopy2)); const saveResult = new DeferredPromise(); - workingCopyHistoryService.onDidAddEntry(e => { + disposables.add(workingCopyHistoryService.onDidAddEntry(e => { if (isEqual(e.entry.workingCopy.resource, workingCopy1.resource)) { assert.fail('Unexpected working copy history entry: ' + e.entry.workingCopy.resource.toString()); } @@ -215,7 +175,7 @@ suite('WorkingCopyHistoryTracker', () => { if (isEqual(e.entry.workingCopy.resource, workingCopy2.resource)) { saveResult.complete(); } - }); + })); await workingCopy1.save(undefined, stat1); await workingCopy2.save(undefined, stat2); @@ -226,7 +186,7 @@ suite('WorkingCopyHistoryTracker', () => { test('entries moved (file rename)', async () => { const entriesMoved = Event.toPromise(workingCopyHistoryService.onDidMoveEntries); - const workingCopy = new TestWorkingCopy(testFile1Path); + const workingCopy = disposables.add(new TestWorkingCopy(testFile1Path)); const entry1 = await addEntry({ resource: workingCopy.resource, source: 'test-source' }, CancellationToken.None); const entry2 = await addEntry({ resource: workingCopy.resource, source: 'test-source' }, CancellationToken.None); @@ -272,8 +232,8 @@ suite('WorkingCopyHistoryTracker', () => { test('entries moved (folder rename)', async () => { const entriesMoved = Event.toPromise(workingCopyHistoryService.onDidMoveEntries); - const workingCopy1 = new TestWorkingCopy(testFile1Path); - const workingCopy2 = new TestWorkingCopy(testFile2Path); + const workingCopy1 = disposables.add(new TestWorkingCopy(testFile1Path)); + const workingCopy2 = disposables.add(new TestWorkingCopy(testFile2Path)); const entry1A = await addEntry({ resource: workingCopy1.resource, source: 'test-source' }, CancellationToken.None); const entry2A = await addEntry({ resource: workingCopy1.resource, source: 'test-source' }, CancellationToken.None); @@ -352,5 +312,6 @@ suite('WorkingCopyHistoryTracker', () => { } } }); -}); + ensureNoDisposablesAreLeakedInTestSuite(); +}); diff --git a/src/vs/workbench/services/workspaces/test/common/testWorkspaceTrustService.ts b/src/vs/workbench/services/workspaces/test/common/testWorkspaceTrustService.ts deleted file mode 100644 index 636b664aa6f..00000000000 --- a/src/vs/workbench/services/workspaces/test/common/testWorkspaceTrustService.ts +++ /dev/null @@ -1,144 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Emitter } from 'vs/base/common/event'; -import { IDisposable } from 'vs/base/common/lifecycle'; -import { URI } from 'vs/base/common/uri'; -import { IWorkspaceTrustEnablementService, IWorkspaceTrustManagementService, IWorkspaceTrustRequestService, IWorkspaceTrustTransitionParticipant, IWorkspaceTrustUriInfo, WorkspaceTrustRequestOptions, WorkspaceTrustUriResponse } from 'vs/platform/workspace/common/workspaceTrust'; - - -export class TestWorkspaceTrustEnablementService implements IWorkspaceTrustEnablementService { - _serviceBrand: undefined; - - constructor(private isEnabled: boolean = true) { } - - isWorkspaceTrustEnabled(): boolean { - return this.isEnabled; - } -} - -export class TestWorkspaceTrustManagementService implements IWorkspaceTrustManagementService { - _serviceBrand: undefined; - - private _onDidChangeTrust = new Emitter(); - onDidChangeTrust = this._onDidChangeTrust.event; - - private _onDidChangeTrustedFolders = new Emitter(); - onDidChangeTrustedFolders = this._onDidChangeTrustedFolders.event; - - private _onDidInitiateWorkspaceTrustRequestOnStartup = new Emitter(); - onDidInitiateWorkspaceTrustRequestOnStartup = this._onDidInitiateWorkspaceTrustRequestOnStartup.event; - - - constructor( - private trusted: boolean = true - ) { } - - get acceptsOutOfWorkspaceFiles(): boolean { - throw new Error('Method not implemented.'); - } - - set acceptsOutOfWorkspaceFiles(value: boolean) { - throw new Error('Method not implemented.'); - } - - addWorkspaceTrustTransitionParticipant(participant: IWorkspaceTrustTransitionParticipant): IDisposable { - throw new Error('Method not implemented.'); - } - - getTrustedUris(): URI[] { - throw new Error('Method not implemented.'); - } - - setParentFolderTrust(trusted: boolean): Promise { - throw new Error('Method not implemented.'); - } - - getUriTrustInfo(uri: URI): Promise { - throw new Error('Method not implemented.'); - } - - async setTrustedUris(folders: URI[]): Promise { - throw new Error('Method not implemented.'); - } - - async setUrisTrust(uris: URI[], trusted: boolean): Promise { - throw new Error('Method not implemented.'); - } - - canSetParentFolderTrust(): boolean { - throw new Error('Method not implemented.'); - } - - canSetWorkspaceTrust(): boolean { - throw new Error('Method not implemented.'); - } - - isWorkspaceTrusted(): boolean { - return this.trusted; - } - - isWorkspaceTrustForced(): boolean { - return false; - } - - get workspaceTrustInitialized(): Promise { - return Promise.resolve(); - } - - get workspaceResolved(): Promise { - return Promise.resolve(); - } - - async setWorkspaceTrust(trusted: boolean): Promise { - if (this.trusted !== trusted) { - this.trusted = trusted; - this._onDidChangeTrust.fire(this.trusted); - } - } -} - -export class TestWorkspaceTrustRequestService implements IWorkspaceTrustRequestService { - _serviceBrand: any; - - private readonly _onDidInitiateOpenFilesTrustRequest = new Emitter(); - readonly onDidInitiateOpenFilesTrustRequest = this._onDidInitiateOpenFilesTrustRequest.event; - - private readonly _onDidInitiateWorkspaceTrustRequest = new Emitter(); - readonly onDidInitiateWorkspaceTrustRequest = this._onDidInitiateWorkspaceTrustRequest.event; - - private readonly _onDidInitiateWorkspaceTrustRequestOnStartup = new Emitter(); - readonly onDidInitiateWorkspaceTrustRequestOnStartup = this._onDidInitiateWorkspaceTrustRequestOnStartup.event; - - constructor(private readonly _trusted: boolean) { } - - requestOpenUrisHandler = async (uris: URI[]) => { - return WorkspaceTrustUriResponse.Open; - }; - - requestOpenFilesTrust(uris: URI[]): Promise { - return this.requestOpenUrisHandler(uris); - } - - async completeOpenFilesTrustRequest(result: WorkspaceTrustUriResponse, saveResponse: boolean): Promise { - throw new Error('Method not implemented.'); - } - - cancelWorkspaceTrustRequest(): void { - throw new Error('Method not implemented.'); - } - - async completeWorkspaceTrustRequest(trusted?: boolean): Promise { - throw new Error('Method not implemented.'); - } - - async requestWorkspaceTrust(options?: WorkspaceTrustRequestOptions): Promise { - return this._trusted; - } - - requestWorkspaceTrustOnStartup(): void { - throw new Error('Method not implemented.'); - } -} diff --git a/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts b/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts index 037330ae50e..a39caa0061e 100644 --- a/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts +++ b/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts @@ -21,8 +21,7 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; import { WorkspaceTrustEnablementService, WorkspaceTrustManagementService, WORKSPACE_TRUST_STORAGE_KEY } from 'vs/workbench/services/workspaces/common/workspaceTrust'; -import { TestWorkspaceTrustEnablementService } from 'vs/workbench/services/workspaces/test/common/testWorkspaceTrustService'; -import { TestContextService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; +import { TestContextService, TestStorageService, TestWorkspaceTrustEnablementService } from 'vs/workbench/test/common/workbenchTestServices'; suite('Workspace Trust', () => { let instantiationService: TestInstantiationService; diff --git a/src/vs/workbench/test/browser/part.test.ts b/src/vs/workbench/test/browser/part.test.ts index 20c15a41f63..b42ade588b8 100644 --- a/src/vs/workbench/test/browser/part.test.ts +++ b/src/vs/workbench/test/browser/part.test.ts @@ -11,9 +11,13 @@ import { append, $, hide } from 'vs/base/browser/dom'; import { TestLayoutService } from 'vs/workbench/test/browser/workbenchTestServices'; import { StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { DisposableStore } from 'vs/base/common/lifecycle'; suite('Workbench parts', () => { + const disposables = new DisposableStore(); + class SimplePart extends Part { minimumWidth: number = 50; @@ -33,7 +37,7 @@ suite('Workbench parts', () => { class MyPart extends SimplePart { constructor(private expectedParent: HTMLElement) { - super('myPart', { hasTitle: true }, new TestThemeService(), new TestStorageService(), new TestLayoutService()); + super('myPart', { hasTitle: true }, new TestThemeService(), disposables.add(new TestStorageService()), new TestLayoutService()); } protected override createTitleArea(parent: HTMLElement): HTMLElement { @@ -58,7 +62,7 @@ suite('Workbench parts', () => { class MyPart2 extends SimplePart { constructor() { - super('myPart2', { hasTitle: true }, new TestThemeService(), new TestStorageService(), new TestLayoutService()); + super('myPart2', { hasTitle: true }, new TestThemeService(), disposables.add(new TestStorageService()), new TestLayoutService()); } protected override createTitleArea(parent: HTMLElement): HTMLElement { @@ -83,7 +87,7 @@ suite('Workbench parts', () => { class MyPart3 extends SimplePart { constructor() { - super('myPart2', { hasTitle: false }, new TestThemeService(), new TestStorageService(), new TestLayoutService()); + super('myPart2', { hasTitle: false }, new TestThemeService(), disposables.add(new TestStorageService()), new TestLayoutService()); } protected override createTitleArea(parent: HTMLElement): HTMLElement { @@ -111,6 +115,7 @@ suite('Workbench parts', () => { teardown(() => { document.body.removeChild(fixture); + disposables.clear(); }); test('Creation', () => { @@ -118,7 +123,7 @@ suite('Workbench parts', () => { document.getElementById(fixtureId)!.appendChild(b); hide(b); - let part = new MyPart(b); + let part = disposables.add(new MyPart(b)); part.create(b); assert.strictEqual(part.getId(), 'myPart'); @@ -132,7 +137,7 @@ suite('Workbench parts', () => { part.testSaveState(); // Re-Create to assert memento contents - part = new MyPart(b); + part = disposables.add(new MyPart(b)); memento = part.testGetMemento(StorageScope.PROFILE, StorageTarget.MACHINE); assert(memento); @@ -144,7 +149,7 @@ suite('Workbench parts', () => { delete memento.bar; part.testSaveState(); - part = new MyPart(b); + part = disposables.add(new MyPart(b)); memento = part.testGetMemento(StorageScope.PROFILE, StorageTarget.MACHINE); assert(memento); assert.strictEqual(isEmptyObject(memento), true); @@ -155,7 +160,7 @@ suite('Workbench parts', () => { document.getElementById(fixtureId)!.appendChild(b); hide(b); - const part = new MyPart2(); + const part = disposables.add(new MyPart2()); part.create(b); assert(document.getElementById('myPart.title')); @@ -167,10 +172,12 @@ suite('Workbench parts', () => { document.getElementById(fixtureId)!.appendChild(b); hide(b); - const part = new MyPart3(); + const part = disposables.add(new MyPart3()); part.create(b); assert(!document.getElementById('myPart.title')); assert(document.getElementById('myPart.content')); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/test/browser/parts/editor/breadcrumbModel.test.ts b/src/vs/workbench/test/browser/parts/editor/breadcrumbModel.test.ts index fdda3787190..47eb7efceba 100644 --- a/src/vs/workbench/test/browser/parts/editor/breadcrumbModel.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/breadcrumbModel.test.ts @@ -13,9 +13,11 @@ import { TestContextService } from 'vs/workbench/test/common/workbenchTestServic import { Workspace } from 'vs/platform/workspace/test/common/testWorkspace'; import { mock } from 'vs/base/test/common/mock'; import { IOutlineService } from 'vs/workbench/services/outline/browser/outline'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('Breadcrumb Model', function () { + let model: BreadcrumbsModel; const workspaceService = new TestContextService(new Workspace('ffff', [new WorkspaceFolder({ uri: URI.parse('foo:/bar/baz/ws'), name: 'ws', index: 0 })])); const configService = new class extends TestConfigurationService { override getValue(...args: any[]) { @@ -32,9 +34,15 @@ suite('Breadcrumb Model', function () { } }; + teardown(function () { + model.dispose(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + test('only uri, inside workspace', function () { - const model = new BreadcrumbsModel(URI.parse('foo:/bar/baz/ws/some/path/file.ts'), undefined, configService, workspaceService, new class extends mock() { }); + model = new BreadcrumbsModel(URI.parse('foo:/bar/baz/ws/some/path/file.ts'), undefined, configService, workspaceService, new class extends mock() { }); const elements = model.getElements(); assert.strictEqual(elements.length, 3); @@ -49,7 +57,7 @@ suite('Breadcrumb Model', function () { test('display uri matters for FileElement', function () { - const model = new BreadcrumbsModel(URI.parse('foo:/bar/baz/ws/some/PATH/file.ts'), undefined, configService, workspaceService, new class extends mock() { }); + model = new BreadcrumbsModel(URI.parse('foo:/bar/baz/ws/some/PATH/file.ts'), undefined, configService, workspaceService, new class extends mock() { }); const elements = model.getElements(); assert.strictEqual(elements.length, 3); @@ -64,7 +72,7 @@ suite('Breadcrumb Model', function () { test('only uri, outside workspace', function () { - const model = new BreadcrumbsModel(URI.parse('foo:/outside/file.ts'), undefined, configService, workspaceService, new class extends mock() { }); + model = new BreadcrumbsModel(URI.parse('foo:/outside/file.ts'), undefined, configService, workspaceService, new class extends mock() { }); const elements = model.getElements(); assert.strictEqual(elements.length, 2); diff --git a/src/vs/workbench/test/browser/parts/editor/diffEditorInput.test.ts b/src/vs/workbench/test/browser/parts/editor/diffEditorInput.test.ts index fbc271d79e1..be643126620 100644 --- a/src/vs/workbench/test/browser/parts/editor/diffEditorInput.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/diffEditorInput.test.ts @@ -10,6 +10,7 @@ import { workbenchInstantiationService } from 'vs/workbench/test/browser/workben import { EditorResourceAccessor, isDiffEditorInput, isResourceDiffEditorInput, isResourceSideBySideEditorInput, IUntypedEditorInput } from 'vs/workbench/common/editor'; import { URI } from 'vs/base/common/uri'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('Diff editor input', () => { @@ -36,31 +37,27 @@ suite('Diff editor input', () => { } } - let disposables: DisposableStore; - - setup(() => { - disposables = new DisposableStore(); - }); + const disposables = new DisposableStore(); teardown(() => { - disposables.dispose(); + disposables.clear(); }); test('basics', () => { const instantiationService = workbenchInstantiationService(undefined, disposables); let counter = 0; - const input = new MyEditorInput(); - input.onWillDispose(() => { + const input = disposables.add(new MyEditorInput()); + disposables.add(input.onWillDispose(() => { assert(true); counter++; - }); + })); - const otherInput = new MyEditorInput(); - otherInput.onWillDispose(() => { + const otherInput = disposables.add(new MyEditorInput()); + disposables.add(otherInput.onWillDispose(() => { assert(true); counter++; - }); + })); const diffInput = instantiationService.createInstance(DiffEditorInput, 'name', 'description', input, otherInput, undefined); @@ -72,7 +69,6 @@ suite('Diff editor input', () => { assert(diffInput.matches(diffInput)); assert(!diffInput.matches(otherInput)); - diffInput.dispose(); assert.strictEqual(counter, 0); }); @@ -80,8 +76,8 @@ suite('Diff editor input', () => { test('toUntyped', () => { const instantiationService = workbenchInstantiationService(undefined, disposables); - const input = new MyEditorInput(URI.file('foo/bar1')); - const otherInput = new MyEditorInput(URI.file('foo/bar2')); + const input = disposables.add(new MyEditorInput(URI.file('foo/bar1'))); + const otherInput = disposables.add(new MyEditorInput(URI.file('foo/bar2'))); const diffInput = instantiationService.createInstance(DiffEditorInput, 'name', 'description', input, otherInput, undefined); @@ -95,27 +91,29 @@ suite('Diff editor input', () => { const instantiationService = workbenchInstantiationService(undefined, disposables); let counter = 0; - let input = new MyEditorInput(); - let otherInput = new MyEditorInput(); + let input = disposables.add(new MyEditorInput()); + let otherInput = disposables.add(new MyEditorInput()); - const diffInput = instantiationService.createInstance(DiffEditorInput, 'name', 'description', input, otherInput, undefined); - diffInput.onWillDispose(() => { + const diffInput = disposables.add(instantiationService.createInstance(DiffEditorInput, 'name', 'description', input, otherInput, undefined)); + disposables.add(diffInput.onWillDispose(() => { counter++; assert(true); - }); + })); input.dispose(); - input = new MyEditorInput(); - otherInput = new MyEditorInput(); + input = disposables.add(new MyEditorInput()); + otherInput = disposables.add(new MyEditorInput()); - const diffInput2 = instantiationService.createInstance(DiffEditorInput, 'name', 'description', input, otherInput, undefined); - diffInput2.onWillDispose(() => { + const diffInput2 = disposables.add(instantiationService.createInstance(DiffEditorInput, 'name', 'description', input, otherInput, undefined)); + disposables.add(diffInput2.onWillDispose(() => { counter++; assert(true); - }); + })); otherInput.dispose(); assert.strictEqual(counter, 2); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/test/browser/parts/editor/editor.test.ts b/src/vs/workbench/test/browser/parts/editor/editor.test.ts index a49e5dd00c1..ced0b141475 100644 --- a/src/vs/workbench/test/browser/parts/editor/editor.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editor.test.ts @@ -12,7 +12,7 @@ import { workbenchInstantiationService, TestServiceAccessor, TestEditorInput, re import { Schemas } from 'vs/base/common/network'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { DisposableStore } from 'vs/base/common/lifecycle'; -import { toResource } from 'vs/base/test/common/utils'; +import { ensureNoDisposablesAreLeakedInTestSuite, toResource } from 'vs/base/test/common/utils'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { whenEditorClosed } from 'vs/workbench/browser/editor'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -39,22 +39,11 @@ suite('Workbench editor utils', () => { let instantiationService: IInstantiationService; let accessor: TestServiceAccessor; - async function createServices(): Promise { - const instantiationService = workbenchInstantiationService(undefined, disposables); - - const part = await createEditorPart(instantiationService, disposables); - instantiationService.stub(IEditorGroupsService, part); - - const editorService = instantiationService.createInstance(EditorService); - instantiationService.stub(IEditorService, editorService); - - return instantiationService.createInstance(TestServiceAccessor); - } - setup(() => { instantiationService = workbenchInstantiationService(undefined, disposables); accessor = instantiationService.createInstance(TestServiceAccessor); + disposables.add(accessor.untitledTextEditorService); disposables.add(registerTestFileEditor()); disposables.add(registerTestSideBySideEditor()); disposables.add(registerTestResourceEditor()); @@ -62,8 +51,6 @@ suite('Workbench editor utils', () => { }); teardown(() => { - accessor.untitledTextEditorService.dispose(); - disposables.clear(); }); @@ -99,8 +86,8 @@ suite('Workbench editor utils', () => { }); test('EditorInputCapabilities', () => { - const testInput1 = new TestFileEditorInput(URI.file('resource1'), 'testTypeId'); - const testInput2 = new TestFileEditorInput(URI.file('resource2'), 'testTypeId'); + const testInput1 = disposables.add(new TestFileEditorInput(URI.file('resource1'), 'testTypeId')); + const testInput2 = disposables.add(new TestFileEditorInput(URI.file('resource2'), 'testTypeId')); testInput1.capabilities = EditorInputCapabilities.None; assert.strictEqual(testInput1.hasCapability(EditorInputCapabilities.None), true); @@ -162,7 +149,7 @@ suite('Workbench editor utils', () => { assert.ok(!EditorResourceAccessor.getCanonicalUri(null!)); assert.ok(!EditorResourceAccessor.getOriginalUri(null!)); - const untitled = instantiationService.createInstance(UntitledTextEditorInput, service.create()); + const untitled = disposables.add(instantiationService.createInstance(UntitledTextEditorInput, service.create())); assert.strictEqual(EditorResourceAccessor.getCanonicalUri(untitled)?.toString(), untitled.resource.toString()); assert.strictEqual(EditorResourceAccessor.getCanonicalUri(untitled, { supportSideBySide: SideBySideEditor.PRIMARY })?.toString(), untitled.resource.toString()); @@ -182,7 +169,7 @@ suite('Workbench editor utils', () => { assert.strictEqual(EditorResourceAccessor.getOriginalUri(untitled, { filterByScheme: [Schemas.file, Schemas.untitled] })?.toString(), untitled.resource.toString()); assert.ok(!EditorResourceAccessor.getOriginalUri(untitled, { filterByScheme: Schemas.file })); - const file = new TestEditorInput(URI.file('/some/path.txt'), 'editorResourceFileTest'); + const file = disposables.add(new TestEditorInput(URI.file('/some/path.txt'), 'editorResourceFileTest')); assert.strictEqual(EditorResourceAccessor.getCanonicalUri(file)?.toString(), file.resource.toString()); assert.strictEqual(EditorResourceAccessor.getCanonicalUri(file, { supportSideBySide: SideBySideEditor.PRIMARY })?.toString(), file.resource.toString()); @@ -246,7 +233,7 @@ suite('Workbench editor utils', () => { const resource = URI.file('/some/path.txt'); const preferredResource = URI.file('/some/PATH.txt'); - const fileWithPreferredResource = new TestEditorInputWithPreferredResource(URI.file('/some/path.txt'), URI.file('/some/PATH.txt'), 'editorResourceFileTest'); + const fileWithPreferredResource = disposables.add(new TestEditorInputWithPreferredResource(URI.file('/some/path.txt'), URI.file('/some/PATH.txt'), 'editorResourceFileTest')); assert.strictEqual(EditorResourceAccessor.getCanonicalUri(fileWithPreferredResource)?.toString(), resource.toString()); assert.strictEqual(EditorResourceAccessor.getOriginalUri(fileWithPreferredResource)?.toString(), preferredResource.toString()); @@ -363,13 +350,13 @@ suite('Workbench editor utils', () => { assert.strictEqual(isEditorIdentifier(undefined), false); assert.strictEqual(isEditorIdentifier('undefined'), false); - const testInput1 = new TestFileEditorInput(URI.file('resource1'), 'testTypeId'); + const testInput1 = disposables.add(new TestFileEditorInput(URI.file('resource1'), 'testTypeId')); assert.strictEqual(isEditorIdentifier(testInput1), false); assert.strictEqual(isEditorIdentifier({ editor: testInput1, groupId: 3 }), true); }); test('isEditorInputWithOptionsAndGroup', () => { - const editorInput = new TestFileEditorInput(URI.file('resource1'), 'testTypeId'); + const editorInput = disposables.add(new TestFileEditorInput(URI.file('resource1'), 'testTypeId')); assert.strictEqual(isEditorInput(editorInput), true); assert.strictEqual(isEditorInputWithOptions(editorInput), false); assert.strictEqual(isEditorInputWithOptionsAndGroup(editorInput), false); @@ -434,6 +421,18 @@ suite('Workbench editor utils', () => { return testWhenEditorClosed(false, true, toResource.call(this, '/path/index.txt'), toResource.call(this, '/test.html')); }); + async function createServices(): Promise { + const instantiationService = workbenchInstantiationService(undefined, disposables); + + const part = await createEditorPart(instantiationService, disposables); + instantiationService.stub(IEditorGroupsService, part); + + const editorService = disposables.add(instantiationService.createInstance(EditorService)); + instantiationService.stub(IEditorService, editorService); + + return instantiationService.createInstance(TestServiceAccessor); + } + async function testWhenEditorClosed(sideBySide: boolean, custom: boolean, ...resources: URI[]): Promise { const accessor = await createServices(); @@ -453,4 +452,6 @@ suite('Workbench editor utils', () => { await closedPromise; } + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/test/browser/parts/editor/editorDiffModel.test.ts b/src/vs/workbench/test/browser/parts/editor/editorDiffModel.test.ts index 6187bda979f..b6ece8430e7 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorDiffModel.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorDiffModel.test.ts @@ -12,42 +12,42 @@ import { workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench import { ITextModel } from 'vs/editor/common/model'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('TextDiffEditorModel', () => { - let disposables: DisposableStore; + const disposables = new DisposableStore(); let instantiationService: IInstantiationService; let accessor: TestServiceAccessor; setup(() => { - disposables = new DisposableStore(); instantiationService = workbenchInstantiationService(undefined, disposables); accessor = instantiationService.createInstance(TestServiceAccessor); }); teardown(() => { - disposables.dispose(); + disposables.clear(); }); test('basics', async () => { - const dispose = accessor.textModelResolverService.registerTextModelContentProvider('test', { + disposables.add(accessor.textModelResolverService.registerTextModelContentProvider('test', { provideTextContent: async function (resource: URI): Promise { if (resource.scheme === 'test') { const modelContent = 'Hello Test'; const languageSelection = accessor.languageService.createById('json'); - return accessor.modelService.createModel(modelContent, languageSelection, resource); + return disposables.add(accessor.modelService.createModel(modelContent, languageSelection, resource)); } return null; } - }); + })); - const input = instantiationService.createInstance(TextResourceEditorInput, URI.from({ scheme: 'test', authority: null!, path: 'thePath' }), 'name', 'description', undefined, undefined); - const otherInput = instantiationService.createInstance(TextResourceEditorInput, URI.from({ scheme: 'test', authority: null!, path: 'thePath' }), 'name2', 'description', undefined, undefined); - const diffInput = instantiationService.createInstance(DiffEditorInput, 'name', 'description', input, otherInput, undefined); + const input = disposables.add(instantiationService.createInstance(TextResourceEditorInput, URI.from({ scheme: 'test', authority: null!, path: 'thePath' }), 'name', 'description', undefined, undefined)); + const otherInput = disposables.add(instantiationService.createInstance(TextResourceEditorInput, URI.from({ scheme: 'test', authority: null!, path: 'thePath' }), 'name2', 'description', undefined, undefined)); + const diffInput = disposables.add(instantiationService.createInstance(DiffEditorInput, 'name', 'description', input, otherInput, undefined)); - let model = await diffInput.resolve() as TextDiffEditorModel; + let model = disposables.add(await diffInput.resolve() as TextDiffEditorModel); assert(model); assert(model instanceof TextDiffEditorModel); @@ -56,13 +56,13 @@ suite('TextDiffEditorModel', () => { assert(diffEditorModel.original); assert(diffEditorModel.modified); - model = await diffInput.resolve() as TextDiffEditorModel; + model = disposables.add(await diffInput.resolve() as TextDiffEditorModel); assert(model.isResolved()); assert(diffEditorModel !== model.textDiffEditorModel); diffInput.dispose(); assert(!model.textDiffEditorModel); - - dispose.dispose(); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/test/browser/parts/editor/editorGroupModel.test.ts b/src/vs/workbench/test/browser/parts/editor/editorGroupModel.test.ts index 3467cdcd507..61be5f1243c 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorGroupModel.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorGroupModel.test.ts @@ -20,11 +20,12 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { IStorageService } from 'vs/platform/storage/common/storage'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { TestContextService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { isEqual } from 'vs/base/common/resources'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('EditorGroupModel', () => { @@ -40,8 +41,8 @@ suite('EditorGroupModel', () => { testInstService = new TestInstantiationService(); } const inst = testInstService; - inst.stub(IStorageService, new TestStorageService()); - inst.stub(ILifecycleService, new TestLifecycleService()); + inst.stub(IStorageService, disposables.add(new TestStorageService())); + inst.stub(ILifecycleService, disposables.add(new TestLifecycleService())); inst.stub(IWorkspaceContextService, new TestContextService()); inst.stub(ITelemetryService, NullTelemetryService); @@ -53,7 +54,15 @@ suite('EditorGroupModel', () => { } function createEditorGroupModel(serialized?: ISerializedEditorGroupModel): EditorGroupModel { - return inst().createInstance(EditorGroupModel, serialized); + const group = disposables.add(inst().createInstance(EditorGroupModel, serialized)); + + disposables.add(toDisposable(() => { + for (const editor of group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)) { + group.closeEditor(editor); + } + })); + + return group; } function closeAllEditors(group: EditorGroupModel): void { @@ -119,7 +128,7 @@ suite('EditorGroupModel', () => { disposed: [] }; - group.onDidModelChange(e => { + disposables.add(group.onDidModelChange(e => { if (e.kind === GroupModelChangeKind.GROUP_LOCKED) { groupEvents.locked.push(group.id); return; @@ -170,7 +179,7 @@ suite('EditorGroupModel', () => { } break; } - }); + })); return groupEvents; } @@ -251,10 +260,10 @@ suite('EditorGroupModel', () => { function input(id = String(index++), nonSerializable?: boolean, resource?: URI): EditorInput { if (resource) { - return new TestFileEditorInput(id, resource); + return disposables.add(new TestFileEditorInput(id, resource)); } - return nonSerializable ? new NonSerializableTestEditorInput(id) : new TestEditorInput(id); + return nonSerializable ? disposables.add(new NonSerializableTestEditorInput(id)) : disposables.add(new TestEditorInput(id)); } interface ISerializedTestInput { @@ -290,7 +299,7 @@ suite('EditorGroupModel', () => { const testInput: ISerializedTestInput = JSON.parse(serializedEditorInput); - return new TestEditorInput(testInput.id); + return disposables.add(new TestEditorInput(testInput.id)); } } @@ -330,7 +339,7 @@ suite('EditorGroupModel', () => { group.lock(true); assert.strictEqual(group.isLocked, true); - const clone = group.clone(); + const clone = disposables.add(group.clone()); assert.notStrictEqual(group.id, clone.id); assert.strictEqual(clone.count, 3); assert.strictEqual(clone.isLocked, false); // locking does not clone over @@ -361,8 +370,8 @@ suite('EditorGroupModel', () => { test('isActive - untyped', () => { const group = createEditorGroupModel(); - const input = new TestFileEditorInput('testInput', URI.file('fake')); - const input2 = new TestFileEditorInput('testInput2', URI.file('fake2')); + const input = disposables.add(new TestFileEditorInput('testInput', URI.file('fake'))); + const input2 = disposables.add(new TestFileEditorInput('testInput2', URI.file('fake2'))); const untypedInput = { resource: URI.file('/fake'), options: { override: 'testInput' } }; const untypedNonActiveInput = { resource: URI.file('/fake2'), options: { override: 'testInput2' } }; @@ -378,8 +387,8 @@ suite('EditorGroupModel', () => { const instantiationService = workbenchInstantiationService(undefined, disposables); const group = createEditorGroupModel(); - const input1 = new TestFileEditorInput('testInput', URI.file('fake1')); - const input2 = new TestFileEditorInput('testInput', URI.file('fake2')); + const input1 = disposables.add(new TestFileEditorInput('testInput', URI.file('fake1'))); + const input2 = disposables.add(new TestFileEditorInput('testInput', URI.file('fake2'))); const sideBySideInputSame = instantiationService.createInstance(SideBySideEditorInput, undefined, undefined, input1, input1); const sideBySideInputDifferent = instantiationService.createInstance(SideBySideEditorInput, undefined, undefined, input1, input2); @@ -406,7 +415,7 @@ suite('EditorGroupModel', () => { const instantiationService = workbenchInstantiationService(undefined, disposables); const group = createEditorGroupModel(); - const input1 = new TestFileEditorInput('testInput', URI.file('fake1')); + const input1 = disposables.add(new TestFileEditorInput('testInput', URI.file('fake1'))); const sideBySideInput = instantiationService.createInstance(SideBySideEditorInput, undefined, undefined, input1, input1); @@ -1044,8 +1053,8 @@ suite('EditorGroupModel', () => { test('Multiple Editors - Pinned and Active (DEFAULT_OPEN_EDITOR_DIRECTION = Direction.LEFT)', function () { const inst = new TestInstantiationService(); - inst.stub(IStorageService, new TestStorageService()); - inst.stub(ILifecycleService, new TestLifecycleService()); + inst.stub(IStorageService, disposables.add(new TestStorageService())); + inst.stub(ILifecycleService, disposables.add(new TestLifecycleService())); inst.stub(IWorkspaceContextService, new TestContextService()); inst.stub(ITelemetryService, NullTelemetryService); @@ -1053,7 +1062,7 @@ suite('EditorGroupModel', () => { inst.stub(IConfigurationService, config); config.setUserConfiguration('workbench', { editor: { openPositioning: 'left' } }); - const group: EditorGroupModel = inst.createInstance(EditorGroupModel, undefined); + const group: EditorGroupModel = disposables.add(inst.createInstance(EditorGroupModel, undefined)); const events = groupListener(group); @@ -1277,8 +1286,8 @@ suite('EditorGroupModel', () => { test('Multiple Editors - closing picks next to the right', function () { const inst = new TestInstantiationService(); - inst.stub(IStorageService, new TestStorageService()); - inst.stub(ILifecycleService, new TestLifecycleService()); + inst.stub(IStorageService, disposables.add(new TestStorageService())); + inst.stub(ILifecycleService, disposables.add(new TestLifecycleService())); inst.stub(IWorkspaceContextService, new TestContextService()); inst.stub(ITelemetryService, NullTelemetryService); @@ -1286,7 +1295,7 @@ suite('EditorGroupModel', () => { config.setUserConfiguration('workbench', { editor: { focusRecentEditorAfterClose: false } }); inst.stub(IConfigurationService, config); - const group = inst.createInstance(EditorGroupModel, undefined); + const group = disposables.add(inst.createInstance(EditorGroupModel, undefined)); const events = groupListener(group); const input1 = input(); @@ -1656,9 +1665,9 @@ suite('EditorGroupModel', () => { test('Single Group, Single Editor - persist', function () { const inst = new TestInstantiationService(); - inst.stub(IStorageService, new TestStorageService()); + inst.stub(IStorageService, disposables.add(new TestStorageService())); inst.stub(IWorkspaceContextService, new TestContextService()); - const lifecycle = new TestLifecycleService(); + const lifecycle = disposables.add(new TestLifecycleService()); inst.stub(ILifecycleService, lifecycle); inst.stub(ITelemetryService, NullTelemetryService); @@ -1679,7 +1688,7 @@ suite('EditorGroupModel', () => { assert.strictEqual(group.isActive(input1), true); // Create model again - should load from storage - group = inst.createInstance(EditorGroupModel, group.serialize()); + group = disposables.add(inst.createInstance(EditorGroupModel, group.serialize())); assert.strictEqual(group.count, 1); assert.strictEqual(group.activeEditor!.matches(input1), true); @@ -1691,9 +1700,9 @@ suite('EditorGroupModel', () => { test('Multiple Groups, Multiple editors - persist', function () { const inst = new TestInstantiationService(); - inst.stub(IStorageService, new TestStorageService()); + inst.stub(IStorageService, disposables.add(new TestStorageService())); inst.stub(IWorkspaceContextService, new TestContextService()); - const lifecycle = new TestLifecycleService(); + const lifecycle = disposables.add(new TestLifecycleService()); inst.stub(ILifecycleService, lifecycle); inst.stub(ITelemetryService, NullTelemetryService); @@ -1739,8 +1748,8 @@ suite('EditorGroupModel', () => { assert.strictEqual(group2.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)[2].matches(g2_input2), true); // Create model again - should load from storage - group1 = inst.createInstance(EditorGroupModel, group1.serialize()); - group2 = inst.createInstance(EditorGroupModel, group2.serialize()); + group1 = disposables.add(inst.createInstance(EditorGroupModel, group1.serialize())); + group2 = disposables.add(inst.createInstance(EditorGroupModel, group2.serialize())); assert.strictEqual(group1.count, 3); assert.strictEqual(group2.count, 3); @@ -1762,9 +1771,9 @@ suite('EditorGroupModel', () => { test('Single group, multiple editors - persist (some not persistable)', function () { const inst = new TestInstantiationService(); - inst.stub(IStorageService, new TestStorageService()); + inst.stub(IStorageService, disposables.add(new TestStorageService())); inst.stub(IWorkspaceContextService, new TestContextService()); - const lifecycle = new TestLifecycleService(); + const lifecycle = disposables.add(new TestLifecycleService()); inst.stub(ILifecycleService, lifecycle); inst.stub(ITelemetryService, NullTelemetryService); @@ -1793,7 +1802,7 @@ suite('EditorGroupModel', () => { assert.strictEqual(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)[2].matches(serializableInput1), true); // Create model again - should load from storage - group = inst.createInstance(EditorGroupModel, group.serialize()); + group = disposables.add(inst.createInstance(EditorGroupModel, group.serialize())); assert.strictEqual(group.count, 2); assert.strictEqual(group.activeEditor!.matches(serializableInput2), true); @@ -1807,9 +1816,9 @@ suite('EditorGroupModel', () => { test('Single group, multiple editors - persist (some not persistable, sticky editors)', function () { const inst = new TestInstantiationService(); - inst.stub(IStorageService, new TestStorageService()); + inst.stub(IStorageService, disposables.add(new TestStorageService())); inst.stub(IWorkspaceContextService, new TestContextService()); - const lifecycle = new TestLifecycleService(); + const lifecycle = disposables.add(new TestLifecycleService()); inst.stub(ILifecycleService, lifecycle); inst.stub(ITelemetryService, NullTelemetryService); @@ -1833,7 +1842,7 @@ suite('EditorGroupModel', () => { assert.strictEqual(group.stickyCount, 1); // Create model again - should load from storage - group = inst.createInstance(EditorGroupModel, group.serialize()); + group = disposables.add(inst.createInstance(EditorGroupModel, group.serialize())); assert.strictEqual(group.count, 2); assert.strictEqual(group.stickyCount, 0); @@ -1843,9 +1852,9 @@ suite('EditorGroupModel', () => { test('Multiple groups, multiple editors - persist (some not persistable, causes empty group)', function () { const inst = new TestInstantiationService(); - inst.stub(IStorageService, new TestStorageService()); + inst.stub(IStorageService, disposables.add(new TestStorageService())); inst.stub(IWorkspaceContextService, new TestContextService()); - const lifecycle = new TestLifecycleService(); + const lifecycle = disposables.add(new TestLifecycleService()); inst.stub(ILifecycleService, lifecycle); inst.stub(ITelemetryService, NullTelemetryService); @@ -1868,8 +1877,8 @@ suite('EditorGroupModel', () => { group2.openEditor(nonSerializableInput); // Create model again - should load from storage - group1 = inst.createInstance(EditorGroupModel, group1.serialize()); - group2 = inst.createInstance(EditorGroupModel, group2.serialize()); + group1 = disposables.add(inst.createInstance(EditorGroupModel, group1.serialize())); + group2 = disposables.add(inst.createInstance(EditorGroupModel, group2.serialize())); assert.strictEqual(group1.count, 2); assert.strictEqual(group1.getEditors(EditorsOrder.SEQUENTIAL)[0].matches(serializableInput1), true); @@ -1937,32 +1946,32 @@ suite('EditorGroupModel', () => { group2.openEditor(input2, { pinned: true, active: true }); let dirty1Counter = 0; - group1.onDidModelChange((e) => { + disposables.add(group1.onDidModelChange((e) => { if (e.kind === GroupModelChangeKind.EDITOR_DIRTY) { dirty1Counter++; } - }); + })); let dirty2Counter = 0; - group2.onDidModelChange((e) => { + disposables.add(group2.onDidModelChange((e) => { if (e.kind === GroupModelChangeKind.EDITOR_DIRTY) { dirty2Counter++; } - }); + })); let label1ChangeCounter = 0; - group1.onDidModelChange((e) => { + disposables.add(group1.onDidModelChange((e) => { if (e.kind === GroupModelChangeKind.EDITOR_LABEL) { label1ChangeCounter++; } - }); + })); let label2ChangeCounter = 0; - group2.onDidModelChange((e) => { + disposables.add(group2.onDidModelChange((e) => { if (e.kind === GroupModelChangeKind.EDITOR_LABEL) { label2ChangeCounter++; } - }); + })); (input1).setDirty(); (input1).setLabel(); @@ -2380,4 +2389,6 @@ suite('EditorGroupModel', () => { assert.strictEqual(group2Events.unsticky[0].editor, input1group2); assert.strictEqual(group2Events.unsticky[0].editorIndex, 1); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/test/browser/parts/editor/editorInput.test.ts b/src/vs/workbench/test/browser/parts/editor/editorInput.test.ts index 9d1a741e97c..4c1b74c31e7 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorInput.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorInput.test.ts @@ -7,6 +7,7 @@ import * as assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IResourceEditorInput, ITextResourceEditorInput } from 'vs/platform/editor/common/editor'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { DEFAULT_EDITOR_ASSOCIATION, IResourceDiffEditorInput, IResourceMergeEditorInput, IResourceSideBySideEditorInput, isEditorInput, isResourceDiffEditorInput, isResourceEditorInput, isResourceMergeEditorInput, isResourceSideBySideEditorInput, isUntitledResourceEditorInput, IUntitledTextResourceEditorInput } from 'vs/workbench/common/editor'; @@ -22,7 +23,7 @@ suite('EditorInput', () => { let instantiationService: IInstantiationService; let accessor: TestServiceAccessor; - let disposables: DisposableStore; + const disposables = new DisposableStore(); const testResource: URI = URI.from({ scheme: 'random', path: '/path' }); const untypedResourceEditorInput: IResourceEditorInput = { resource: testResource, options: { override: DEFAULT_EDITOR_ASSOCIATION.id } }; @@ -52,7 +53,6 @@ suite('EditorInput', () => { }; setup(() => { - disposables = new DisposableStore(); instantiationService = workbenchInstantiationService(undefined, disposables); accessor = instantiationService.createInstance(TestServiceAccessor); @@ -74,7 +74,7 @@ suite('EditorInput', () => { }); teardown(() => { - disposables.dispose(); + disposables.clear(); }); class MyEditorInput extends EditorInput { @@ -86,8 +86,8 @@ suite('EditorInput', () => { test('basics', () => { let counter = 0; - const input = new MyEditorInput(); - const otherInput = new MyEditorInput(); + const input = disposables.add(new MyEditorInput()); + const otherInput = disposables.add(new MyEditorInput()); assert.ok(isEditorInput(input)); assert.ok(!isEditorInput(undefined)); @@ -104,10 +104,10 @@ suite('EditorInput', () => { assert(!input.matches(otherInput)); assert(input.getName()); - input.onWillDispose(() => { + disposables.add(input.onWillDispose(() => { assert(true); counter++; - }); + })); input.dispose(); assert.strictEqual(counter, 1); @@ -116,7 +116,7 @@ suite('EditorInput', () => { test('untyped matches', () => { const testInputID = 'untypedMatches'; const testInputResource = URI.file('/fake'); - const testInput = new TestEditorInput(testInputResource, testInputID); + const testInput = disposables.add(new TestEditorInput(testInputResource, testInputID)); const testUntypedInput = { resource: testInputResource, options: { override: testInputID } }; const tetUntypedInputWrongResource = { resource: URI.file('/incorrectFake'), options: { override: testInputID } }; const testUntypedInputWrongId = { resource: testInputResource, options: { override: 'wrongId' } }; @@ -126,7 +126,6 @@ suite('EditorInput', () => { assert.ok(!testInput.matches(tetUntypedInputWrongResource)); assert.ok(!testInput.matches(testUntypedInputWrongId)); assert.ok(!testInput.matches(testUntypedInputWrong)); - }); test('Untpyed inputs properly match TextResourceEditorInput', () => { @@ -236,4 +235,6 @@ suite('EditorInput', () => { fileEditorInput1.dispose(); fileEditorInput2.dispose(); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts b/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts index 2548c2db93e..76c2ca3d373 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts @@ -35,6 +35,8 @@ import { ILanguageConfigurationService } from 'vs/editor/common/languages/langua import { TestAccessibilityService } from 'vs/platform/accessibility/test/common/testAccessibilityService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IStorageService } from 'vs/platform/storage/common/storage'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('EditorModel', () => { @@ -61,33 +63,35 @@ suite('EditorModel', () => { instantiationService.stub(IUndoRedoService, undoRedoService); instantiationService.stub(IEditorService, new TestEditorService()); instantiationService.stub(IThemeService, new TestThemeService()); - instantiationService.stub(ILanguageConfigurationService, new TestLanguageConfigurationService()); - instantiationService.stub(IStorageService, new TestStorageService()); + instantiationService.stub(ILanguageConfigurationService, disposables.add(new TestLanguageConfigurationService())); + instantiationService.stub(IStorageService, disposables.add(new TestStorageService())); - return instantiationService.createInstance(ModelService); + return disposables.add(instantiationService.createInstance(ModelService)); } let instantiationService: TestInstantiationService; let languageService: ILanguageService; + const disposables = new DisposableStore(); + setup(() => { - instantiationService = new TestInstantiationService(); + instantiationService = disposables.add(new TestInstantiationService()); languageService = instantiationService.stub(ILanguageService, LanguageService); }); teardown(() => { - instantiationService.dispose(); + disposables.clear(); }); test('basics', async () => { let counter = 0; - const model = new MyEditorModel(); + const model = disposables.add(new MyEditorModel()); - model.onWillDispose(() => { + disposables.add(model.onWillDispose(() => { assert(true); counter++; - }); + })); await model.resolve(); assert.strictEqual(model.isDisposed(), false); @@ -100,11 +104,12 @@ suite('EditorModel', () => { test('BaseTextEditorModel', async () => { const modelService = stubModelService(instantiationService); - const model = new MyTextEditorModel(modelService, languageService, instantiationService.createInstance(LanguageDetectionService), instantiationService.createInstance(TestAccessibilityService)); + const model = disposables.add(new MyTextEditorModel(modelService, languageService, disposables.add(instantiationService.createInstance(LanguageDetectionService)), instantiationService.createInstance(TestAccessibilityService))); await model.resolve(); - model.testCreateTextEditorModel(createTextBufferFactory('foo'), null!, Mimes.text); + disposables.add(model.testCreateTextEditorModel(createTextBufferFactory('foo'), null!, Mimes.text)); assert.strictEqual(model.isResolved(), true); - model.dispose(); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts b/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts index 8baa06d4b3d..380a2ad5ad0 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts @@ -19,16 +19,16 @@ import { URI } from 'vs/base/common/uri'; import { EditorPaneDescriptor, EditorPaneRegistry } from 'vs/workbench/browser/editor'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IEditorModel } from 'vs/platform/editor/common/editor'; -import { DisposableStore, dispose } from 'vs/base/common/lifecycle'; -import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { TestStorageService, TestWorkspaceTrustManagementService } from 'vs/workbench/test/common/workbenchTestServices'; import { extUri } from 'vs/base/common/resources'; import { EditorService } from 'vs/workbench/services/editor/browser/editorService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { TestWorkspaceTrustManagementService } from 'vs/workbench/services/workspaces/test/common/testWorkspaceTrustService'; import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; const NullThemeService = new TestThemeService(); @@ -37,8 +37,11 @@ const editorInputRegistry: IEditorFactoryRegistry = Registry.as(EditorExtensions class TestEditor extends EditorPane { - constructor(@ITelemetryService telemetryService: ITelemetryService) { - super('TestEditor', NullTelemetryService, NullThemeService, new TestStorageService()); + constructor() { + const disposables = new DisposableStore(); + super('TestEditor', NullTelemetryService, NullThemeService, disposables.add(new TestStorageService())); + + this._register(disposables); } override getId(): string { return 'testEditor'; } @@ -46,10 +49,13 @@ class TestEditor extends EditorPane { protected createEditor(): any { } } -export class OtherTestEditor extends EditorPane { +class OtherTestEditor extends EditorPane { - constructor(@ITelemetryService telemetryService: ITelemetryService) { - super('testOtherEditor', NullTelemetryService, NullThemeService, new TestStorageService()); + constructor() { + const disposables = new DisposableStore(); + super('testOtherEditor', NullTelemetryService, NullThemeService, disposables.add(new TestStorageService())); + + this._register(disposables); } override getId(): string { return 'testOtherEditor'; } @@ -106,9 +112,15 @@ class TestResourceEditorInput extends TextResourceEditorInput { } suite('EditorPane', () => { + const disposables = new DisposableStore(); + + teardown(() => { + disposables.clear(); + }); + test('EditorPane API', async () => { - const editor = new TestEditor(NullTelemetryService); - const input = new OtherTestInput(); + const editor = new TestEditor(); + const input = disposables.add(new OtherTestInput()); const options = {}; assert(!editor.isVisible()); @@ -120,9 +132,6 @@ suite('EditorPane', () => { editor.setVisible(true, group); assert(editor.isVisible()); assert.strictEqual(editor.group, group); - input.onWillDispose(() => { - assert(false); - }); editor.dispose(); editor.clearInput(); editor.setVisible(false, group); @@ -144,57 +153,46 @@ suite('EditorPane', () => { const oldEditorsCnt = editorRegistry.getEditorPanes().length; const oldInputCnt = editorRegistry.getEditors().length; - const dispose1 = editorRegistry.registerEditorPane(editorDescriptor1, [new SyncDescriptor(TestInput)]); - const dispose2 = editorRegistry.registerEditorPane(editorDescriptor2, [new SyncDescriptor(TestInput), new SyncDescriptor(OtherTestInput)]); + disposables.add(editorRegistry.registerEditorPane(editorDescriptor1, [new SyncDescriptor(TestInput)])); + disposables.add(editorRegistry.registerEditorPane(editorDescriptor2, [new SyncDescriptor(TestInput), new SyncDescriptor(OtherTestInput)])); assert.strictEqual(editorRegistry.getEditorPanes().length, oldEditorsCnt + 2); assert.strictEqual(editorRegistry.getEditors().length, oldInputCnt + 3); - assert.strictEqual(editorRegistry.getEditorPane(new TestInput()), editorDescriptor2); - assert.strictEqual(editorRegistry.getEditorPane(new OtherTestInput()), editorDescriptor2); + assert.strictEqual(editorRegistry.getEditorPane(disposables.add(new TestInput())), editorDescriptor2); + assert.strictEqual(editorRegistry.getEditorPane(disposables.add(new OtherTestInput())), editorDescriptor2); assert.strictEqual(editorRegistry.getEditorPaneByType('id1'), editorDescriptor1); assert.strictEqual(editorRegistry.getEditorPaneByType('id2'), editorDescriptor2); assert(!editorRegistry.getEditorPaneByType('id3')); - - dispose([dispose1, dispose2]); }); test('Editor Pane Lookup favors specific class over superclass (match on specific class)', function () { const d1 = EditorPaneDescriptor.create(TestEditor, 'id1', 'name'); - const disposables = new DisposableStore(); - disposables.add(registerTestResourceEditor()); disposables.add(editorRegistry.registerEditorPane(d1, [new SyncDescriptor(TestResourceEditorInput)])); const inst = workbenchInstantiationService(undefined, disposables); - const editor = editorRegistry.getEditorPane(inst.createInstance(TestResourceEditorInput, URI.file('/fake'), 'fake', '', undefined, undefined))!.instantiate(inst); + const editor = disposables.add(editorRegistry.getEditorPane(disposables.add(inst.createInstance(TestResourceEditorInput, URI.file('/fake'), 'fake', '', undefined, undefined)))!.instantiate(inst)); assert.strictEqual(editor.getId(), 'testEditor'); - const otherEditor = editorRegistry.getEditorPane(inst.createInstance(TextResourceEditorInput, URI.file('/fake'), 'fake', '', undefined, undefined))!.instantiate(inst); + const otherEditor = disposables.add(editorRegistry.getEditorPane(disposables.add(inst.createInstance(TextResourceEditorInput, URI.file('/fake'), 'fake', '', undefined, undefined)))!.instantiate(inst)); assert.strictEqual(otherEditor.getId(), 'workbench.editors.textResourceEditor'); - - disposables.dispose(); }); test('Editor Pane Lookup favors specific class over superclass (match on super class)', function () { - const disposables = new DisposableStore(); - const inst = workbenchInstantiationService(undefined, disposables); disposables.add(registerTestResourceEditor()); - const editor = editorRegistry.getEditorPane(inst.createInstance(TestResourceEditorInput, URI.file('/fake'), 'fake', '', undefined, undefined))!.instantiate(inst); + const editor = disposables.add(editorRegistry.getEditorPane(disposables.add(inst.createInstance(TestResourceEditorInput, URI.file('/fake'), 'fake', '', undefined, undefined)))!.instantiate(inst)); assert.strictEqual('workbench.editors.textResourceEditor', editor.getId()); - - disposables.dispose(); }); test('Editor Input Serializer', function () { - const disposables = new DisposableStore(); - const testInput = new TestEditorInput(URI.file('/fake'), 'testTypeId'); + const testInput = disposables.add(new TestEditorInput(URI.file('/fake'), 'testTypeId')); workbenchInstantiationService(undefined, disposables).invokeFunction(accessor => editorInputRegistry.start(accessor)); disposables.add(editorInputRegistry.registerEditorSerializer(testInput.typeId, TestInputSerializer)); @@ -206,8 +204,6 @@ suite('EditorPane', () => { // throws when registering serializer for same type assert.throws(() => editorInputRegistry.registerEditorSerializer(testInput.typeId, TestInputSerializer)); - - disposables.dispose(); }); test('EditorMemento - basics', function () { @@ -228,7 +224,7 @@ suite('EditorPane', () => { } const rawMemento = Object.create(null); - let memento = new EditorMemento('id', 'key', rawMemento, 3, editorGroupService, configurationService); + let memento = disposables.add(new EditorMemento('id', 'key', rawMemento, 3, editorGroupService, configurationService)); let res = memento.loadEditorState(testGroup0, URI.file('/A')); assert.ok(!res); @@ -263,7 +259,7 @@ suite('EditorPane', () => { memento.saveState(); - memento = new EditorMemento('id', 'key', rawMemento, 3, editorGroupService, configurationService); + memento = disposables.add(new EditorMemento('id', 'key', rawMemento, 3, editorGroupService, configurationService)); assert.ok(memento.loadEditorState(testGroup0, URI.file('/C'))); assert.ok(memento.loadEditorState(testGroup0, URI.file('/D'))); assert.ok(memento.loadEditorState(testGroup0, URI.file('/E'))); @@ -289,7 +285,7 @@ suite('EditorPane', () => { interface TestViewState { line: number } const rawMemento = Object.create(null); - const memento = new EditorMemento('id', 'key', rawMemento, 3, editorGroupService, configurationService); + const memento = disposables.add(new EditorMemento('id', 'key', rawMemento, 3, editorGroupService, configurationService)); memento.saveEditorState(testGroup0, URI.file('/some/folder/file-1.txt'), { line: 1 }); memento.saveEditorState(testGroup0, URI.file('/some/folder/file-2.txt'), { line: 2 }); @@ -332,9 +328,9 @@ suite('EditorPane', () => { } const rawMemento = Object.create(null); - const memento = new EditorMemento('id', 'key', rawMemento, 3, new TestEditorGroupsService(), new TestTextResourceConfigurationService()); + const memento = disposables.add(new EditorMemento('id', 'key', rawMemento, 3, new TestEditorGroupsService(), new TestTextResourceConfigurationService())); - const testInputA = new TestEditorInput(URI.file('/A')); + const testInputA = disposables.add(new TestEditorInput(URI.file('/A'))); let res = memento.loadEditorState(testGroup0, testInputA); assert.ok(!res); @@ -370,9 +366,9 @@ suite('EditorPane', () => { } const rawMemento = Object.create(null); - const memento = new EditorMemento('id', 'key', rawMemento, 3, new TestEditorGroupsService(), new TestTextResourceConfigurationService()); + const memento = disposables.add(new EditorMemento('id', 'key', rawMemento, 3, new TestEditorGroupsService(), new TestTextResourceConfigurationService())); - const testInputA = new TestEditorInput(URI.file('/A')); + const testInputA = disposables.add(new TestEditorInput(URI.file('/A'))); let res = memento.loadEditorState(testGroup0, testInputA); assert.ok(!res); @@ -388,7 +384,7 @@ suite('EditorPane', () => { res = memento.loadEditorState(testGroup0, testInputA); assert.ok(res); - const testInputB = new TestEditorInput(URI.file('/B')); + const testInputB = disposables.add(new TestEditorInput(URI.file('/B'))); res = memento.loadEditorState(testGroup0, testInputB); assert.ok(!res); @@ -422,7 +418,7 @@ suite('EditorPane', () => { interface TestViewState { line: number } const rawMemento = Object.create(null); - const memento = new EditorMemento('id', 'key', rawMemento, 3, editorGroupService, configurationService); + const memento = disposables.add(new EditorMemento('id', 'key', rawMemento, 3, editorGroupService, configurationService)); const resource = URI.file('/some/folder/file-1.txt'); memento.saveEditorState(testGroup0, resource, { line: 1 }); @@ -459,7 +455,7 @@ suite('EditorPane', () => { class TrustRequiredTestEditor extends EditorPane { constructor(@ITelemetryService telemetryService: ITelemetryService) { - super('TestEditor', NullTelemetryService, NullThemeService, new TestStorageService()); + super('TestEditor', NullTelemetryService, NullThemeService, disposables.add(new TestStorageService())); } override getId(): string { return 'trustRequiredTestEditor'; } @@ -484,17 +480,15 @@ suite('EditorPane', () => { } } - const disposables = new DisposableStore(); - const instantiationService = workbenchInstantiationService(undefined, disposables); - const workspaceTrustService = instantiationService.createInstance(TestWorkspaceTrustManagementService); + const workspaceTrustService = disposables.add(instantiationService.createInstance(TestWorkspaceTrustManagementService)); instantiationService.stub(IWorkspaceTrustManagementService, workspaceTrustService); workspaceTrustService.setWorkspaceTrust(false); const editorPart = await createEditorPart(instantiationService, disposables); instantiationService.stub(IEditorGroupsService, editorPart); - const editorService = instantiationService.createInstance(EditorService); + const editorService = disposables.add(instantiationService.createInstance(EditorService)); instantiationService.stub(IEditorService, editorService); const group = editorPart.activeGroup; @@ -502,7 +496,7 @@ suite('EditorPane', () => { const editorDescriptor = EditorPaneDescriptor.create(TrustRequiredTestEditor, 'id1', 'name'); disposables.add(editorRegistry.registerEditorPane(editorDescriptor, [new SyncDescriptor(TrustRequiredTestInput)])); - const testInput = new TrustRequiredTestInput(); + const testInput = disposables.add(new TrustRequiredTestInput()); await group.openEditor(testInput); assert.strictEqual(group.activeEditorPane?.getId(), WorkspaceTrustRequiredPlaceholderEditor.ID); @@ -520,6 +514,8 @@ suite('EditorPane', () => { workspaceTrustService.setWorkspaceTrust(false); assert.strictEqual(await getEditorPaneIdAsync(), WorkspaceTrustRequiredPlaceholderEditor.ID); - dispose(disposables); + await group.closeAllEditors(); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/test/browser/parts/editor/resourceEditorInput.test.ts b/src/vs/workbench/test/browser/parts/editor/resourceEditorInput.test.ts index 2fd994d4639..f44f395fac6 100644 --- a/src/vs/workbench/test/browser/parts/editor/resourceEditorInput.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/resourceEditorInput.test.ts @@ -13,10 +13,11 @@ import { IFileService } from 'vs/platform/files/common/files'; import { EditorInputCapabilities, Verbosity } from 'vs/workbench/common/editor'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('ResourceEditorInput', () => { - let disposables: DisposableStore; + const disposables = new DisposableStore(); let instantiationService: IInstantiationService; class TestResourceEditorInput extends AbstractResourceEditorInput { @@ -34,18 +35,17 @@ suite('ResourceEditorInput', () => { } setup(() => { - disposables = new DisposableStore(); instantiationService = workbenchInstantiationService(undefined, disposables); }); teardown(() => { - disposables.dispose(); + disposables.clear(); }); test('basics', async () => { const resource = URI.from({ scheme: 'testResource', path: 'thePath/of/the/resource.txt' }); - const input = instantiationService.createInstance(TestResourceEditorInput, resource); + const input = disposables.add(instantiationService.createInstance(TestResourceEditorInput, resource)); assert.ok(input.getName().length > 0); @@ -61,4 +61,6 @@ suite('ResourceEditorInput', () => { assert.strictEqual(input.isReadonly(), false); assert.strictEqual(input.hasCapability(EditorInputCapabilities.Untitled), true); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/test/browser/parts/editor/sideBySideEditorInput.test.ts b/src/vs/workbench/test/browser/parts/editor/sideBySideEditorInput.test.ts index 9ace2cbba4c..cbd6264885f 100644 --- a/src/vs/workbench/test/browser/parts/editor/sideBySideEditorInput.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/sideBySideEditorInput.test.ts @@ -6,6 +6,7 @@ import * as assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { EditorResourceAccessor, IResourceSideBySideEditorInput, isResourceSideBySideEditorInput, isSideBySideEditorInput, IUntypedEditorInput } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; @@ -13,14 +14,10 @@ import { TestFileEditorInput, workbenchInstantiationService } from 'vs/workbench suite('SideBySideEditorInput', () => { - let disposables: DisposableStore; - - setup(() => { - disposables = new DisposableStore(); - }); + const disposables = new DisposableStore(); teardown(() => { - disposables.dispose(); + disposables.clear(); }); class MyEditorInput extends EditorInput { @@ -62,19 +59,19 @@ suite('SideBySideEditorInput', () => { const instantiationService = workbenchInstantiationService(undefined, disposables); let counter = 0; - const input = new MyEditorInput(URI.file('/fake')); - input.onWillDispose(() => { + const input = disposables.add(new MyEditorInput(URI.file('/fake'))); + disposables.add(input.onWillDispose(() => { assert(true); counter++; - }); + })); - const otherInput = new MyEditorInput(URI.file('/fake2')); - otherInput.onWillDispose(() => { + const otherInput = disposables.add(new MyEditorInput(URI.file('/fake2'))); + disposables.add(otherInput.onWillDispose(() => { assert(true); counter++; - }); + })); - const sideBySideInput = instantiationService.createInstance(SideBySideEditorInput, 'name', 'description', input, otherInput); + const sideBySideInput = disposables.add(instantiationService.createInstance(SideBySideEditorInput, 'name', 'description', input, otherInput)); assert.strictEqual(sideBySideInput.getName(), 'name'); assert.strictEqual(sideBySideInput.getDescription(), 'description'); @@ -89,7 +86,7 @@ suite('SideBySideEditorInput', () => { sideBySideInput.dispose(); assert.strictEqual(counter, 0); - const sideBySideInputSame = instantiationService.createInstance(SideBySideEditorInput, undefined, undefined, input, input); + const sideBySideInputSame = disposables.add(instantiationService.createInstance(SideBySideEditorInput, undefined, undefined, input, input)); assert.strictEqual(sideBySideInputSame.getName(), input.getName()); assert.strictEqual(sideBySideInputSame.getDescription(), input.getDescription()); assert.strictEqual(sideBySideInputSame.getTitle(), input.getTitle()); @@ -99,21 +96,21 @@ suite('SideBySideEditorInput', () => { test('events dispatching', () => { const instantiationService = workbenchInstantiationService(undefined, disposables); - const input = new MyEditorInput(); - const otherInput = new MyEditorInput(); + const input = disposables.add(new MyEditorInput()); + const otherInput = disposables.add(new MyEditorInput()); - const sideBySideInut = instantiationService.createInstance(SideBySideEditorInput, 'name', 'description', otherInput, input); + const sideBySideInut = disposables.add(instantiationService.createInstance(SideBySideEditorInput, 'name', 'description', otherInput, input)); assert.ok(isSideBySideEditorInput(sideBySideInut)); let capabilitiesChangeCounter = 0; - sideBySideInut.onDidChangeCapabilities(() => capabilitiesChangeCounter++); + disposables.add(sideBySideInut.onDidChangeCapabilities(() => capabilitiesChangeCounter++)); let dirtyChangeCounter = 0; - sideBySideInut.onDidChangeDirty(() => dirtyChangeCounter++); + disposables.add(sideBySideInut.onDidChangeDirty(() => dirtyChangeCounter++)); let labelChangeCounter = 0; - sideBySideInut.onDidChangeLabel(() => labelChangeCounter++); + disposables.add(sideBySideInut.onDidChangeLabel(() => labelChangeCounter++)); input.fireCapabilitiesChangeEvent(); assert.strictEqual(capabilitiesChangeCounter, 1); @@ -133,10 +130,10 @@ suite('SideBySideEditorInput', () => { test('toUntyped', () => { const instantiationService = workbenchInstantiationService(undefined, disposables); - const primaryInput = new MyEditorInput(URI.file('/fake')); - const secondaryInput = new MyEditorInput(URI.file('/fake2')); + const primaryInput = disposables.add(new MyEditorInput(URI.file('/fake'))); + const secondaryInput = disposables.add(new MyEditorInput(URI.file('/fake2'))); - const sideBySideInput = instantiationService.createInstance(SideBySideEditorInput, 'Side By Side Test', undefined, secondaryInput, primaryInput); + const sideBySideInput = disposables.add(instantiationService.createInstance(SideBySideEditorInput, 'Side By Side Test', undefined, secondaryInput, primaryInput)); const untypedSideBySideInput = sideBySideInput.toUntyped(); assert.ok(isResourceSideBySideEditorInput(untypedSideBySideInput)); @@ -145,9 +142,9 @@ suite('SideBySideEditorInput', () => { test('untyped matches', () => { const instantiationService = workbenchInstantiationService(undefined, disposables); - const primaryInput = new TestFileEditorInput(URI.file('/fake'), 'primaryId'); - const secondaryInput = new TestFileEditorInput(URI.file('/fake2'), 'secondaryId'); - const sideBySideInput = instantiationService.createInstance(SideBySideEditorInput, 'Side By Side Test', undefined, secondaryInput, primaryInput); + const primaryInput = disposables.add(new TestFileEditorInput(URI.file('/fake'), 'primaryId')); + const secondaryInput = disposables.add(new TestFileEditorInput(URI.file('/fake2'), 'secondaryId')); + const sideBySideInput = disposables.add(instantiationService.createInstance(SideBySideEditorInput, 'Side By Side Test', undefined, secondaryInput, primaryInput)); const primaryUntypedInput = { resource: URI.file('/fake'), options: { override: 'primaryId' } }; const secondaryUntypedInput = { resource: URI.file('/fake2'), options: { override: 'secondaryId' } }; @@ -167,4 +164,6 @@ suite('SideBySideEditorInput', () => { assert.ok(!sideBySideInput.matches(sideBySideUntyped3)); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/test/browser/parts/editor/textEditorPane.test.ts b/src/vs/workbench/test/browser/parts/editor/textEditorPane.test.ts index f678591223a..4574694f503 100644 --- a/src/vs/workbench/test/browser/parts/editor/textEditorPane.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/textEditorPane.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { toResource } from 'vs/base/test/common/utils'; +import { ensureNoDisposablesAreLeakedInTestSuite, toResource } from 'vs/base/test/common/utils'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { workbenchInstantiationService, TestServiceAccessor, registerTestFileEditor, createEditorPart, TestTextFileEditor } from 'vs/workbench/test/browser/workbenchTestServices'; import { IResolvedTextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; @@ -35,7 +35,7 @@ suite('TextEditorPane', () => { const part = await createEditorPart(instantiationService, disposables); instantiationService.stub(IEditorGroupsService, part); - const editorService = instantiationService.createInstance(EditorService); + const editorService = disposables.add(instantiationService.createInstance(EditorService)); instantiationService.stub(IEditorService, editorService); return instantiationService.createInstance(TestServiceAccessor); @@ -50,16 +50,16 @@ suite('TextEditorPane', () => { assert.ok(pane && isEditorPaneWithSelection(pane)); const onDidFireSelectionEventOfEditType = new DeferredPromise(); - pane.onDidChangeSelection(e => { + disposables.add(pane.onDidChangeSelection(e => { if (e.reason === EditorPaneSelectionChangeReason.EDIT) { onDidFireSelectionEventOfEditType.complete(e); } - }); + })); // Changing model reports selection change // of EDIT kind - const model = await accessor.textFileService.files.resolve(resource) as IResolvedTextFileEditorModel; + const model = disposables.add(await accessor.textFileService.files.resolve(resource) as IResolvedTextFileEditorModel); model.textEditorModel.setValue('Hello World'); const event = await onDidFireSelectionEventOfEditType.p; @@ -83,6 +83,9 @@ suite('TextEditorPane', () => { const newSelection = pane.getSelection(); assert.ok(newSelection); assert.strictEqual(newSelection.compare(selection), EditorPaneSelectionCompareResult.IDENTICAL); + + await model.revert(); + await pane.group?.closeAllEditors(); }); test('TextEditorPaneSelection', function () { @@ -96,4 +99,6 @@ suite('TextEditorPane', () => { assert.strictEqual(sel1.compare(sel3), EditorPaneSelectionCompareResult.DIFFERENT); assert.strictEqual(sel1.compare(sel4), EditorPaneSelectionCompareResult.DIFFERENT); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/test/browser/parts/editor/textResourceEditorInput.test.ts b/src/vs/workbench/test/browser/parts/editor/textResourceEditorInput.test.ts index d2c732e94db..029ad438984 100644 --- a/src/vs/workbench/test/browser/parts/editor/textResourceEditorInput.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/textResourceEditorInput.test.ts @@ -12,30 +12,31 @@ import { workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench import { snapshotToString } from 'vs/workbench/services/textfile/common/textfiles'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('TextResourceEditorInput', () => { - let disposables: DisposableStore; + const disposables = new DisposableStore(); + let instantiationService: IInstantiationService; let accessor: TestServiceAccessor; setup(() => { - disposables = new DisposableStore(); instantiationService = workbenchInstantiationService(undefined, disposables); accessor = instantiationService.createInstance(TestServiceAccessor); }); teardown(() => { - disposables.dispose(); + disposables.clear(); }); test('basics', async () => { const resource = URI.from({ scheme: 'inmemory', authority: null!, path: 'thePath' }); accessor.modelService.createModel('function test() {}', accessor.languageService.createById(PLAINTEXT_LANGUAGE_ID), resource); - const input = instantiationService.createInstance(TextResourceEditorInput, resource, 'The Name', 'The Description', undefined, undefined); + const input = disposables.add(instantiationService.createInstance(TextResourceEditorInput, resource, 'The Name', 'The Description', undefined, undefined)); - const model = await input.resolve(); + const model = disposables.add(await input.resolve()); assert.ok(model); assert.strictEqual(snapshotToString(((model as TextResourceEditorModel).createSnapshot()!)), 'function test() {}'); @@ -49,16 +50,16 @@ suite('TextResourceEditorInput', () => { const resource = URI.from({ scheme: 'inmemory', authority: null!, path: 'thePath' }); accessor.modelService.createModel('function test() {}', accessor.languageService.createById(PLAINTEXT_LANGUAGE_ID), resource); - const input = instantiationService.createInstance(TextResourceEditorInput, resource, 'The Name', 'The Description', 'resource-input-test', undefined); + const input = disposables.add(instantiationService.createInstance(TextResourceEditorInput, resource, 'The Name', 'The Description', 'resource-input-test', undefined)); - const model = await input.resolve(); + const model = disposables.add(await input.resolve()); assert.ok(model); assert.strictEqual(model.textEditorModel?.getLanguageId(), 'resource-input-test'); input.setLanguageId('text'); assert.strictEqual(model.textEditorModel?.getLanguageId(), PLAINTEXT_LANGUAGE_ID); - await input.resolve(); + disposables.add(await input.resolve()); assert.strictEqual(model.textEditorModel?.getLanguageId(), PLAINTEXT_LANGUAGE_ID); registration.dispose(); }); @@ -71,10 +72,10 @@ suite('TextResourceEditorInput', () => { const resource = URI.from({ scheme: 'inmemory', authority: null!, path: 'thePath' }); accessor.modelService.createModel('function test() {}', accessor.languageService.createById(PLAINTEXT_LANGUAGE_ID), resource); - const input = instantiationService.createInstance(TextResourceEditorInput, resource, 'The Name', 'The Description', undefined, undefined); + const input = disposables.add(instantiationService.createInstance(TextResourceEditorInput, resource, 'The Name', 'The Description', undefined, undefined)); input.setPreferredLanguageId('resource-input-test'); - const model = await input.resolve(); + const model = disposables.add(await input.resolve()); assert.ok(model); assert.strictEqual(model.textEditorModel?.getLanguageId(), 'resource-input-test'); registration.dispose(); @@ -84,16 +85,16 @@ suite('TextResourceEditorInput', () => { const resource = URI.from({ scheme: 'inmemory', authority: null!, path: 'thePath' }); accessor.modelService.createModel('function test() {}', accessor.languageService.createById(PLAINTEXT_LANGUAGE_ID), resource); - const input = instantiationService.createInstance(TextResourceEditorInput, resource, 'The Name', 'The Description', undefined, 'My Resource Input Contents'); + const input = disposables.add(instantiationService.createInstance(TextResourceEditorInput, resource, 'The Name', 'The Description', undefined, 'My Resource Input Contents')); - const model = await input.resolve(); + const model = disposables.add(await input.resolve()); assert.ok(model); assert.strictEqual(model.textEditorModel?.getValue(), 'My Resource Input Contents'); model.textEditorModel.setValue('Some other contents'); assert.strictEqual(model.textEditorModel?.getValue(), 'Some other contents'); - await input.resolve(); + disposables.add(await input.resolve()); assert.strictEqual(model.textEditorModel?.getValue(), 'Some other contents'); // preferred contents only used once }); @@ -101,17 +102,19 @@ suite('TextResourceEditorInput', () => { const resource = URI.from({ scheme: 'inmemory', authority: null!, path: 'thePath' }); accessor.modelService.createModel('function test() {}', accessor.languageService.createById(PLAINTEXT_LANGUAGE_ID), resource); - const input = instantiationService.createInstance(TextResourceEditorInput, resource, 'The Name', 'The Description', undefined, undefined); + const input = disposables.add(instantiationService.createInstance(TextResourceEditorInput, resource, 'The Name', 'The Description', undefined, undefined)); input.setPreferredContents('My Resource Input Contents'); - const model = await input.resolve(); + const model = disposables.add(await input.resolve()); assert.ok(model); assert.strictEqual(model.textEditorModel?.getValue(), 'My Resource Input Contents'); model.textEditorModel.setValue('Some other contents'); assert.strictEqual(model.textEditorModel?.getValue(), 'Some other contents'); - await input.resolve(); + disposables.add(await input.resolve()); assert.strictEqual(model.textEditorModel?.getValue(), 'Some other contents'); // preferred contents only used once }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/test/browser/parts/statusbar/statusbarModel.test.ts b/src/vs/workbench/test/browser/parts/statusbar/statusbarModel.test.ts index c42d46e4d88..3f0f767be42 100644 --- a/src/vs/workbench/test/browser/parts/statusbar/statusbarModel.test.ts +++ b/src/vs/workbench/test/browser/parts/statusbar/statusbarModel.test.ts @@ -7,12 +7,20 @@ import * as assert from 'assert'; import { StatusbarViewModel } from 'vs/workbench/browser/parts/statusbar/statusbarModel'; import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; import { StatusbarAlignment } from 'vs/workbench/services/statusbar/browser/statusbar'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { DisposableStore } from 'vs/base/common/lifecycle'; suite('Workbench status bar model', () => { + const disposables = new DisposableStore(); + + teardown(() => { + disposables.clear(); + }); + test('basics', () => { const container = document.createElement('div'); - const model = new StatusbarViewModel(new TestStorageService()); + const model = disposables.add(new StatusbarViewModel(disposables.add(new TestStorageService()))); assert.strictEqual(model.entries.length, 0); @@ -44,9 +52,9 @@ suite('Workbench status bar model', () => { assert.ok(model.findEntry(container)); let didChangeEntryVisibility: { id: string; visible: boolean } = { id: '', visible: false }; - model.onDidChangeEntryVisibility(e => { + disposables.add(model.onDidChangeEntryVisibility(e => { didChangeEntryVisibility = e; - }); + })); assert.strictEqual(model.isHidden('1'), false); model.hide('1'); @@ -72,7 +80,7 @@ suite('Workbench status bar model', () => { test('secondary priority used when primary is same', () => { const container = document.createElement('div'); - const model = new StatusbarViewModel(new TestStorageService()); + const model = disposables.add(new StatusbarViewModel(disposables.add(new TestStorageService()))); assert.strictEqual(model.entries.length, 0); @@ -88,7 +96,7 @@ suite('Workbench status bar model', () => { test('insertion order preserved when priorites are the same', () => { const container = document.createElement('div'); - const model = new StatusbarViewModel(new TestStorageService()); + const model = disposables.add(new StatusbarViewModel(disposables.add(new TestStorageService()))); assert.strictEqual(model.entries.length, 0); @@ -104,7 +112,7 @@ suite('Workbench status bar model', () => { test('entry with reference to other entry (existing)', () => { const container = document.createElement('div'); - const model = new StatusbarViewModel(new TestStorageService()); + const model = disposables.add(new StatusbarViewModel(disposables.add(new TestStorageService()))); // Existing reference, Alignment: left model.add({ id: 'a', alignment: StatusbarAlignment.LEFT, name: '1', priority: { primary: 2, secondary: 1 }, container, labelContainer: container, hasCommand: false }); @@ -134,7 +142,7 @@ suite('Workbench status bar model', () => { test('entry with reference to other entry (nonexistent)', () => { const container = document.createElement('div'); - const model = new StatusbarViewModel(new TestStorageService()); + const model = disposables.add(new StatusbarViewModel(disposables.add(new TestStorageService()))); // Nonexistent reference, Alignment: left model.add({ id: 'a', alignment: StatusbarAlignment.LEFT, name: '1', priority: { primary: 2, secondary: 1 }, container, labelContainer: container, hasCommand: false }); @@ -164,7 +172,7 @@ suite('Workbench status bar model', () => { test('entry with reference to other entry resorts based on other entry being there or not', () => { const container = document.createElement('div'); - const model = new StatusbarViewModel(new TestStorageService()); + const model = disposables.add(new StatusbarViewModel(disposables.add(new TestStorageService()))); model.add({ id: 'a', alignment: StatusbarAlignment.LEFT, name: '1', priority: { primary: 2, secondary: 1 }, container, labelContainer: container, hasCommand: false }); model.add({ id: 'b', alignment: StatusbarAlignment.LEFT, name: '2', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false }); @@ -197,7 +205,7 @@ suite('Workbench status bar model', () => { test('entry with reference to other entry but different alignment does not explode', () => { const container = document.createElement('div'); - const model = new StatusbarViewModel(new TestStorageService()); + const model = disposables.add(new StatusbarViewModel(disposables.add(new TestStorageService()))); model.add({ id: '1-left', alignment: StatusbarAlignment.LEFT, name: '1-left', priority: { primary: 2, secondary: 1 }, container, labelContainer: container, hasCommand: false }); model.add({ id: '2-left', alignment: StatusbarAlignment.LEFT, name: '2-left', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false }); @@ -223,4 +231,6 @@ suite('Workbench status bar model', () => { assert.strictEqual(model.getEntries(StatusbarAlignment.LEFT).length, 2); assert.strictEqual(model.getEntries(StatusbarAlignment.RIGHT).length, 3); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/test/browser/viewlet.test.ts b/src/vs/workbench/test/browser/viewlet.test.ts index da133da18e2..eefc87cb2d6 100644 --- a/src/vs/workbench/test/browser/viewlet.test.ts +++ b/src/vs/workbench/test/browser/viewlet.test.ts @@ -8,6 +8,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { PaneCompositeDescriptor, Extensions, PaneCompositeRegistry, PaneComposite } from 'vs/workbench/browser/panecomposite'; import { isFunction } from 'vs/base/common/types'; import { IBoundarySashes } from 'vs/base/browser/ui/sash/sash'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('Viewlets', () => { @@ -58,4 +59,6 @@ suite('Viewlets', () => { assert(d === Registry.as(Extensions.Viewlets).getPaneComposite('reg-test-id')); assert.strictEqual(oldCount + 1, Registry.as(Extensions.Viewlets).getPaneComposites().length); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/test/browser/webview.test.ts b/src/vs/workbench/test/browser/webview.test.ts index 2eac5352c0a..484be00a657 100644 --- a/src/vs/workbench/test/browser/webview.test.ts +++ b/src/vs/workbench/test/browser/webview.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { parentOriginHash } from 'vs/workbench/browser/iframe'; +import { parentOriginHash } from 'vs/base/browser/iframe'; suite('parentOriginHash', () => { diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 1340c38c2de..3fa67419252 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -99,7 +99,7 @@ import { IInputBox, IInputOptions, IPickOptions, IQuickInputButton, IQuickInputS import { QuickInputService } from 'vs/workbench/services/quickinput/browser/quickInputService'; import { IListService } from 'vs/platform/list/browser/listService'; import { win32, posix } from 'vs/base/common/path'; -import { TestContextService, TestStorageService, TestTextResourcePropertiesService, TestExtensionService, TestProductService, createFileStat, TestLoggerService } from 'vs/workbench/test/common/workbenchTestServices'; +import { TestContextService, TestStorageService, TestTextResourcePropertiesService, TestExtensionService, TestProductService, createFileStat, TestLoggerService, TestWorkspaceTrustManagementService, TestWorkspaceTrustRequestService } from 'vs/workbench/test/common/workbenchTestServices'; import { IViewsService, IView, ViewContainer, ViewContainerLocation } from 'vs/workbench/common/views'; import { IPaneComposite } from 'vs/workbench/common/panecomposite'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; @@ -121,7 +121,6 @@ import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/u import { SideBySideEditor } from 'vs/workbench/browser/parts/editor/sideBySideEditor'; import { IEnterWorkspaceResult, IRecent, IRecentlyOpened, IWorkspaceFolderCreationData, IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; import { IWorkspaceTrustManagementService, IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; -import { TestWorkspaceTrustManagementService, TestWorkspaceTrustRequestService } from 'vs/workbench/services/workspaces/test/common/testWorkspaceTrustService'; import { IExtensionTerminalProfile, IShellLaunchConfig, ITerminalBackend, ITerminalProfile, TerminalIcon, TerminalLocation, TerminalShellType } from 'vs/platform/terminal/common/terminal'; import { ICreateTerminalOptions, IDeserializedTerminalEditorInput, ITerminalEditorService, ITerminalGroup, ITerminalGroupService, ITerminalInstance, ITerminalInstanceService, TerminalEditorLocation } from 'vs/workbench/contrib/terminal/browser/terminal'; import { assertIsDefined } from 'vs/base/common/types'; @@ -188,14 +187,14 @@ Registry.as(EditorExtensions.EditorFactory).registerFile export class TestTextResourceEditor extends TextResourceEditor { protected override createEditorControl(parent: HTMLElement, configuration: any): void { - this.editorControl = this.instantiationService.createInstance(TestCodeEditor, parent, configuration, {}); + this.editorControl = this._register(this.instantiationService.createInstance(TestCodeEditor, parent, configuration, {})); } } export class TestTextFileEditor extends TextFileEditor { protected override createEditorControl(parent: HTMLElement, configuration: any): void { - this.editorControl = this.instantiationService.createInstance(TestCodeEditor, parent, configuration, { contributions: [] }); + this.editorControl = this._register(this.instantiationService.createInstance(TestCodeEditor, parent, configuration, { contributions: [] })); } setSelection(selection: Selection | undefined, reason: EditorPaneSelectionChangeReason): void { @@ -243,7 +242,7 @@ export function workbenchInstantiationService( }, disposables: Pick = new DisposableStore() ): TestInstantiationService { - const instantiationService = disposables.add(new TestInstantiationService(new ServiceCollection([ILifecycleService, new TestLifecycleService()]))); + const instantiationService = disposables.add(new TestInstantiationService(new ServiceCollection([ILifecycleService, disposables.add(new TestLifecycleService())]))); instantiationService.stub(IEditorWorkerService, new TestEditorWorkerService()); instantiationService.stub(IWorkingCopyService, disposables.add(new TestWorkingCopyService())); @@ -285,15 +284,15 @@ export function workbenchInstantiationService( instantiationService.stub(IThemeService, themeService); instantiationService.stub(ILanguageConfigurationService, disposables.add(new TestLanguageConfigurationService())); instantiationService.stub(IModelService, disposables.add(instantiationService.createInstance(ModelService))); - const fileService = overrides?.fileService ? overrides.fileService(instantiationService) : new TestFileService(); + const fileService = overrides?.fileService ? overrides.fileService(instantiationService) : disposables.add(new TestFileService()); instantiationService.stub(IFileService, fileService); const uriIdentityService = new UriIdentityService(fileService); disposables.add(uriIdentityService); instantiationService.stub(IFilesConfigurationService, disposables.add(new TestFilesConfigurationService(contextKeyService, configService, workspaceContextService, environmentService, uriIdentityService, fileService))); - instantiationService.stub(IUriIdentityService, uriIdentityService); + instantiationService.stub(IUriIdentityService, disposables.add(uriIdentityService)); const userDataProfilesService = instantiationService.stub(IUserDataProfilesService, disposables.add(new UserDataProfilesService(environmentService, fileService, uriIdentityService, new NullLogService()))); instantiationService.stub(IUserDataProfileService, disposables.add(new UserDataProfileService(userDataProfilesService.defaultProfile, userDataProfilesService))); - instantiationService.stub(IWorkingCopyBackupService, overrides?.workingCopyBackupService ? overrides?.workingCopyBackupService(instantiationService) : new TestWorkingCopyBackupService()); + instantiationService.stub(IWorkingCopyBackupService, overrides?.workingCopyBackupService ? overrides?.workingCopyBackupService(instantiationService) : disposables.add(new TestWorkingCopyBackupService())); instantiationService.stub(ITelemetryService, NullTelemetryService); instantiationService.stub(INotificationService, new TestNotificationService()); instantiationService.stub(IUntitledTextEditorService, disposables.add(instantiationService.createInstance(UntitledTextEditorService))); @@ -323,7 +322,8 @@ export function workbenchInstantiationService( const hoverService = instantiationService.stub(IHoverService, instantiationService.createInstance(TestHoverService)); instantiationService.stub(IQuickInputService, disposables.add(new QuickInputService(configService, instantiationService, keybindingService, contextKeyService, themeService, layoutService, hoverService))); instantiationService.stub(IWorkspacesService, new TestWorkspacesService()); - instantiationService.stub(IWorkspaceTrustManagementService, new TestWorkspaceTrustManagementService()); + instantiationService.stub(IWorkspaceTrustManagementService, disposables.add(new TestWorkspaceTrustManagementService())); + instantiationService.stub(IWorkspaceTrustRequestService, disposables.add(new TestWorkspaceTrustRequestService(false))); instantiationService.stub(ITerminalInstanceService, new TestTerminalInstanceService()); instantiationService.stub(IElevatedFileService, new BrowserElevatedFileService()); instantiationService.stub(IRemoteSocketFactoryService, new RemoteSocketFactoryService()); @@ -1195,17 +1195,20 @@ export class InMemoryTestWorkingCopyBackupService extends BrowserWorkingCopyBack discardedBackups: IWorkingCopyIdentifier[]; constructor() { + const disposables = new DisposableStore(); const environmentService = TestEnvironmentService; const logService = new NullLogService(); - const fileService = new FileService(logService); - fileService.registerProvider(Schemas.file, new InMemoryFileSystemProvider()); - fileService.registerProvider(Schemas.vscodeUserData, new InMemoryFileSystemProvider()); + const fileService = disposables.add(new FileService(logService)); + disposables.add(fileService.registerProvider(Schemas.file, disposables.add(new InMemoryFileSystemProvider()))); + disposables.add(fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new InMemoryFileSystemProvider()))); super(new TestContextService(TestWorkspace), environmentService, fileService, logService); this.backupResourceJoiners = []; this.discardBackupJoiners = []; this.discardedBackups = []; + + this._register(disposables); } testGetFileService(): IFileService { @@ -1246,26 +1249,26 @@ export class InMemoryTestWorkingCopyBackupService extends BrowserWorkingCopyBack } } -export class TestLifecycleService implements ILifecycleService { +export class TestLifecycleService extends Disposable implements ILifecycleService { declare readonly _serviceBrand: undefined; phase!: LifecyclePhase; startupKind!: StartupKind; - private readonly _onBeforeShutdown = new Emitter(); + private readonly _onBeforeShutdown = this._register(new Emitter()); get onBeforeShutdown(): Event { return this._onBeforeShutdown.event; } - private readonly _onBeforeShutdownError = new Emitter(); + private readonly _onBeforeShutdownError = this._register(new Emitter()); get onBeforeShutdownError(): Event { return this._onBeforeShutdownError.event; } - private readonly _onShutdownVeto = new Emitter(); + private readonly _onShutdownVeto = this._register(new Emitter()); get onShutdownVeto(): Event { return this._onShutdownVeto.event; } - private readonly _onWillShutdown = new Emitter(); + private readonly _onWillShutdown = this._register(new Emitter()); get onWillShutdown(): Event { return this._onWillShutdown.event; } - private readonly _onDidShutdown = new Emitter(); + private readonly _onDidShutdown = this._register(new Emitter()); get onDidShutdown(): Event { return this._onDidShutdown.event; } async when(): Promise { } @@ -1488,12 +1491,14 @@ export class TestEditorInput extends EditorInput { } export function registerTestEditor(id: string, inputs: SyncDescriptor[], serializerInputId?: string): IDisposable { + const disposables = new DisposableStore(); + class TestEditor extends EditorPane { private _scopedContextKeyService: IContextKeyService; constructor() { - super(id, NullTelemetryService, new TestThemeService(), new TestStorageService()); + super(id, NullTelemetryService, new TestThemeService(), disposables.add(new TestStorageService())); this._scopedContextKeyService = new MockContextKeyService(); } @@ -1512,8 +1517,6 @@ export function registerTestEditor(id: string, inputs: SyncDescriptor(Extensions.EditorPane).registerEditorPane(EditorPaneDescriptor.create(TestEditor, id, 'Test Editor Control'), inputs)); if (serializerInputId) { @@ -2089,3 +2092,22 @@ export class TestWebExtensionsScannerService implements IWebExtensionsScannerSer throw new Error('Method not implemented.'); } } + +export async function workbenchTeardown(instantiationService: IInstantiationService): Promise { + return instantiationService.invokeFunction(async accessor => { + const workingCopyService = accessor.get(IWorkingCopyService); + const editorGroupService = accessor.get(IEditorGroupsService); + + for (const workingCopy of workingCopyService.workingCopies) { + await workingCopy.revert(); + } + + for (const group of editorGroupService.groups) { + await group.closeAllEditors(); + } + + for (const group of editorGroupService.groups) { + editorGroupService.removeGroup(group); + } + }); +} diff --git a/src/vs/workbench/test/common/memento.test.ts b/src/vs/workbench/test/common/memento.test.ts index e523402f4bd..84502b1c321 100644 --- a/src/vs/workbench/test/common/memento.test.ts +++ b/src/vs/workbench/test/common/memento.test.ts @@ -4,20 +4,27 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { StorageScope, IStorageService, StorageTarget } from 'vs/platform/storage/common/storage'; import { Memento } from 'vs/workbench/common/memento'; import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; suite('Memento', () => { + const disposables = new DisposableStore(); let storage: IStorageService; setup(() => { - storage = new TestStorageService(); + storage = disposables.add(new TestStorageService()); Memento.clear(StorageScope.APPLICATION); Memento.clear(StorageScope.PROFILE); Memento.clear(StorageScope.WORKSPACE); }); + teardown(() => { + disposables.clear(); + }); + test('Loading and Saving Memento with Scopes', () => { const myMemento = new Memento('memento.test', storage); @@ -213,7 +220,7 @@ suite('Memento', () => { myMemento.saveMemento(); // Clear - storage = new TestStorageService(); + storage = disposables.add(new TestStorageService()); Memento.clear(StorageScope.PROFILE); Memento.clear(StorageScope.WORKSPACE); @@ -224,4 +231,6 @@ suite('Memento', () => { assert.deepStrictEqual(profileMemento, {}); assert.deepStrictEqual(workspaceMemento, {}); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/test/common/notifications.test.ts b/src/vs/workbench/test/common/notifications.test.ts index bc96b826ea9..651d70c43cc 100644 --- a/src/vs/workbench/test/common/notifications.test.ts +++ b/src/vs/workbench/test/common/notifications.test.ts @@ -11,9 +11,17 @@ import { createErrorWithActions } from 'vs/base/common/errorMessage'; import { NotificationService } from 'vs/workbench/services/notification/common/notificationService'; import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; import { timeout } from 'vs/base/common/async'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { DisposableStore } from 'vs/base/common/lifecycle'; suite('Notifications', () => { + const disposables = new DisposableStore(); + + teardown(() => { + disposables.clear(); + }); + test('Items', () => { // Invalid @@ -25,8 +33,8 @@ suite('Notifications', () => { const item2 = NotificationViewItem.create({ severity: Severity.Error, message: 'Error Message' })!; const item3 = NotificationViewItem.create({ severity: Severity.Info, message: 'Info Message' })!; const item4 = NotificationViewItem.create({ severity: Severity.Error, message: 'Error Message', source: 'Source' })!; - const item5 = NotificationViewItem.create({ severity: Severity.Error, message: 'Error Message', actions: { primary: [new Action('id', 'label')] } })!; - const item6 = NotificationViewItem.create({ severity: Severity.Error, message: 'Error Message', actions: { primary: [new Action('id', 'label')] }, progress: { infinite: true } })!; + const item5 = NotificationViewItem.create({ severity: Severity.Error, message: 'Error Message', actions: { primary: [disposables.add(new Action('id', 'label'))] } })!; + const item6 = NotificationViewItem.create({ severity: Severity.Error, message: 'Error Message', actions: { primary: [disposables.add(new Action('id', 'label'))] }, progress: { infinite: true } })!; assert.strictEqual(item1.equals(item1), true); assert.strictEqual(item2.equals(item2), true); @@ -55,9 +63,9 @@ suite('Notifications', () => { // Events let called = 0; - item1.onDidChangeExpansion(() => { + disposables.add(item1.onDidChangeExpansion(() => { called++; - }); + })); item1.expand(); item1.expand(); @@ -67,11 +75,11 @@ suite('Notifications', () => { assert.strictEqual(called, 2); called = 0; - item1.onDidChangeContent(e => { + disposables.add(item1.onDidChangeContent(e => { if (e.kind === NotificationViewItemContentChangeKind.PROGRESS) { called++; } - }); + })); item1.progress.infinite(); item1.progress.done(); @@ -79,38 +87,38 @@ suite('Notifications', () => { assert.strictEqual(called, 2); called = 0; - item1.onDidChangeContent(e => { + disposables.add(item1.onDidChangeContent(e => { if (e.kind === NotificationViewItemContentChangeKind.MESSAGE) { called++; } - }); + })); item1.updateMessage('message update'); called = 0; - item1.onDidChangeContent(e => { + disposables.add(item1.onDidChangeContent(e => { if (e.kind === NotificationViewItemContentChangeKind.SEVERITY) { called++; } - }); + })); item1.updateSeverity(Severity.Error); called = 0; - item1.onDidChangeContent(e => { + disposables.add(item1.onDidChangeContent(e => { if (e.kind === NotificationViewItemContentChangeKind.ACTIONS) { called++; } - }); + })); - item1.updateActions({ primary: [new Action('id2', 'label')] }); + item1.updateActions({ primary: [disposables.add(new Action('id2', 'label'))] }); assert.strictEqual(called, 1); called = 0; - item1.onDidChangeVisibility(e => { + disposables.add(item1.onDidChangeVisibility(e => { called++; - }); + })); item1.updateVisibility(true); item1.updateVisibility(false); @@ -119,15 +127,15 @@ suite('Notifications', () => { assert.strictEqual(called, 2); called = 0; - item1.onDidClose(() => { + disposables.add(item1.onDidClose(() => { called++; - }); + })); item1.close(); assert.strictEqual(called, 1); // Error with Action - const item7 = NotificationViewItem.create({ severity: Severity.Error, message: createErrorWithActions('Hello Error', [new Action('id', 'label')]) })!; + const item7 = NotificationViewItem.create({ severity: Severity.Error, message: createErrorWithActions('Hello Error', [disposables.add(new Action('id', 'label'))]) })!; assert.strictEqual(item7.actions!.primary!.length, 1); // Filter @@ -142,15 +150,19 @@ suite('Notifications', () => { const item11 = NotificationViewItem.create({ severity: Severity.Warning, message: 'Error Message' }, NotificationsFilter.ERROR)!; assert.strictEqual(item11.priority, NotificationPriority.SILENT); + + for (const item of [item1, item2, item3, item4, item5, item6, itemId1, itemId2, item7, item8, item9, item10, item11]) { + item.close(); + } }); test('Items - does not fire changed when message did not change (content, severity)', async () => { const item1 = NotificationViewItem.create({ severity: Severity.Error, message: 'Error Message' })!; let fired = false; - item1.onDidChangeContent(() => { + disposables.add(item1.onDidChangeContent(() => { fired = true; - }); + })); item1.updateMessage('Error Message'); await timeout(0); @@ -159,22 +171,26 @@ suite('Notifications', () => { item1.updateSeverity(Severity.Error); await timeout(0); assert.ok(!fired, 'Expected onDidChangeContent to not be fired'); + + for (const item of [item1]) { + item.close(); + } }); test('Model', () => { - const model = new NotificationsModel(); + const model = disposables.add(new NotificationsModel()); let lastNotificationEvent!: INotificationChangeEvent; - model.onDidChangeNotification(e => { + disposables.add(model.onDidChangeNotification(e => { lastNotificationEvent = e; - }); + })); let lastStatusMessageEvent!: IStatusMessageChangeEvent; - model.onDidChangeStatusMessage(e => { + disposables.add(model.onDidChangeStatusMessage(e => { lastStatusMessageEvent = e; - }); + })); - const item1: INotification = { severity: Severity.Error, message: 'Error Message', actions: { primary: [new Action('id', 'label')] } }; + const item1: INotification = { severity: Severity.Error, message: 'Error Message', actions: { primary: [disposables.add(new Action('id', 'label'))] } }; const item2: INotification = { severity: Severity.Warning, message: 'Warning Message', source: 'Some Source' }; const item2Duplicate: INotification = { severity: Severity.Warning, message: 'Warning Message', source: 'Some Source' }; const item3: INotification = { severity: Severity.Info, message: 'Info Message' }; @@ -207,7 +223,7 @@ suite('Notifications', () => { assert.strictEqual(lastNotificationEvent.index, 0); assert.strictEqual(lastNotificationEvent.kind, NotificationChangeType.ADD); - model.addNotification(item3); + const item3Handle = model.addNotification(item3); assert.strictEqual(lastNotificationEvent.item.severity, item3.severity); assert.strictEqual(lastNotificationEvent.item.message.linkedText.toString(), item3.message); assert.strictEqual(lastNotificationEvent.index, 0); @@ -216,9 +232,9 @@ suite('Notifications', () => { assert.strictEqual(model.notifications.length, 3); let called = 0; - item1Handle.onDidClose(() => { + disposables.add(item1Handle.onDidClose(() => { called++; - }); + })); item1Handle.close(); assert.strictEqual(called, 1); @@ -228,7 +244,7 @@ suite('Notifications', () => { assert.strictEqual(lastNotificationEvent.index, 2); assert.strictEqual(lastNotificationEvent.kind, NotificationChangeType.REMOVE); - model.addNotification(item2Duplicate); + const item2DuplicateHandle = model.addNotification(item2Duplicate); assert.strictEqual(model.notifications.length, 2); assert.strictEqual(lastNotificationEvent.item.severity, item2Duplicate.severity); assert.strictEqual(lastNotificationEvent.item.message.linkedText.toString(), item2Duplicate.message); @@ -266,22 +282,26 @@ suite('Notifications', () => { disposable3.dispose(); assert.ok(!model.statusMessage); + + item2DuplicateHandle.close(); + item3Handle.close(); }); test('Service', async () => { - const service = new NotificationService(new TestStorageService()); + const service = disposables.add(new NotificationService(disposables.add(new TestStorageService()))); let addNotificationCount = 0; let notification!: INotification; - service.onDidAddNotification(n => { + disposables.add(service.onDidAddNotification(n => { addNotificationCount++; notification = n; - }); + })); service.info('hello there'); assert.strictEqual(addNotificationCount, 1); assert.strictEqual(notification.message, 'hello there'); assert.strictEqual(notification.priority, NotificationPriority.DEFAULT); assert.strictEqual(notification.source, undefined); + service.model.notifications[0].close(); let notificationHandle = service.notify({ message: 'important message', severity: Severity.Warning }); assert.strictEqual(addNotificationCount, 2); @@ -289,10 +309,10 @@ suite('Notifications', () => { assert.strictEqual(notification.severity, Severity.Warning); let removeNotificationCount = 0; - service.onDidRemoveNotification(n => { + disposables.add(service.onDidRemoveNotification(n => { removeNotificationCount++; notification = n; - }); + })); notificationHandle.close(); assert.strictEqual(removeNotificationCount, 1); assert.strictEqual(notification.message, 'important message'); @@ -303,5 +323,8 @@ suite('Notifications', () => { assert.strictEqual(notification.priority, NotificationPriority.SILENT); notificationHandle.close(); assert.strictEqual(removeNotificationCount, 2); + notificationHandle.close(); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/test/common/resources.test.ts b/src/vs/workbench/test/common/resources.test.ts index a7849b94648..6ce08bcf9ea 100644 --- a/src/vs/workbench/test/common/resources.test.ts +++ b/src/vs/workbench/test/common/resources.test.ts @@ -4,7 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ResourceGlobMatcher } from 'vs/workbench/common/resources'; @@ -17,6 +19,8 @@ suite('ResourceGlobMatcher', () => { let contextService: IWorkspaceContextService; let configurationService: TestConfigurationService; + const disposables = new DisposableStore(); + setup(() => { contextService = new TestContextService(); configurationService = new TestConfigurationService({ @@ -27,8 +31,12 @@ suite('ResourceGlobMatcher', () => { }); }); + teardown(() => { + disposables.clear(); + }); + test('Basics', async () => { - const matcher = new ResourceGlobMatcher(() => configurationService.getValue(SETTING), e => e.affectsConfiguration(SETTING), contextService, configurationService); + const matcher = disposables.add(new ResourceGlobMatcher(() => configurationService.getValue(SETTING), e => e.affectsConfiguration(SETTING), contextService, configurationService)); // Matching assert.equal(matcher.matches(URI.file('/foo/bar')), false); @@ -37,7 +45,7 @@ suite('ResourceGlobMatcher', () => { // Events let eventCounter = 0; - matcher.onExpressionChange(() => eventCounter++); + disposables.add(matcher.onExpressionChange(() => eventCounter++)); await configurationService.setUserConfiguration(SETTING, { '**/*.foo': true }); configurationService.onDidChangeConfigurationEmitter.fire({ affectsConfiguration: (key: string) => key === SETTING } as any); @@ -64,4 +72,6 @@ suite('ResourceGlobMatcher', () => { assert.equal(matcher.matches(URI.file('/bar/foo.1')), true); assert.equal(matcher.matches(URI.file('C:/bar/foo.1')), true); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/test/common/utils.ts b/src/vs/workbench/test/common/utils.ts index f4a175f315a..b28eb1d1c8b 100644 --- a/src/vs/workbench/test/common/utils.ts +++ b/src/vs/workbench/test/common/utils.ts @@ -18,5 +18,5 @@ export function assertCleanState(): void { // If this test fails, it is a clear indication that // your test or suite is leaking services (e.g. via leaking text models) // assert.strictEqual(LanguageService.instanceCount, 0, 'No leaking ILanguageService'); - assert.strictEqual(LanguagesRegistry.instanceCount, 0, 'No leaking LanguagesRegistry'); + assert.strictEqual(LanguagesRegistry.instanceCount, 0, 'Error: Test run should not leak in LanguagesRegistry.'); } diff --git a/src/vs/workbench/test/common/workbenchTestServices.ts b/src/vs/workbench/test/common/workbenchTestServices.ts index 627f0ea0b77..117a4412509 100644 --- a/src/vs/workbench/test/common/workbenchTestServices.ts +++ b/src/vs/workbench/test/common/workbenchTestServices.ts @@ -28,6 +28,7 @@ import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { AutoSaveMode, IAutoSaveConfiguration, IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +import { IWorkspaceTrustEnablementService, IWorkspaceTrustManagementService, IWorkspaceTrustRequestService, IWorkspaceTrustTransitionParticipant, IWorkspaceTrustUriInfo, WorkspaceTrustRequestOptions, WorkspaceTrustUriResponse } from 'vs/platform/workspace/common/workspaceTrust'; export class TestLoggerService extends AbstractLoggerService { constructor(logsHome?: URI) { @@ -315,3 +316,141 @@ export const NullFilesConfigurationService = new class implements IFilesConfigur async updateReadonly(resource: URI, readonly: boolean | 'toggle' | 'reset'): Promise { } preventSaveConflicts(resource: URI, language?: string | undefined): boolean { throw new Error('Method not implemented.'); } }; + +export class TestWorkspaceTrustEnablementService implements IWorkspaceTrustEnablementService { + _serviceBrand: undefined; + + constructor(private isEnabled: boolean = true) { } + + isWorkspaceTrustEnabled(): boolean { + return this.isEnabled; + } +} + +export class TestWorkspaceTrustManagementService extends Disposable implements IWorkspaceTrustManagementService { + _serviceBrand: undefined; + + private _onDidChangeTrust = this._register(new Emitter()); + onDidChangeTrust = this._onDidChangeTrust.event; + + private _onDidChangeTrustedFolders = this._register(new Emitter()); + onDidChangeTrustedFolders = this._onDidChangeTrustedFolders.event; + + private _onDidInitiateWorkspaceTrustRequestOnStartup = this._register(new Emitter()); + onDidInitiateWorkspaceTrustRequestOnStartup = this._onDidInitiateWorkspaceTrustRequestOnStartup.event; + + + constructor( + private trusted: boolean = true + ) { + super(); + } + + get acceptsOutOfWorkspaceFiles(): boolean { + throw new Error('Method not implemented.'); + } + + set acceptsOutOfWorkspaceFiles(value: boolean) { + throw new Error('Method not implemented.'); + } + + addWorkspaceTrustTransitionParticipant(participant: IWorkspaceTrustTransitionParticipant): IDisposable { + throw new Error('Method not implemented.'); + } + + getTrustedUris(): URI[] { + throw new Error('Method not implemented.'); + } + + setParentFolderTrust(trusted: boolean): Promise { + throw new Error('Method not implemented.'); + } + + getUriTrustInfo(uri: URI): Promise { + throw new Error('Method not implemented.'); + } + + async setTrustedUris(folders: URI[]): Promise { + throw new Error('Method not implemented.'); + } + + async setUrisTrust(uris: URI[], trusted: boolean): Promise { + throw new Error('Method not implemented.'); + } + + canSetParentFolderTrust(): boolean { + throw new Error('Method not implemented.'); + } + + canSetWorkspaceTrust(): boolean { + throw new Error('Method not implemented.'); + } + + isWorkspaceTrusted(): boolean { + return this.trusted; + } + + isWorkspaceTrustForced(): boolean { + return false; + } + + get workspaceTrustInitialized(): Promise { + return Promise.resolve(); + } + + get workspaceResolved(): Promise { + return Promise.resolve(); + } + + async setWorkspaceTrust(trusted: boolean): Promise { + if (this.trusted !== trusted) { + this.trusted = trusted; + this._onDidChangeTrust.fire(this.trusted); + } + } +} + +export class TestWorkspaceTrustRequestService extends Disposable implements IWorkspaceTrustRequestService { + _serviceBrand: any; + + private readonly _onDidInitiateOpenFilesTrustRequest = this._register(new Emitter()); + readonly onDidInitiateOpenFilesTrustRequest = this._onDidInitiateOpenFilesTrustRequest.event; + + private readonly _onDidInitiateWorkspaceTrustRequest = this._register(new Emitter()); + readonly onDidInitiateWorkspaceTrustRequest = this._onDidInitiateWorkspaceTrustRequest.event; + + private readonly _onDidInitiateWorkspaceTrustRequestOnStartup = this._register(new Emitter()); + readonly onDidInitiateWorkspaceTrustRequestOnStartup = this._onDidInitiateWorkspaceTrustRequestOnStartup.event; + + constructor(private readonly _trusted: boolean) { + super(); + } + + requestOpenUrisHandler = async (uris: URI[]) => { + return WorkspaceTrustUriResponse.Open; + }; + + requestOpenFilesTrust(uris: URI[]): Promise { + return this.requestOpenUrisHandler(uris); + } + + async completeOpenFilesTrustRequest(result: WorkspaceTrustUriResponse, saveResponse: boolean): Promise { + throw new Error('Method not implemented.'); + } + + cancelWorkspaceTrustRequest(): void { + throw new Error('Method not implemented.'); + } + + async completeWorkspaceTrustRequest(trusted?: boolean): Promise { + throw new Error('Method not implemented.'); + } + + async requestWorkspaceTrust(options?: WorkspaceTrustRequestOptions): Promise { + return this._trusted; + } + + requestWorkspaceTrustOnStartup(): void { + throw new Error('Method not implemented.'); + } +} diff --git a/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts b/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts index 1a40dc62bd5..b88818f68fa 100644 --- a/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts @@ -8,7 +8,7 @@ import { workbenchInstantiationService as browserWorkbenchInstantiationService, import { ISharedProcessService } from 'vs/platform/ipc/electron-sandbox/services'; import { INativeHostService, IOSProperties, IOSStatistics } from 'vs/platform/native/common/native'; import { VSBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IFileDialogService, INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs'; import { IPartsSplash } from 'vs/platform/theme/common/themeService'; @@ -52,11 +52,8 @@ export class TestSharedProcessService implements ISharedProcessService { declare readonly _serviceBrand: undefined; createRawConnection(): never { throw new Error('Not Implemented'); } - getChannel(channelName: string): any { return undefined; } - registerChannel(channelName: string, channel: any): void { } - notifyRestored(): void { } } @@ -178,7 +175,7 @@ export function workbenchInstantiationService(overrides?: { textEditorService?: (instantiationService: IInstantiationService) => ITextEditorService; }, disposables = new DisposableStore()): ITestInstantiationService { const instantiationService = browserWorkbenchInstantiationService({ - workingCopyBackupService: (instantiationService: IInstantiationService) => new TestNativeWorkingCopyBackupService(), + workingCopyBackupService: () => disposables.add(new TestNativeWorkingCopyBackupService()), ...overrides }, disposables); @@ -216,7 +213,7 @@ export class TestNativeTextFileServiceWithEncodingOverrides extends NativeTextFi } } -export class TestNativeWorkingCopyBackupService extends NativeWorkingCopyBackupService { +export class TestNativeWorkingCopyBackupService extends NativeWorkingCopyBackupService implements IDisposable { private backupResourceJoiners: Function[]; private discardBackupJoiners: Function[]; @@ -231,15 +228,18 @@ export class TestNativeWorkingCopyBackupService extends NativeWorkingCopyBackupS const lifecycleService = new TestLifecycleService(); super(environmentService as any, fileService, logService, lifecycleService); - const inMemoryFileSystemProvider = new InMemoryFileSystemProvider(); - fileService.registerProvider(Schemas.inMemory, inMemoryFileSystemProvider); - fileService.registerProvider(Schemas.vscodeUserData, new FileUserDataProvider(Schemas.file, inMemoryFileSystemProvider, Schemas.vscodeUserData, logService)); + const inMemoryFileSystemProvider = this._register(new InMemoryFileSystemProvider()); + this._register(fileService.registerProvider(Schemas.inMemory, inMemoryFileSystemProvider)); + this._register(fileService.registerProvider(Schemas.vscodeUserData, this._register(new FileUserDataProvider(Schemas.file, inMemoryFileSystemProvider, Schemas.vscodeUserData, logService)))); this.backupResourceJoiners = []; this.discardBackupJoiners = []; this.discardedBackups = []; this.pendingBackupsArr = []; this.discardedAllBackups = false; + + this._register(fileService); + this._register(lifecycleService); } testGetFileService(): IFileService { diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index 97f52cb344d..17ee7553c42 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -7764,9 +7764,8 @@ declare module 'vscode' { readonly logPath: string; /** - * The mode the extension is running in. This is specific to the current - * extension. One extension may be in `ExtensionMode.Development` while - * other extensions in the host run in `ExtensionMode.Release`. + * The mode the extension is running in. See {@link ExtensionMode} + * for possible values and scenarios. */ readonly extensionMode: ExtensionMode; diff --git a/src/vscode-dts/vscode.proposed.interactive.d.ts b/src/vscode-dts/vscode.proposed.interactive.d.ts index 39e1e22e191..63e1429c82e 100644 --- a/src/vscode-dts/vscode.proposed.interactive.d.ts +++ b/src/vscode-dts/vscode.proposed.interactive.d.ts @@ -60,22 +60,31 @@ declare module 'vscode' { export interface TextDocumentContext { document: TextDocument; selection: Selection; - action?: string; + } + + export interface InteractiveEditorSessionProviderMetadata { + label: string; } export interface InteractiveEditorSessionProvider { + /** + * @deprecated + */ label: string; // Create a session. The lifetime of this session is the duration of the editing session with the input mode widget. prepareInteractiveEditorSession(context: TextDocumentContext, token: CancellationToken): ProviderResult; - provideInteractiveEditorResponse(request: InteractiveEditorRequest, token: CancellationToken): ProviderResult; + provideInteractiveEditorResponse(session: S, request: Omit, progress: Progress<{ message: string; edits: TextEdit[] }>, token: CancellationToken): ProviderResult; + + /** + * @deprecated + */ provideInteractiveEditorResponse2?(request: InteractiveEditorRequest, progress: Progress<{ message: string; edits: TextEdit[] }>, token: CancellationToken): ProviderResult; // eslint-disable-next-line local/vscode-dts-provider-naming releaseInteractiveEditorSession?(session: S): any; - // todo@API use enum instead of boolean // eslint-disable-next-line local/vscode-dts-provider-naming handleInteractiveEditorResponseFeedback?(session: S, response: R, kind: InteractiveEditorResponseFeedbackKind): void; } @@ -210,7 +219,7 @@ declare module 'vscode' { export function sendInteractiveRequestToProvider(providerId: string, message: InteractiveSessionDynamicRequest): void; - export function registerInteractiveEditorSessionProvider(provider: InteractiveEditorSessionProvider): Disposable; + export function registerInteractiveEditorSessionProvider(provider: InteractiveEditorSessionProvider, metadata?: InteractiveEditorSessionProviderMetadata): Disposable; export function transferChatSession(session: InteractiveSession, toWorkspace: Uri): void; } diff --git a/test/automation/src/electron.ts b/test/automation/src/electron.ts index 4f1cb9b27e5..da2087a8faa 100644 --- a/test/automation/src/electron.ts +++ b/test/automation/src/electron.ts @@ -29,7 +29,7 @@ export async function resolveElectronConfiguration(options: LaunchOptions): Prom '--disable-telemetry', '--no-cached-data', '--disable-updates', - '--disable-keytar', + '--use-inmemory-secretstorage', `--crash-reporter-directory=${crashesPath}`, '--disable-workspace-trust', `--extensions-dir=${extensionsPath}`, diff --git a/test/unit/electron/renderer.html b/test/unit/electron/renderer.html index 617a3bac138..5fcbc9661aa 100644 --- a/test/unit/electron/renderer.html +++ b/test/unit/electron/renderer.html @@ -23,16 +23,6 @@ window.alert = function () { throw new Error('window.alert() is not supported in tests!'); } window.confirm = function () { throw new Error('window.confirm() is not supported in tests!'); } - // Ignore uncaught cancelled promise errors - window.addEventListener('unhandledrejection', e => { - const name = e && e.reason && e.reason.name; - - if (name === 'Canceled') { - e.preventDefault(); - e.stopPropagation(); - } - }); - mocha.setup({ ui: 'tdd', timeout: typeof process.env['BUILD_ARTIFACTSTAGINGDIRECTORY'] === 'string' ? 30000 : 5000, diff --git a/test/unit/electron/renderer.js b/test/unit/electron/renderer.js index 21ef972f7c4..b7e785a037d 100644 --- a/test/unit/electron/renderer.js +++ b/test/unit/electron/renderer.js @@ -166,12 +166,57 @@ function loadTestModules(opts) { }).then(loadModules); } +let currentTestTitle; + function loadTests(opts) { + //#region Unexpected Output + + const _allowedTestOutput = new Set([ + 'The vm module of Node.js is deprecated in the renderer process and will be removed.', + ]); + + const _allowedTestsWithOutput = new Set([ + 'creates a snapshot', // https://github.com/microsoft/vscode/issues/192439 + 'validates a snapshot', // https://github.com/microsoft/vscode/issues/192439 + 'cleans up old snapshots', // https://github.com/microsoft/vscode/issues/192439 + 'issue #149412: VS Code hangs when bad semantic token data is received', // https://github.com/microsoft/vscode/issues/192440 + 'issue #134973: invalid semantic tokens should be handled better', // https://github.com/microsoft/vscode/issues/192440 + 'issue #148651: VSCode UI process can hang if a semantic token with negative values is returned by language service', // https://github.com/microsoft/vscode/issues/192440 + 'issue #149130: vscode freezes because of Bracket Pair Colorization', // https://github.com/microsoft/vscode/issues/192440 + 'property limits', // https://github.com/microsoft/vscode/issues/192443 + 'Error events', // https://github.com/microsoft/vscode/issues/192443 + 'Ensure output channel is logged to', // https://github.com/microsoft/vscode/issues/192443 + 'guards calls after runs are ended' // https://github.com/microsoft/vscode/issues/192468 + ]); + + let _testsWithUnexpectedOutput = false; + + for (const consoleFn of [console.log, console.error, console.info, console.warn, console.trace, console.debug]) { + console[consoleFn.name] = function (msg) { + if (!_allowedTestOutput.has(msg) && !_allowedTestsWithOutput.has(currentTestTitle)) { + _testsWithUnexpectedOutput = true; + consoleFn.apply(console, arguments); + } + }; + } + + //#endregion + + //#region Unexpected / Loader Errors + const _unexpectedErrors = []; const _loaderErrors = []; - // collect loader errors + const _allowedTestsWithUnhandledRejections = new Set([ + // Lifecycle tests + 'onWillShutdown - join with error is handled', + 'onBeforeShutdown - veto with error is treated as veto', + 'onBeforeShutdown - final veto with error is treated as veto', + // Search tests + 'Search Model: Search reports timed telemetry on search when error is called' + ]); + loader.require.config({ onError(err) { _loaderErrors.push(err); @@ -179,18 +224,40 @@ function loadTests(opts) { } }); - // collect unexpected errors loader.require(['vs/base/common/errors'], function (errors) { - errors.setUnexpectedErrorHandler(function (err) { + + const onUnexpectedError = function (err) { + if (err.name === 'Canceled') { + return; // ignore canceled errors that are common + } + let stack = (err ? err.stack : null); if (!stack) { stack = new Error().stack; } _unexpectedErrors.push((err && err.message ? err.message : err) + '\n' + stack); + }; + + process.on('uncaughtException', error => onUnexpectedError(error)); + process.on('unhandledRejection', (reason, promise) => { + onUnexpectedError(reason); + promise.catch(() => {}); }); + window.addEventListener('unhandledrejection', event => { + event.preventDefault(); // Do not log to test output, we show an error later when test ends + event.stopPropagation(); + + if (!_allowedTestsWithUnhandledRejections.has(currentTestTitle)) { + onUnexpectedError(event.reason); + } + }); + + errors.setUnexpectedErrorHandler(err => unexpectedErrorHandler(err)); }); + //#endregion + return loadWorkbenchTestingUtilsModule().then((workbenchTestingModule) => { const assertCleanState = workbenchTestingModule.assertCleanState; @@ -200,24 +267,30 @@ function loadTests(opts) { }); }); - return loadTestModules(opts).then(() => { - suite('Unexpected Errors & Loader Errors', function () { - test('should not have unexpected errors', function () { - const errors = _unexpectedErrors.concat(_loaderErrors); - if (errors.length) { - errors.forEach(function (stack) { - console.error(''); - console.error(stack); - }); - assert.ok(false, errors); - } - }); + teardown(() => { - test('assertCleanState - check that registries are clean and objects are disposed at the end of test running', () => { - assertCleanState(); - }); - }); + // should not have unexpected output + if (_testsWithUnexpectedOutput) { + assert.ok(false, 'Error: Unexpected console output in test run. Please ensure no console.[log|error|info|warn] usage in tests or runtime errors.'); + } + + // should not have unexpected errors + const errors = _unexpectedErrors.concat(_loaderErrors); + if (errors.length) { + for (const error of errors) { + console.error(`Error: Test run should not have unexpected errors:\n${error}`); + } + assert.ok(false, 'Error: Test run should not have unexpected errors.'); + } }); + + suiteTeardown(() => { // intentionally not in teardown because some tests only cleanup in suiteTeardown + + // should have cleaned up in registries + assertCleanState(); + }); + + return loadTestModules(opts); }); } @@ -329,9 +402,10 @@ function runTests(opts) { }); }); + runner.on('test', test => currentTestTitle = test.title); + if (opts.dev) { runner.on('fail', (test, err) => { - console.error(test.fullTitle()); console.error(err.stack); }); diff --git a/yarn.lock b/yarn.lock index 59f8d5ce788..bcba41352f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10008,10 +10008,10 @@ typescript@^4.7.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6" integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ== -typescript@^5.3.0-dev.20230905: - version "5.3.0-dev.20230905" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.0-dev.20230905.tgz#b88de602ef4afcc3a80a9c38023df82b7529e42a" - integrity sha512-Nl9MoKWN0YYlCvQnw850L4ZgqdmqwVGCi9cAoQDw4PsqRGaWAi9HKizS9xu0q4qgKKsEKetWCZHT8dBtJTGaMg== +typescript@^5.3.0-dev.20230911: + version "5.3.0-dev.20230911" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.0-dev.20230911.tgz#7f60e82ee86e381655ddc63141408eb90b6ca31d" + integrity sha512-2iI2l7OuGvU668gBje+JQKE8bsf7SH8w8ScwUkENHCcrbaDpXa/Oqfuwq5gdFM7SfVfp5p6c8kHZRMvL+kabJg== typical@^4.0.0: version "4.0.0" @@ -10685,45 +10685,45 @@ xtend@~2.1.1: dependencies: object-keys "~0.4.0" -xterm-addon-canvas@0.5.0-beta.22: - version "0.5.0-beta.22" - resolved "https://registry.yarnpkg.com/xterm-addon-canvas/-/xterm-addon-canvas-0.5.0-beta.22.tgz#513f0c2b7cf96073f47627b27e8965c1b1a22431" - integrity sha512-9F6ZI0DMRgffVfHkLkDwl5n8VscvCaV10tWI3skXOX7Y7Aws6OEeglkOPoU3IllofCU792kHKM4pPoToUxTltg== +xterm-addon-canvas@0.6.0-beta.16: + version "0.6.0-beta.16" + resolved "https://registry.yarnpkg.com/xterm-addon-canvas/-/xterm-addon-canvas-0.6.0-beta.16.tgz#6d6a475824c3c0129f3cb38eaea69cf12386cc31" + integrity sha512-uh90h+uozwrZbc/yMhbRnKIY7J34CTse6njibajdeK+0hQvj09HnBXgkvALImR/sEwyIz8SVtSL+OOZTlmAHIQ== -xterm-addon-image@0.6.0-beta.14: - version "0.6.0-beta.14" - resolved "https://registry.yarnpkg.com/xterm-addon-image/-/xterm-addon-image-0.6.0-beta.14.tgz#75fc3f824123183a4bbb5306e22f8b2c6966b0a6" - integrity sha512-D5Gh5JTKhHaPt1KwQNf6diF37KA4eToJw3XId1wy62tWmSqfq+QflhOGTfd+SnSQYCktU05ETzM+0tncIU62pQ== +xterm-addon-image@0.6.0-beta.21: + version "0.6.0-beta.21" + resolved "https://registry.yarnpkg.com/xterm-addon-image/-/xterm-addon-image-0.6.0-beta.21.tgz#e3708bc504c56a23ff31f12a2eeb335331a92aac" + integrity sha512-8/PTaXVPa4kQ0xzVeuZZk10OpbZBj2cgfwhM2B0ChSPvwrk0lX+ksnXdtDKH3tg+JYvo7fIhNXtkr4NwWt7VJQ== -xterm-addon-search@0.13.0-beta.20: - version "0.13.0-beta.20" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.13.0-beta.20.tgz#8ddd0513e2a70fcefa325722100d2e1bfaf3b9cb" - integrity sha512-wrx6187cJ1UenGL6ZeYv3jFvRPhhENTfbC+Hv1Fnww8LmsKhcj+0+Pm6yInNjX/9hNVsNzdqKyqNeEMoykyoyA== +xterm-addon-search@0.14.0-beta.15: + version "0.14.0-beta.15" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.14.0-beta.15.tgz#fcea611a04a8f4fd5dd3ec5e3403cce9cde72f0a" + integrity sha512-wPk3FzOFIeEAgVMjNL6CHoAfPcmk3DWwf28AtICfJ512JVJ3d0o9mUEh853m08/eaJJikAMXMEgTGRgjNtZU3Q== -xterm-addon-serialize@0.11.0-beta.20: - version "0.11.0-beta.20" - resolved "https://registry.yarnpkg.com/xterm-addon-serialize/-/xterm-addon-serialize-0.11.0-beta.20.tgz#e879b34d214761403f1081833f9221c6903bf0c3" - integrity sha512-OXnC1SATaz7kEFjFWhyv9MJaXi8yHdPjazpGLNi11h33CRTKtCQiqqPBHU87dztnXmpEX6Jw0/jr3zlyXuAmnw== +xterm-addon-serialize@0.12.0-beta.15: + version "0.12.0-beta.15" + resolved "https://registry.yarnpkg.com/xterm-addon-serialize/-/xterm-addon-serialize-0.12.0-beta.15.tgz#8e210d566c03b616823317a31dbce474a728b3c9" + integrity sha512-4iF9pQ/q6831ayADPlngAM0aa/mhzvZ4XEd4UNJIgW7mpgoSTvnbh6d6lb37pNkpdDFIRLJv6rlqDjlh6EkoJg== -xterm-addon-unicode11@0.6.0-beta.12: - version "0.6.0-beta.12" - resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.6.0-beta.12.tgz#ac6df9d635325dc692e4c602e74a2fc27a09405c" - integrity sha512-9wWWf/5nFafYgq0pn9EgAWnXaXGleVxfjNOqavpLRYFv0nw42QbaYyGvnGcxyYHM5Aqx/8rYE/DDVWZBqQZdYA== +xterm-addon-unicode11@0.7.0-beta.15: + version "0.7.0-beta.15" + resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.7.0-beta.15.tgz#07359424a862aedc456f35f95126739cd4fbe7de" + integrity sha512-CdPuahtDiacsBO618XTNc+z3diWPVzvpVlk0NcIsUpcdsF+44rgRBFaD7mT4tfAFU1w1ibTzTfOakrFlYZuM0A== -xterm-addon-webgl@0.16.0-beta.30: - version "0.16.0-beta.30" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.16.0-beta.30.tgz#820d5c65f868b14ec4177bfb8a294931a53616bf" - integrity sha512-39qPHPFmNENxcHf8/CzGHS6wzKMMegoRkHB1+scqtBhSxFaD8tX5Ye33HZIEdQ9nXe9xtr4FWVp77T+n9hdrew== +xterm-addon-webgl@0.17.0-beta.15: + version "0.17.0-beta.15" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.17.0-beta.15.tgz#53e15a170e4e9d8ddafb8971b2e63b4c596b9744" + integrity sha512-3JJ8KumPzFut1yloNhIrvObBwqp8kbqBI/up77MIyGE/6WiGhgecUFPmT8rYUCv4g/GBXoMan755yOEUNkKtcg== -xterm-headless@5.3.0-beta.61: - version "5.3.0-beta.61" - resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-5.3.0-beta.61.tgz#28654550cb572709b99ea3eb8672d4568ae141c9" - integrity sha512-yfkbPLUtKjE4K7DsZ204A1BuOKpu6Usqi6rIYWT4XRMi+LjnkTbBjGr2BSjyJ3Gmtm+cSgBD0SvRN+V3xNxbxA== +xterm-headless@5.4.0-beta.17: + version "5.4.0-beta.17" + resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-5.4.0-beta.17.tgz#cb779eba8da66ecad6f4be41af649453bb3260e9" + integrity sha512-ax2asr5QS4EnJAmRnKDDnAqESst+r2G/MckaYdxTDgX0pc997TU4B/JQ6c5BhxVLQvRwmotTm11/n909HiHadQ== -xterm@5.3.0-beta.61: - version "5.3.0-beta.61" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.3.0-beta.61.tgz#a6c27d90a5314da51d80deeb32f3bd77f1e1c8f6" - integrity sha512-rJHpCc48GSpHnu0SSERynQ80D5ikvFVsqhv6JdmeONTrnAFRr134OglJRIpbi2YK8UPbV6F6Dfqm/AQh+9GZzA== +xterm@5.4.0-beta.17: + version "5.4.0-beta.17" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.4.0-beta.17.tgz#7e7841b09339670c5ada967b73a1416d1b3dc9f7" + integrity sha512-hTSJHkyH/m26nQJt1oNI1KJU18JVJpOy41pP9WRRFQx1KoHhYq4HMRl7LO42SJt6Vs0mig0UYkbEnRWfT4FCww== y18n@^3.2.1: version "3.2.2"