diff --git a/.eslintrc.json b/.eslintrc.json
index 9c8493dd09..e9fdce694c 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -11,6 +11,7 @@
       "src/browser/tsconfig.json",
       "src/common/tsconfig.json",
       "src/headless/tsconfig.json",
+      "src/vs/tsconfig.json",
       "test/benchmark/tsconfig.json",
       "test/playwright/tsconfig.json",
       "addons/addon-attach/src/tsconfig.json",
@@ -43,6 +44,7 @@
   },
   "ignorePatterns": [
     "addons/*/src/third-party/*.ts",
+    "src/vs/*",
     "out/*",
     "out-test/*",
     "out-esbuild/*",
diff --git a/.vscode/settings.json b/.vscode/settings.json
index b6926a32f1..3bf1c69144 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -4,7 +4,9 @@
   },
   // Hide output files from the file explorer, comment this out to see the build output
   "files.exclude": {
+    "**/.nyc_output": true,
     "**/lib": true,
+    "**/dist": true,
     "**/out": true,
     "**/out-*": true,
   },
diff --git a/addons/addon-fit/src/FitAddon.ts b/addons/addon-fit/src/FitAddon.ts
index 2087e6146a..283af35457 100644
--- a/addons/addon-fit/src/FitAddon.ts
+++ b/addons/addon-fit/src/FitAddon.ts
@@ -6,6 +6,7 @@
 import type { Terminal, ITerminalAddon } from '@xterm/xterm';
 import type { FitAddon as IFitApi } from '@xterm/addon-fit';
 import { IRenderDimensions } from 'browser/renderer/shared/Types';
+import { ViewportConstants } from 'browser/shared/Constants';
 
 interface ITerminalDimensions {
   /**
@@ -64,8 +65,9 @@ export class FitAddon implements ITerminalAddon , IFitApi {
       return undefined;
     }
 
-    const scrollbarWidth = this._terminal.options.scrollback === 0 ?
-      0 : core.viewport.scrollBarWidth;
+    const scrollbarWidth = (this._terminal.options.scrollback === 0
+      ? 0
+      : (this._terminal.options.overviewRulerWidth || ViewportConstants.DEFAULT_SCROLL_BAR_WIDTH));
 
     const parentElementStyle = window.getComputedStyle(this._terminal.element.parentElement);
     const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
diff --git a/bin/esbuild.mjs b/bin/esbuild.mjs
index f056e30db4..a8ccf48f01 100644
--- a/bin/esbuild.mjs
+++ b/bin/esbuild.mjs
@@ -20,9 +20,11 @@ const config = {
 
 /** @type {esbuild.BuildOptions} */
 const commonOptions = {
+  bundle: true,
   format: 'esm',
   target: 'es2021',
   sourcemap: true,
+  treeShaking: true,
   logLevel: 'debug',
 };
 
@@ -34,8 +36,6 @@ const devOptions = {
 /** @type {esbuild.BuildOptions} */
 const prodOptions = {
   minify: true,
-  treeShaking: true,
-  logLevel: 'debug',
   legalComments: 'none',
   // TODO: Mangling private and protected properties will reduce bundle size quite a bit, we must
   //       make sure we don't cast privates to `any` in order to prevent regressions.
@@ -80,20 +80,21 @@ function getAddonEntryPoint(addon) {
 
 /** @type {esbuild.BuildOptions} */
 let bundleConfig = {
-  bundle: true,
   ...commonOptions,
   ...(config.isProd ? prodOptions : devOptions)
 };
 
 /** @type {esbuild.BuildOptions} */
 let outConfig = {
-  format: 'cjs'
+  format: 'cjs',
+  sourcemap: true,
 }
 let skipOut = false;
 
 /** @type {esbuild.BuildOptions} */
 let outTestConfig = {
-  format: 'cjs'
+  format: 'cjs',
+  sourcemap: true,
 }
 let skipOutTest = false;
 
@@ -171,7 +172,13 @@ if (config.addon) {
   };
   outConfig = {
     ...outConfig,
-    entryPoints: ['src/**/*.ts'],
+    entryPoints: [
+      'src/browser/**/*.ts',
+      'src/common/**/*.ts',
+      'src/headless/**/*.ts',
+      'src/vs/base/**/*.ts',
+      'src/vs/patches/**/*.ts'
+    ],
     outdir: 'out-esbuild/'
   };
   outTestConfig = {
diff --git a/bin/test_unit.js b/bin/test_unit.js
index e1e48bcedb..e1cd8494b4 100644
--- a/bin/test_unit.js
+++ b/bin/test_unit.js
@@ -34,7 +34,14 @@ const checkCoverage = flagArgs.indexOf('--coverage') >= 0;
 if (checkCoverage) {
   flagArgs.splice(flagArgs.indexOf('--coverage'), 1);
   const executable = npmBinScript('nyc');
-  const args = ['--check-coverage', `--lines=${COVERAGE_LINES_THRESHOLD}`, npmBinScript('mocha'), ...testFiles, ...flagArgs];
+  const args = [
+    '--check-coverage',
+    `--lines=${COVERAGE_LINES_THRESHOLD}`,
+    '--exclude=out-esbuild/vs/**',
+    npmBinScript('mocha'),
+    ...testFiles,
+    ...flagArgs
+  ];
   console.info('executable', executable);
   console.info('args', args);
   const run = cp.spawnSync(
diff --git a/bin/vs_base_find_unused.js b/bin/vs_base_find_unused.js
new file mode 100644
index 0000000000..df54aab59a
--- /dev/null
+++ b/bin/vs_base_find_unused.js
@@ -0,0 +1,41 @@
+// @ts-check
+
+const { dirname } = require("path");
+const ts = require("typescript");
+const fs = require("fs");
+
+function findUnusedSymbols(
+  /** @type string */ tsconfigPath
+) {
+  // Initialize a program using the project's tsconfig.json
+  const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
+  const parsedConfig = ts.parseJsonConfigFileContent(configFile.config, ts.sys, dirname(tsconfigPath));
+
+  // Initialize a program with the parsed configuration
+  const program = ts.createProgram(parsedConfig.fileNames, {
+    ...parsedConfig.options,
+    noUnusedLocals: true
+  });
+  const sourceFiles = program.getSourceFiles();
+  const usedBaseSourceFiles = sourceFiles.filter(e => e.fileName.includes('src/vs/base/'));
+  const usedFilesInBase = usedBaseSourceFiles.map(e => e.fileName.replace(/^.+\/src\//, 'src/')).sort((a, b) => a.localeCompare(b));
+  // console.log('Source files used in src/vs/base/:', used);
+
+  // Get an array of all files that exist in src/vs/base/
+  const allFilesInBase = (
+    fs.readdirSync('src/vs/base', { recursive: true, withFileTypes: true })
+      .filter(e => e.isFile())
+      // @ts-ignore HACK: This is only available in Node 20
+      .map(e => `${e.parentPath}/${e.name}`.replace(/\\/g, '/'))
+  );
+  const unusedFilesInBase = allFilesInBase.filter(e => !usedFilesInBase.includes(e));
+
+  console.log({
+    allFilesInBase,
+    usedFilesInBase,
+    unusedFilesInBase
+  });
+}
+
+// Example usage
+findUnusedSymbols("./src/browser/tsconfig.json");
diff --git a/bin/vs_base_update.ps1 b/bin/vs_base_update.ps1
new file mode 100644
index 0000000000..f483fd9655
--- /dev/null
+++ b/bin/vs_base_update.ps1
@@ -0,0 +1,61 @@
+# Get latest vscode repo
+if (Test-Path -Path "src/vs/temp") {
+  Write-Host "`e[32m> Fetching latest`e[0m"
+  git -C src/vs/temp checkout
+  git -C src/vs/temp pull
+} else {
+  Write-Host "`e[32m> Cloning microsoft/vscode`e[0m"
+  $null = New-Item -ItemType Directory -Path "src/vs/temp" -Force
+  git clone https://github.com/microsoft/vscode src/vs/temp
+}
+
+# Delete old base
+Write-Host "`e[32m> Deleting old base`e[0m"
+$null = Remove-Item -Recurse -Force "src/vs/base"
+
+# Copy base
+Write-Host "`e[32m> Copying base`e[0m"
+Copy-Item -Path "src/vs/temp/src/vs/base" -Destination "src/vs/base" -Recurse
+
+# Comment out any CSS imports
+Write-Host "`e[32m> Commenting out CSS imports" -NoNewline
+$baseFiles = Get-ChildItem -Path "src/vs/base" -Recurse -File
+$count = 0
+foreach ($file in $baseFiles) {
+  $content = Get-Content -Path $file.FullName
+  $updatedContent = $content | ForEach-Object {
+    if ($_ -match "^import 'vs/css!") {
+      Write-Host "`e[32m." -NoNewline
+      $count++
+      "// $_"
+    } else {
+      $_
+    }
+  }
+  $updatedContent | Set-Content -Path $file.FullName
+}
+Write-Host " $count files patched`e[0m"
+
+# Replace `monaco-*` with `xterm-*`, this will help avoid any styling conflicts when monaco and
+# xterm.js are used in the same project.
+Write-Host "`e[32m> Replacing monaco-* class names with xterm-* `e[0m" -NoNewline
+$baseFiles = Get-ChildItem -Path "src/vs/base" -Recurse -File
+$count = 0
+foreach ($file in $baseFiles) {
+  $content = Get-Content -Path $file.FullName
+  if ($content -match "monaco-([a-zA-Z\-]+)") {
+    $updatedContent = $content -replace "monaco-([a-zA-Z\-]+)", 'xterm-$1'
+    Write-Host "`e[32m." -NoNewline
+    $count++
+    $updatedContent | Set-Content -Path $file.FullName
+  }
+}
+Write-Host " $count files patched`e[0m"
+
+# Copy typings
+Write-Host "`e[32m> Copying typings`e[0m"
+Copy-Item -Path "src/vs/temp/src/typings" -Destination "src/vs" -Recurse -Force
+
+# Deleting unwanted typings
+Write-Host "`e[32m> Deleting unwanted typings`e[0m"
+$null = Remove-Item -Path "src/vs/typings/vscode-globals-modules.d.ts" -Force
diff --git a/css/xterm.css b/css/xterm.css
index 51e9b39e76..819654e453 100644
--- a/css/xterm.css
+++ b/css/xterm.css
@@ -112,10 +112,6 @@
     top: 0;
 }
 
-.xterm .xterm-scroll-area {
-    visibility: hidden;
-}
-
 .xterm-char-measure-element {
     display: inline-block;
     visibility: hidden;
@@ -222,3 +218,68 @@
     z-index: 2;
     position: relative;
 }
+
+
+
+/* Derived from vs/base/browser/ui/scrollbar/media/scrollbar.css */
+
+/* xterm.js customization: Override xterm's cursor style */
+.xterm .xterm-scrollable-element > .scrollbar {
+    cursor: default;
+}
+
+/* Arrows */
+.xterm .xterm-scrollable-element > .scrollbar > .scra {
+	cursor: pointer;
+	font-size: 11px !important;
+}
+
+.xterm .xterm-scrollable-element > .visible {
+	opacity: 1;
+
+	/* Background rule added for IE9 - to allow clicks on dom node */
+	background:rgba(0,0,0,0);
+
+	transition: opacity 100ms linear;
+	/* In front of peek view */
+	z-index: 11;
+}
+.xterm .xterm-scrollable-element > .invisible {
+	opacity: 0;
+	pointer-events: none;
+}
+.xterm .xterm-scrollable-element > .invisible.fade {
+	transition: opacity 800ms linear;
+}
+
+/* Scrollable Content Inset Shadow */
+.xterm .xterm-scrollable-element > .shadow {
+	position: absolute;
+	display: none;
+}
+.xterm .xterm-scrollable-element > .shadow.top {
+	display: block;
+	top: 0;
+	left: 3px;
+	height: 3px;
+	width: 100%;
+	box-shadow: var(--vscode-scrollbar-shadow, #000) 0 6px 6px -6px inset;
+}
+.xterm .xterm-scrollable-element > .shadow.left {
+	display: block;
+	top: 3px;
+	left: 0;
+	height: 100%;
+	width: 3px;
+	box-shadow: var(--vscode-scrollbar-shadow, #000) 6px 0 6px -6px inset;
+}
+.xterm .xterm-scrollable-element > .shadow.top-left-corner {
+	display: block;
+	top: 0;
+	left: 0;
+	height: 3px;
+	width: 3px;
+}
+.xterm .xterm-scrollable-element > .shadow.top.left {
+	box-shadow: var(--vscode-scrollbar-shadow, #000) 6px 0 6px -6px inset;
+}
diff --git a/demo/client.ts b/demo/client.ts
index 0e794f3ed4..57208184a9 100644
--- a/demo/client.ts
+++ b/demo/client.ts
@@ -437,12 +437,13 @@ function initOptions(term: Terminal): void {
     'logger',
     'theme',
     'windowOptions',
-    'windowsPty'
+    'windowsPty',
+    // Deprecated
+    'fastScrollModifier'
   ];
   const stringOptions = {
     cursorStyle: ['block', 'underline', 'bar'],
     cursorInactiveStyle: ['outline', 'block', 'bar', 'underline', 'none'],
-    fastScrollModifier: ['none', 'alt', 'ctrl', 'shift'],
     fontFamily: null,
     fontWeight: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
     fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
@@ -575,6 +576,7 @@ function initOptions(term: Terminal): void {
               cursor: '#333333',
               cursorAccent: '#ffffff',
               selectionBackground: '#add6ff',
+              overviewRulerBorder: '#aaaaaa',
               black: '#000000',
               blue: '#0451a5',
               brightBlack: '#666666',
diff --git a/demo/webpack.config.js b/demo/webpack.config.js
deleted file mode 100644
index 46a853949e..0000000000
--- a/demo/webpack.config.js
+++ /dev/null
@@ -1,62 +0,0 @@
-/**
- * Copyright (c) 2019 The xterm.js authors. All rights reserved.
- * @license MIT
- */
-
-// @ts-check
-
-const path = require('path');
-
-/**
- * This webpack config does a production build for xterm.js. It works by taking the output from tsc
- * (via `yarn watch` or `yarn prebuild`) which are put into `out/` and webpacks them into a
- * production mode umd library module in `lib/`. The aliases are used fix up the absolute paths
- * output by tsc (because of `baseUrl` and `paths` in `tsconfig.json`.
- *
- * @type {import('webpack').Configuration}
- */
-const config = {
-  entry: path.resolve(__dirname, 'client.ts'),
-  devtool: 'inline-source-map',
-  module: {
-    rules: [
-      {
-        test: /\.tsx?$/,
-        use: 'ts-loader',
-        exclude: /node_modules/
-      },
-      {
-        test: /\.js$/,
-        use: ["source-map-loader"],
-        enforce: "pre",
-        exclude: /node_modules/
-      }
-    ]
-  },
-  resolve: {
-    modules: [
-      'node_modules',
-      path.resolve(__dirname, '..'),
-      path.resolve(__dirname, '../addons')
-    ],
-    extensions: [ '.tsx', '.ts', '.js' ],
-    alias: {
-      common: path.resolve('./out/common'),
-      browser: path.resolve('./out/browser')
-    },
-    fallback: {
-      // The ligature modules contains fallbacks for node environments, we never want to browserify them
-      stream: false,
-      util: false,
-      os: false,
-      path: false,
-      fs: false
-    }
-  },
-  output: {
-    filename: 'client-bundle.js',
-    path: path.resolve(__dirname, 'dist')
-  },
-  mode: 'development'
-};
-module.exports = config;
diff --git a/package.json b/package.json
index 941f78c957..894ec18a09 100644
--- a/package.json
+++ b/package.json
@@ -27,15 +27,11 @@
     "setup": "npm run build",
     "presetup": "npm run install-addons",
     "install-addons": "node ./bin/install-addons.js",
-
     "start": "node demo/start",
-    "build-demo": "webpack --config ./demo/webpack.config.js",
-
     "build": "npm run tsc",
     "watch": "npm run tsc-watch",
     "tsc": "tsc -b ./tsconfig.all.json",
     "tsc-watch": "tsc -b -w ./tsconfig.all.json --preserveWatchOutput",
-
     "esbuild": "node bin/esbuild_all.mjs",
     "esbuild-watch": "node bin/esbuild_all.mjs --watch",
     "esbuild-package": "node bin/esbuild_all.mjs --prod",
@@ -43,33 +39,26 @@
     "esbuild-package-headless-only": "node bin/esbuild.mjs --prod --headless",
     "esbuild-demo": "node bin/esbuild.mjs --demo-client",
     "esbuild-demo-watch": "node bin/esbuild.mjs --demo-client --watch",
-
     "test": "npm run test-unit",
     "posttest": "npm run lint",
-
     "lint": "eslint -c .eslintrc.json --max-warnings 0 --ext .ts src/ addons/",
     "lint-api": "eslint --no-eslintrc -c .eslintrc.json.typings --max-warnings 0 --no-ignore --ext .d.ts typings/",
-
     "test-unit": "node ./bin/test_unit.js",
     "test-unit-coverage": "node ./bin/test_unit.js --coverage",
     "test-unit-dev": "cross-env NODE_PATH='./out' mocha",
-
     "test-integration": "node ./bin/test_integration.js --workers=75%",
     "test-integration-chromium": "node ./bin/test_integration.js --workers=75% \"--project=ChromeStable\"",
     "test-integration-firefox": "node ./bin/test_integration.js --workers=75% \"--project=FirefoxStable\"",
     "test-integration-webkit": "node ./bin/test_integration.js --workers=75% \"--project=WebKit\"",
     "test-integration-debug": "node ./bin/test_integration.js  --workers=1 --headed --timeout=30000",
-
     "benchmark": "NODE_PATH=./out xterm-benchmark -r 5 -c test/benchmark/benchmark.json",
     "benchmark-baseline": "NODE_PATH=./out xterm-benchmark -r 5 -c test/benchmark/benchmark.json --baseline out-tsc/test-benchmark/test/benchmark/*benchmark.js",
     "benchmark-eval": "NODE_PATH=./out xterm-benchmark -r 5 -c test/benchmark/benchmark.json --eval out-tsc/test-benchmark/test/benchmark/*benchmark.js",
-
     "clean": "rm -rf lib out addons/*/lib addons/*/out",
     "vtfeatures": "node bin/extract_vtfeatures.js src/**/*.ts src/*.ts",
-
     "prepackage": "npm run build",
     "package": "webpack",
-    "postpackage":"npm run esbuild-package",
+    "postpackage": "npm run esbuild-package",
     "prepackage-headless": "npm run esbuild-package-headless-only",
     "package-headless": "webpack --config ./webpack.config.headless.js",
     "postpackage-headless": "node ./bin/package_headless.js",
@@ -88,6 +77,7 @@
     "@types/jsdom": "^16.2.13",
     "@types/mocha": "^9.0.0",
     "@types/node": "^18.16.0",
+    "@types/trusted-types": "^1.0.6",
     "@types/utf8": "^3.0.0",
     "@types/webpack": "^5.28.0",
     "@types/ws": "^8.2.0",
diff --git a/src/browser/CoreBrowserTerminal.ts b/src/browser/CoreBrowserTerminal.ts
index f9eefaed35..6755006a0f 100644
--- a/src/browser/CoreBrowserTerminal.ts
+++ b/src/browser/CoreBrowserTerminal.ts
@@ -21,12 +21,12 @@
  *   http://linux.die.net/man/7/urxvt
  */
 
+import { IDecoration, IDecorationOptions, IDisposable, ILinkProvider, IMarker } from '@xterm/xterm';
 import { copyHandler, handlePasteEvent, moveTextAreaUnderMouseCursor, paste, rightClickHandler } from 'browser/Clipboard';
 import { addDisposableDomListener } from 'browser/Lifecycle';
-import { Linkifier } from './Linkifier';
 import * as Strings from 'browser/LocalizableStrings';
 import { OscLinkProvider } from 'browser/OscLinkProvider';
-import { CharacterJoinerHandler, CustomKeyEventHandler, CustomWheelEventHandler, IBrowser, IBufferRange, ICompositionHelper, ILinkifier2, ITerminal, IViewport } from 'browser/Types';
+import { CharacterJoinerHandler, CustomKeyEventHandler, CustomWheelEventHandler, IBrowser, IBufferRange, ICompositionHelper, ILinkifier2, ITerminal } from 'browser/Types';
 import { Viewport } from 'browser/Viewport';
 import { BufferDecorationRenderer } from 'browser/decorations/BufferDecorationRenderer';
 import { OverviewRulerRenderer } from 'browser/decorations/OverviewRulerRenderer';
@@ -36,6 +36,7 @@ import { IRenderer } from 'browser/renderer/shared/Types';
 import { CharSizeService } from 'browser/services/CharSizeService';
 import { CharacterJoinerService } from 'browser/services/CharacterJoinerService';
 import { CoreBrowserService } from 'browser/services/CoreBrowserService';
+import { LinkProviderService } from 'browser/services/LinkProviderService';
 import { MouseService } from 'browser/services/MouseService';
 import { RenderService } from 'browser/services/RenderService';
 import { SelectionService } from 'browser/services/SelectionService';
@@ -46,7 +47,7 @@ import { CoreTerminal } from 'common/CoreTerminal';
 import { EventEmitter, IEvent, forwardEvent } from 'common/EventEmitter';
 import { MutableDisposable, toDisposable } from 'common/Lifecycle';
 import * as Browser from 'common/Platform';
-import { ColorRequestType, CoreMouseAction, CoreMouseButton, CoreMouseEventType, IColorEvent, ITerminalOptions, KeyboardResultType, ScrollSource, SpecialColorIndex } from 'common/Types';
+import { ColorRequestType, CoreMouseAction, CoreMouseButton, CoreMouseEventType, IColorEvent, ITerminalOptions, KeyboardResultType, SpecialColorIndex } from 'common/Types';
 import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
 import { IBuffer } from 'common/buffer/Types';
 import { C0, C1_ESCAPED } from 'common/data/EscapeSequences';
@@ -54,10 +55,9 @@ import { evaluateKeyboardEvent } from 'common/input/Keyboard';
 import { toRgbString } from 'common/input/XParseColor';
 import { DecorationService } from 'common/services/DecorationService';
 import { IDecorationService } from 'common/services/Services';
-import { IDecoration, IDecorationOptions, IDisposable, ILinkProvider, IMarker } from '@xterm/xterm';
 import { WindowsOptionsReportType } from '../common/InputHandler';
 import { AccessibilityManager } from './AccessibilityManager';
-import { LinkProviderService } from 'browser/services/LinkProviderService';
+import { Linkifier } from './Linkifier';
 
 export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
   public textarea: HTMLTextAreaElement | undefined;
@@ -65,13 +65,13 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
   public screenElement: HTMLElement | undefined;
 
   private _document: Document | undefined;
-  private _viewportScrollArea: HTMLElement | undefined;
   private _viewportElement: HTMLElement | undefined;
   private _helperContainer: HTMLElement | undefined;
   private _compositionView: HTMLElement | undefined;
 
   public linkifier: ILinkifier2 | undefined;
   private _overviewRulerRenderer: OverviewRulerRenderer | undefined;
+  private _viewport: Viewport | undefined;
 
   public browser: IBrowser = Browser as any;
 
@@ -118,7 +118,6 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
    */
   private _unprocessedDeadKey: boolean = false;
 
-  public viewport: IViewport | undefined;
   private _compositionHelper: ICompositionHelper | undefined;
   private _accessibilityManager: MutableDisposable<AccessibilityManager> = this.register(new MutableDisposable());
 
@@ -427,10 +426,6 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
     this._viewportElement.classList.add('xterm-viewport');
     fragment.appendChild(this._viewportElement);
 
-    this._viewportScrollArea = this._document.createElement('div');
-    this._viewportScrollArea.classList.add('xterm-scroll-area');
-    this._viewportElement.appendChild(this._viewportScrollArea);
-
     this.screenElement = this._document.createElement('div');
     this.screenElement.classList.add('xterm-screen');
     this.register(addDisposableDomListener(this.screenElement, 'mousemove', (ev: MouseEvent) => this.updateCursorStyle(ev)));
@@ -503,11 +498,6 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
       this._renderService.setRenderer(this._createRenderer());
     }
 
-    this.viewport = this._instantiationService.createInstance(Viewport, this._viewportElement, this._viewportScrollArea);
-    this.viewport.onRequestScrollLines(e => this.scrollLines(e.amount, e.suppressScrollEvent, ScrollSource.VIEWPORT)),
-    this.register(this._inputHandler.onRequestSyncScrollBar(() => this.viewport!.syncScrollArea()));
-    this.register(this.viewport);
-
     this.register(this.onCursorMove(() => {
       this._renderService!.handleCursorMove();
       this._syncTextArea();
@@ -515,7 +505,9 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
     this.register(this.onResize(() => this._renderService!.handleResize(this.cols, this.rows)));
     this.register(this.onBlur(() => this._renderService!.handleBlur()));
     this.register(this.onFocus(() => this._renderService!.handleFocus()));
-    this.register(this._renderService.onDimensionsChange(() => this.viewport!.syncScrollArea()));
+
+    this._viewport = this.register(this._instantiationService.createInstance(Viewport, this.element, this.screenElement));
+    this.register(this._viewport.onRequestScrollLines(e => super.scrollLines(e, false)));
 
     this._selectionService = this.register(this._instantiationService.createInstance(SelectionService,
       this.element,
@@ -534,11 +526,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
       this.textarea!.focus();
       this.textarea!.select();
     }));
-    this.register(this._onScroll.event(ev => {
-      this.viewport!.syncScrollArea();
-      this._selectionService!.refresh();
-    }));
-    this.register(addDisposableDomListener(this._viewportElement, 'scroll', () => this._selectionService!.refresh()));
+    this.register(this._onScroll.event(() => this._selectionService!.refresh()));
 
     this.register(this._instantiationService.createInstance(BufferDecorationRenderer, this.screenElement));
     this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService!.handleMouseDown(e)));
@@ -642,13 +630,11 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
           if (self._customWheelEventHandler && self._customWheelEventHandler(ev as WheelEvent) === false) {
             return false;
           }
-          const amount = self.viewport!.getLinesScrolled(ev as WheelEvent);
-
-          if (amount === 0) {
+          const deltaY = (ev as WheelEvent).deltaY;
+          if (deltaY === 0) {
             return false;
           }
-
-          action = (ev as WheelEvent).deltaY < 0 ? CoreMouseAction.UP : CoreMouseAction.DOWN;
+          action = deltaY < 0 ? CoreMouseAction.UP : CoreMouseAction.DOWN;
           but = CoreMouseButton.WHEEL;
           break;
         default:
@@ -807,42 +793,23 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
 
       if (!this.buffer.hasScrollback) {
         // Convert wheel events into up/down events when the buffer does not have scrollback, this
-        // enables scrolling in apps hosted in the alt buffer such as vim or tmux.
-        const amount = this.viewport!.getLinesScrolled(ev);
+        // enables scrolling in apps hosted in the alt buffer such as vim or tmux even when mouse
+        // events are not enabled.
+        // This used implementation used get the actual lines/partial lines scrolled from the
+        // viewport but since moving to the new viewport implementation has been simplified to
+        // simply send a single up or down sequence.
 
         // Do nothing if there's no vertical scroll
-        if (amount === 0) {
-          return;
+        const deltaY = (ev as WheelEvent).deltaY;
+        if (deltaY === 0) {
+          return false;
         }
 
         // Construct and send sequences
         const sequence = C0.ESC + (this.coreService.decPrivateModes.applicationCursorKeys ? 'O' : '[') + (ev.deltaY < 0 ? 'A' : 'B');
-        let data = '';
-        for (let i = 0; i < Math.abs(amount); i++) {
-          data += sequence;
-        }
-        this.coreService.triggerDataEvent(data, true);
+        this.coreService.triggerDataEvent(sequence, true);
         return this.cancel(ev, true);
       }
-
-      // normal viewport scrolling
-      // conditionally stop event, if the viewport still had rows to scroll within
-      if (this.viewport!.handleWheel(ev)) {
-        return this.cancel(ev);
-      }
-    }, { passive: false }));
-
-    this.register(addDisposableDomListener(el, 'touchstart', (ev: TouchEvent) => {
-      if (this.coreMouseService.areMouseEventsActive) return;
-      this.viewport!.handleTouchStart(ev);
-      return this.cancel(ev);
-    }, { passive: true }));
-
-    this.register(addDisposableDomListener(el, 'touchmove', (ev: TouchEvent) => {
-      if (this.coreMouseService.areMouseEventsActive) return;
-      if (!this.viewport!.handleTouchMove(ev)) {
-        return this.cancel(ev);
-      }
     }, { passive: false }));
   }
 
@@ -878,12 +845,36 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
     }
   }
 
-  public scrollLines(disp: number, suppressScrollEvent?: boolean, source = ScrollSource.TERMINAL): void {
-    if (source === ScrollSource.VIEWPORT) {
-      super.scrollLines(disp, suppressScrollEvent, source);
-      this.refresh(0, this.rows - 1);
+  public scrollLines(disp: number, suppressScrollEvent?: boolean): void {
+    // All scrollLines methods need to go via the viewport in order to support smooth scroll
+    if (this._viewport) {
+      this._viewport.scrollLines(disp);
+    } else {
+      super.scrollLines(disp, suppressScrollEvent);
+    }
+    this.refresh(0, this.rows - 1);
+  }
+
+  public scrollPages(pageCount: number): void {
+    this.scrollLines(pageCount * (this.rows - 1));
+  }
+
+  public scrollToTop(): void {
+    this.scrollLines(-this._bufferService.buffer.ydisp);
+  }
+
+  public scrollToBottom(disableSmoothScroll?: boolean): void {
+    if (disableSmoothScroll && this._viewport) {
+      this._viewport.scrollToLine(this.buffer.ybase, true);
     } else {
-      this.viewport?.scrollLines(disp);
+      this.scrollLines(this._bufferService.buffer.ybase - this._bufferService.buffer.ydisp);
+    }
+  }
+
+  public scrollToLine(line: number): void {
+    const scrollAmount = line - this._bufferService.buffer.ydisp;
+    if (scrollAmount !== 0) {
+      this.scrollLines(scrollAmount);
     }
   }
 
@@ -1011,7 +1002,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
 
     if (!shouldIgnoreComposition && !this._compositionHelper!.keydown(event)) {
       if (this.options.scrollOnUserInput && this.buffer.ybase !== this.buffer.ydisp) {
-        this.scrollToBottom();
+        this.scrollToBottom(true);
       }
       return false;
     }
@@ -1212,10 +1203,6 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
 
   private _afterResize(x: number, y: number): void {
     this._charSizeService?.measure();
-
-    // Sync the scroll area to make sure scroll events don't fire and scroll the viewport to an
-    // invalid location
-    this.viewport?.syncScrollArea(true);
   }
 
   /**
@@ -1237,8 +1224,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
     }
     // IMPORTANT: Fire scroll event before viewport is reset. This ensures embedders get the clear
     // scroll event and that the viewport's state will be valid for immediate writes.
-    this._onScroll.fire({ position: this.buffer.ydisp, source: ScrollSource.TERMINAL });
-    this.viewport?.reset();
+    this._onScroll.fire({ position: this.buffer.ydisp });
     this.refresh(0, this.rows - 1);
   }
 
@@ -1263,7 +1249,6 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
     super.reset();
     this._selectionService?.reset();
     this._decorationService.reset();
-    this.viewport?.reset();
 
     // reattach
     this._customKeyEventHandler = customKeyEventHandler;
diff --git a/src/browser/Terminal.test.ts b/src/browser/Terminal.test.ts
index 11f676a340..b871058e82 100644
--- a/src/browser/Terminal.test.ts
+++ b/src/browser/Terminal.test.ts
@@ -3,14 +3,13 @@
  * @license MIT
  */
 
+import { MockCompositionHelper, MockRenderer, MockViewport, TestTerminal } from 'browser/TestUtils.test';
+import type { IBrowser } from 'browser/Types';
 import { assert } from 'chai';
-import { MockViewport, MockCompositionHelper, MockRenderer, TestTerminal } from 'browser/TestUtils.test';
 import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
 import { CellData } from 'common/buffer/CellData';
 import { MockUnicodeService } from 'common/TestUtils.test';
-import { IMarker, ScrollSource } from 'common/Types';
-import { ICoreService } from 'common/services/Services';
-import type { IBrowser } from 'browser/Types';
+import { IMarker } from 'common/Types';
 
 const INIT_COLS = 80;
 const INIT_ROWS = 24;
@@ -29,8 +28,7 @@ describe('Terminal', () => {
     term = new TestTerminal(termOptions);
     term.refresh = () => { };
     (term as any).renderer = new MockRenderer();
-    term.viewport = new MockViewport();
-    term.viewport.onRequestScrollLines(e => term.scrollLines(e.amount, e.suppressScrollEvent, ScrollSource.VIEWPORT));
+    (term as any).viewport = new MockViewport();
     (term as any)._compositionHelper = new MockCompositionHelper();
     (term as any).element = {
       classList: {
diff --git a/src/browser/Types.ts b/src/browser/Types.ts
index 9ef9d3a811..43e1f9e265 100644
--- a/src/browser/Types.ts
+++ b/src/browser/Types.ts
@@ -19,7 +19,6 @@ export interface ITerminal extends InternalPassthroughApis, ICoreTerminal {
   browser: IBrowser;
   buffer: IBuffer;
   linkifier: ILinkifier2 | undefined;
-  viewport: IViewport | undefined;
   options: Required<ITerminalOptions>;
 
   onBlur: IEvent<void>;
@@ -67,6 +66,10 @@ export interface IColorSet {
   selectionBackgroundOpaque: IColor;
   selectionInactiveBackgroundTransparent: IColor;
   selectionInactiveBackgroundOpaque: IColor;
+  scrollbarSliderBackground: IColor;
+  scrollbarSliderHoverBackground: IColor;
+  scrollbarSliderActiveBackground: IColor;
+  overviewRulerBorder: IColor;
   ansi: IColor[];
   /** Maps original colors to colors that respect minimum contrast ratio. */
   contrastCache: IColorContrastCache;
diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts
index cb0f35ead4..64fcba2c88 100644
--- a/src/browser/Viewport.ts
+++ b/src/browser/Viewport.ts
@@ -1,414 +1,186 @@
 /**
- * Copyright (c) 2016 The xterm.js authors. All rights reserved.
+ * Copyright (c) 2024 The xterm.js authors. All rights reserved.
  * @license MIT
  */
 
-import { addDisposableDomListener } from 'browser/Lifecycle';
-import { IViewport, ReadonlyColorSet } from 'browser/Types';
-import { IRenderDimensions } from 'browser/renderer/shared/Types';
-import { ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
-import { EventEmitter } from 'common/EventEmitter';
-import { Disposable } from 'common/Lifecycle';
-import { IBuffer } from 'common/buffer/Types';
-import { IBufferService, IOptionsService } from 'common/services/Services';
+import { ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
+import { ViewportConstants } from 'browser/shared/Constants';
+import { EventEmitter, runAndSubscribe } from 'common/EventEmitter';
+import { Disposable, toDisposable } from 'common/Lifecycle';
+import { IBufferService, ICoreMouseService, IOptionsService } from 'common/services/Services';
+import { CoreMouseEventType } from 'common/Types';
+import { scheduleAtNextAnimationFrame } from 'vs/base/browser/dom';
+import { SmoothScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
+import type { ScrollableElementChangeOptions } from 'vs/base/browser/ui/scrollbar/scrollableElementOptions';
+import { Scrollable, ScrollbarVisibility, type ScrollEvent } from 'vs/base/common/scrollable';
 
-const FALLBACK_SCROLL_BAR_WIDTH = 15;
+export class Viewport extends Disposable {
 
-interface ISmoothScrollState {
-  startTime: number;
-  origin: number;
-  target: number;
-}
-
-/**
- * Represents the viewport of a terminal, the visible area within the larger buffer of output.
- * Logic for the virtual scroll bar is included in this object.
- */
-export class Viewport extends Disposable implements IViewport {
-  public scrollBarWidth: number = 0;
-  private _currentRowHeight: number = 0;
-  private _currentDeviceCellHeight: number = 0;
-  private _lastRecordedBufferLength: number = 0;
-  private _lastRecordedViewportHeight: number = 0;
-  private _lastRecordedBufferHeight: number = 0;
-  private _lastTouchY: number = 0;
-  private _lastScrollTop: number = 0;
-  private _activeBuffer: IBuffer;
-  private _renderDimensions: IRenderDimensions;
-
-  private _smoothScrollAnimationFrame: number = 0;
-
-  // Stores a partial line amount when scrolling, this is used to keep track of how much of a line
-  // is scrolled so we can "scroll" over partial lines and feel natural on touchpads. This is a
-  // quick fix and could have a more robust solution in place that reset the value when needed.
-  private _wheelPartialScroll: number = 0;
+  protected _onRequestScrollLines = this.register(new EventEmitter<number>());
+  public readonly onRequestScrollLines = this._onRequestScrollLines.event;
 
-  private _refreshAnimationFrame: number | null = null;
-  private _ignoreNextScrollEvent: boolean = false;
-  private _smoothScrollState: ISmoothScrollState = {
-    startTime: 0,
-    origin: -1,
-    target: -1
-  };
+  private _scrollableElement: SmoothScrollableElement;
+  private _styleElement: HTMLStyleElement;
 
-  private _ensureTimeout: number;
-
-  private readonly _onRequestScrollLines = this.register(new EventEmitter<{ amount: number, suppressScrollEvent: boolean }>());
-  public readonly onRequestScrollLines = this._onRequestScrollLines.event;
+  private _queuedAnimationFrame?: number;
+  private _latestYDisp?: number;
+  private _isSyncing: boolean = false;
+  private _isHandlingScroll: boolean = false;
+  private _suppressOnScrollHandler: boolean = false;
 
   constructor(
-    private readonly _viewportElement: HTMLElement,
-    private readonly _scrollArea: HTMLElement,
+    element: HTMLElement,
+    screenElement: HTMLElement,
     @IBufferService private readonly _bufferService: IBufferService,
+    @ICoreBrowserService coreBrowserService: ICoreBrowserService,
+    @ICoreMouseService coreMouseService: ICoreMouseService,
+    @IThemeService themeService: IThemeService,
     @IOptionsService private readonly _optionsService: IOptionsService,
-    @ICharSizeService private readonly _charSizeService: ICharSizeService,
-    @IRenderService private readonly _renderService: IRenderService,
-    @ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService,
-    @IThemeService themeService: IThemeService
+    @IRenderService private readonly _renderService: IRenderService
   ) {
     super();
 
-    // Measure the width of the scrollbar. If it is 0 we can assume it's an OSX overlay scrollbar.
-    // Unfortunately the overlay scrollbar would be hidden underneath the screen element in that
-    // case, therefore we account for a standard amount to make it visible
-    this.scrollBarWidth = (this._viewportElement.offsetWidth - this._scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH;
-    this.register(addDisposableDomListener(this._viewportElement, 'scroll', this._handleScroll.bind(this)));
-
-    // Track properties used in performance critical code manually to avoid using slow getters
-    this._activeBuffer = this._bufferService.buffer;
-    this.register(this._bufferService.buffers.onBufferActivate(e => this._activeBuffer = e.activeBuffer));
-    this._renderDimensions = this._renderService.dimensions;
-    this.register(this._renderService.onDimensionsChange(e => this._renderDimensions = e));
-
-    this._handleThemeChange(themeService.colors);
-    this.register(themeService.onChangeColors(e => this._handleThemeChange(e)));
-    this.register(this._optionsService.onSpecificOptionChange('scrollback', () => this.syncScrollArea()));
-
-    // Perform this async to ensure the ICharSizeService is ready.
-    this._ensureTimeout = window.setTimeout(() => this.syncScrollArea());
+    const scrollable = this.register(new Scrollable({
+      forceIntegerValues: false,
+      smoothScrollDuration: this._optionsService.rawOptions.smoothScrollDuration,
+      // This is used over `IRenderService.addRefreshCallback` since it can be canceled
+      scheduleAtNextAnimationFrame: cb => scheduleAtNextAnimationFrame(coreBrowserService.window, cb)
+    }));
+    this.register(this._optionsService.onSpecificOptionChange('smoothScrollDuration', () => {
+      scrollable.setSmoothScrollDuration(this._optionsService.rawOptions.smoothScrollDuration);
+    }));
+
+    this._scrollableElement = this.register(new SmoothScrollableElement(screenElement, {
+      vertical: ScrollbarVisibility.Auto,
+      horizontal: ScrollbarVisibility.Hidden,
+      useShadows: false,
+      mouseWheelSmoothScroll: true,
+      ...this._getChangeOptions()
+    }, scrollable));
+    this.register(this._optionsService.onMultipleOptionChange([
+      'scrollSensitivity',
+      'fastScrollSensitivity',
+      'overviewRulerWidth'
+    ], () => this._scrollableElement.updateOptions(this._getChangeOptions())));
+    // Don't handle mouse wheel if wheel events are supported by the current mouse prototcol
+    this.register(coreMouseService.onProtocolChange(type => {
+      this._scrollableElement.updateOptions({
+        handleMouseWheel: !(type & CoreMouseEventType.WHEEL)
+      });
+    }));
+
+    this._scrollableElement.setScrollDimensions({ height: 0, scrollHeight: 0 });
+    this.register(runAndSubscribe(themeService.onChangeColors, () => {
+      this._scrollableElement.getDomNode().style.backgroundColor = themeService.colors.background.css;
+    }));
+    element.appendChild(this._scrollableElement.getDomNode());
+    this.register(toDisposable(() => this._scrollableElement.getDomNode().remove()));
+
+    this._styleElement = coreBrowserService.window.document.createElement('style');
+    screenElement.appendChild(this._styleElement);
+    this.register(toDisposable(() => this._styleElement.remove()));
+    this.register(runAndSubscribe(themeService.onChangeColors, () => {
+      this._styleElement.textContent = [
+        `.xterm .xterm-scrollable-element > .scrollbar > .slider {`,
+        `  background: ${themeService.colors.scrollbarSliderBackground.css};`,
+        `}`,
+        `.xterm .xterm-scrollable-element > .scrollbar > .slider:hover {`,
+        `  background: ${themeService.colors.scrollbarSliderHoverBackground.css};`,
+        `}`,
+        `.xterm .xterm-scrollable-element > .scrollbar > .slider.active {`,
+        `  background: ${themeService.colors.scrollbarSliderActiveBackground.css};`,
+        `}`
+      ].join('\n');
+    }));
+
+    this.register(this._bufferService.onResize(() => this._queueSync()));
+    this.register(this._bufferService.onScroll(() => this._sync()));
+
+    this.register(this._scrollableElement.onScroll(e => this._handleScroll(e)));
   }
 
-  private _handleThemeChange(colors: ReadonlyColorSet): void {
-    this._viewportElement.style.backgroundColor = colors.background.css;
-  }
-
-  public reset(): void {
-    this._currentRowHeight = 0;
-    this._currentDeviceCellHeight = 0;
-    this._lastRecordedBufferLength = 0;
-    this._lastRecordedViewportHeight = 0;
-    this._lastRecordedBufferHeight = 0;
-    this._lastTouchY = 0;
-    this._lastScrollTop = 0;
-    // Sync on next animation frame to ensure the new terminal state is used
-    this._coreBrowserService.window.requestAnimationFrame(() => this.syncScrollArea());
-  }
-
-  /**
-   * Refreshes row height, setting line-height, viewport height and scroll area height if
-   * necessary.
-   */
-  private _refresh(immediate: boolean): void {
-    if (immediate) {
-      this._innerRefresh();
-      if (this._refreshAnimationFrame !== null) {
-        this._coreBrowserService.window.cancelAnimationFrame(this._refreshAnimationFrame);
-      }
-      return;
-    }
-    if (this._refreshAnimationFrame === null) {
-      this._refreshAnimationFrame = this._coreBrowserService.window.requestAnimationFrame(() => this._innerRefresh());
-    }
+  public scrollLines(disp: number): void {
+    const pos = this._scrollableElement.getScrollPosition();
+    this._scrollableElement.setScrollPosition({
+      reuseAnimation: true,
+      scrollTop: pos.scrollTop + disp * this._renderService.dimensions.css.cell.height
+    });
   }
 
-  private _innerRefresh(): void {
-    if (this._charSizeService.height > 0) {
-      this._currentRowHeight = this._renderDimensions.device.cell.height / this._coreBrowserService.dpr;
-      this._currentDeviceCellHeight = this._renderDimensions.device.cell.height;
-      this._lastRecordedViewportHeight = this._viewportElement.offsetHeight;
-      const newBufferHeight = Math.round(this._currentRowHeight * this._lastRecordedBufferLength) + (this._lastRecordedViewportHeight - this._renderDimensions.css.canvas.height);
-      if (this._lastRecordedBufferHeight !== newBufferHeight) {
-        this._lastRecordedBufferHeight = newBufferHeight;
-        this._scrollArea.style.height = this._lastRecordedBufferHeight + 'px';
-      }
-    }
-
-    // Sync scrollTop
-    const scrollTop = this._bufferService.buffer.ydisp * this._currentRowHeight;
-    if (this._viewportElement.scrollTop !== scrollTop) {
-      // Ignore the next scroll event which will be triggered by setting the scrollTop as we do not
-      // want this event to scroll the terminal
-      this._ignoreNextScrollEvent = true;
-      this._viewportElement.scrollTop = scrollTop;
+  public scrollToLine(line: number, disableSmoothScroll?: boolean): void {
+    if (disableSmoothScroll) {
+      this._latestYDisp = line;
     }
-
-    this._refreshAnimationFrame = null;
+    this._scrollableElement.setScrollPosition({
+      reuseAnimation: !disableSmoothScroll,
+      scrollTop: line * this._renderService.dimensions.css.cell.height
+    });
   }
 
-  /**
-   * Updates dimensions and synchronizes the scroll area if necessary.
-   */
-  public syncScrollArea(immediate: boolean = false): void {
-    // If buffer height changed
-    if (this._lastRecordedBufferLength !== this._bufferService.buffer.lines.length) {
-      this._lastRecordedBufferLength = this._bufferService.buffer.lines.length;
-      this._refresh(immediate);
-      return;
-    }
-
-    // If viewport height changed
-    if (this._lastRecordedViewportHeight !== this._renderService.dimensions.css.canvas.height) {
-      this._refresh(immediate);
-      return;
-    }
-
-    // If the buffer position doesn't match last scroll top
-    if (this._lastScrollTop !== this._activeBuffer.ydisp * this._currentRowHeight) {
-      this._refresh(immediate);
-      return;
-    }
-
-    // If row height changed
-    if (this._renderDimensions.device.cell.height !== this._currentDeviceCellHeight) {
-      this._refresh(immediate);
-      return;
-    }
+  private _getChangeOptions(): ScrollableElementChangeOptions {
+    return {
+      mouseWheelScrollSensitivity: this._optionsService.rawOptions.scrollSensitivity,
+      fastScrollSensitivity: this._optionsService.rawOptions.fastScrollSensitivity,
+      verticalScrollbarSize: this._optionsService.rawOptions.overviewRulerWidth || ViewportConstants.DEFAULT_SCROLL_BAR_WIDTH
+    };
   }
 
-  /**
-   * Handles scroll events on the viewport, calculating the new viewport and requesting the
-   * terminal to scroll to it.
-   * @param ev The scroll event.
-   */
-  private _handleScroll(ev: Event): void {
-    // Record current scroll top position
-    this._lastScrollTop = this._viewportElement.scrollTop;
-
-    // Don't attempt to scroll if the element is not visible, otherwise scrollTop will be corrupt
-    // which causes the terminal to scroll the buffer to the top
-    if (!this._viewportElement.offsetParent) {
-      return;
+  private _queueSync(ydisp?: number): void {
+    // Update state
+    if (ydisp !== undefined) {
+      this._latestYDisp = ydisp;
     }
 
-    // Ignore the event if it was flagged to ignore (when the source of the event is from Viewport)
-    if (this._ignoreNextScrollEvent) {
-      this._ignoreNextScrollEvent = false;
-      // Still trigger the scroll so lines get refreshed
-      this._onRequestScrollLines.fire({ amount: 0, suppressScrollEvent: true });
+    // Don't queue more than one callback
+    if (this._queuedAnimationFrame !== undefined) {
       return;
     }
-
-    const newRow = Math.round(this._lastScrollTop / this._currentRowHeight);
-    const diff = newRow - this._bufferService.buffer.ydisp;
-    this._onRequestScrollLines.fire({ amount: diff, suppressScrollEvent: true });
+    this._queuedAnimationFrame = this._renderService.addRefreshCallback(() => {
+      this._queuedAnimationFrame = undefined;
+      this._sync(this._latestYDisp);
+    });
   }
 
-  private _smoothScroll(): void {
-    // Check valid state
-    if (this._isDisposed || this._smoothScrollState.origin === -1 || this._smoothScrollState.target === -1) {
+  private _sync(ydisp: number = this._bufferService.buffer.ydisp): void {
+    if (!this._renderService || this._isSyncing) {
       return;
     }
+    this._isSyncing = true;
 
-    // Calculate position complete
-    const percent = this._smoothScrollPercent();
-    this._viewportElement.scrollTop = this._smoothScrollState.origin + Math.round(percent * (this._smoothScrollState.target - this._smoothScrollState.origin));
+    // Ignore any onScroll event that happens as a result of dimensions changing as this should
+    // never cause a scrollLines call, only setScrollPosition can do that.
+    this._suppressOnScrollHandler = true;
+    this._scrollableElement.setScrollDimensions({
+      height: this._renderService.dimensions.css.canvas.height,
+      scrollHeight: this._renderService.dimensions.css.cell.height * this._bufferService.buffer.lines.length
+    });
+    this._suppressOnScrollHandler = false;
 
-    // Continue or finish smooth scroll
-    if (percent < 1) {
-      if (!this._smoothScrollAnimationFrame) {
-        this._smoothScrollAnimationFrame = this._coreBrowserService.window.requestAnimationFrame(() => {
-          this._smoothScrollAnimationFrame = 0;
-          this._smoothScroll();
-        });
-      }
-    } else {
-      this._clearSmoothScrollState();
+    // If ydisp has been changed by some other copmonent (input/buffer), then stop animating smooth
+    // scroll and scroll there immediately.
+    if (ydisp !== this._latestYDisp) {
+      this._scrollableElement.setScrollPosition({
+        scrollTop: ydisp * this._renderService.dimensions.css.cell.height
+      });
     }
-  }
-
-  private _smoothScrollPercent(): number {
-    if (!this._optionsService.rawOptions.smoothScrollDuration || !this._smoothScrollState.startTime) {
-      return 1;
-    }
-    return Math.max(Math.min((Date.now() - this._smoothScrollState.startTime) / this._optionsService.rawOptions.smoothScrollDuration, 1), 0);
-  }
-
-  private _clearSmoothScrollState(): void {
-    this._smoothScrollState.startTime = 0;
-    this._smoothScrollState.origin = -1;
-    this._smoothScrollState.target = -1;
-  }
 
-  /**
-   * Handles bubbling of scroll event in case the viewport has reached top or bottom
-   * @param ev The scroll event.
-   * @param amount The amount scrolled
-   */
-  private _bubbleScroll(ev: Event, amount: number): boolean {
-    const scrollPosFromTop = this._viewportElement.scrollTop + this._lastRecordedViewportHeight;
-    if ((amount < 0 && this._viewportElement.scrollTop !== 0) ||
-      (amount > 0 && scrollPosFromTop < this._lastRecordedBufferHeight)) {
-      if (ev.cancelable) {
-        ev.preventDefault();
-      }
-      return false;
-    }
-    return true;
+    this._isSyncing = false;
   }
 
-  /**
-   * Handles mouse wheel events by adjusting the viewport's scrollTop and delegating the actual
-   * scrolling to `onScroll`, this event needs to be attached manually by the consumer of
-   * `Viewport`.
-   * @param ev The mouse wheel event.
-   */
-  public handleWheel(ev: WheelEvent): boolean {
-    const amount = this._getPixelsScrolled(ev);
-    if (amount === 0) {
-      return false;
-    }
-    if (!this._optionsService.rawOptions.smoothScrollDuration) {
-      this._viewportElement.scrollTop += amount;
-    } else {
-      this._smoothScrollState.startTime = Date.now();
-      if (this._smoothScrollPercent() < 1) {
-        this._smoothScrollState.origin = this._viewportElement.scrollTop;
-        if (this._smoothScrollState.target === -1) {
-          this._smoothScrollState.target = this._viewportElement.scrollTop + amount;
-        } else {
-          this._smoothScrollState.target += amount;
-        }
-        this._smoothScrollState.target = Math.max(Math.min(this._smoothScrollState.target, this._viewportElement.scrollHeight), 0);
-        this._smoothScroll();
-      } else {
-        this._clearSmoothScrollState();
-      }
-    }
-    return this._bubbleScroll(ev, amount);
-  }
-
-  public scrollLines(disp: number): void {
-    if (disp === 0) {
+  private _handleScroll(e: ScrollEvent): void {
+    if (!this._renderService) {
       return;
     }
-    if (!this._optionsService.rawOptions.smoothScrollDuration) {
-      this._onRequestScrollLines.fire({ amount: disp, suppressScrollEvent: false });
-    } else {
-      const amount = disp * this._currentRowHeight;
-      this._smoothScrollState.startTime = Date.now();
-      if (this._smoothScrollPercent() < 1) {
-        this._smoothScrollState.origin = this._viewportElement.scrollTop;
-        this._smoothScrollState.target = this._smoothScrollState.origin + amount;
-        this._smoothScrollState.target = Math.max(Math.min(this._smoothScrollState.target, this._viewportElement.scrollHeight), 0);
-        this._smoothScroll();
-      } else {
-        this._clearSmoothScrollState();
-      }
-    }
-  }
-
-  private _getPixelsScrolled(ev: WheelEvent): number {
-    // Do nothing if it's not a vertical scroll event
-    if (ev.deltaY === 0 || ev.shiftKey) {
-      return 0;
-    }
-
-    // Fallback to WheelEvent.DOM_DELTA_PIXEL
-    let amount = this._applyScrollModifier(ev.deltaY, ev);
-    if (ev.deltaMode === WheelEvent.DOM_DELTA_LINE) {
-      amount *= this._currentRowHeight;
-    } else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
-      amount *= this._currentRowHeight * this._bufferService.rows;
-    }
-    return amount;
-  }
-
-
-  public getBufferElements(startLine: number, endLine?: number): { bufferElements: HTMLElement[], cursorElement?: HTMLElement } {
-    let currentLine: string = '';
-    let cursorElement: HTMLElement | undefined;
-    const bufferElements: HTMLElement[] = [];
-    const end = endLine ?? this._bufferService.buffer.lines.length;
-    const lines = this._bufferService.buffer.lines;
-    for (let i = startLine; i < end; i++) {
-      const line = lines.get(i);
-      if (!line) {
-        continue;
-      }
-      const isWrapped = lines.get(i + 1)?.isWrapped;
-      currentLine += line.translateToString(!isWrapped);
-      if (!isWrapped || i === lines.length - 1) {
-        const div = document.createElement('div');
-        div.textContent = currentLine;
-        bufferElements.push(div);
-        if (currentLine.length > 0) {
-          cursorElement = div;
-        }
-        currentLine = '';
-      }
-    }
-    return { bufferElements, cursorElement };
-  }
-
-  /**
-   * Gets the number of pixels scrolled by the mouse event taking into account what type of delta
-   * is being used.
-   * @param ev The mouse wheel event.
-   */
-  public getLinesScrolled(ev: WheelEvent): number {
-    // Do nothing if it's not a vertical scroll event
-    if (ev.deltaY === 0 || ev.shiftKey) {
-      return 0;
-    }
-
-    // Fallback to WheelEvent.DOM_DELTA_LINE
-    let amount = this._applyScrollModifier(ev.deltaY, ev);
-    if (ev.deltaMode === WheelEvent.DOM_DELTA_PIXEL) {
-      amount /= this._currentRowHeight + 0.0; // Prevent integer division
-      this._wheelPartialScroll += amount;
-      amount = Math.floor(Math.abs(this._wheelPartialScroll)) * (this._wheelPartialScroll > 0 ? 1 : -1);
-      this._wheelPartialScroll %= 1;
-    } else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
-      amount *= this._bufferService.rows;
-    }
-    return amount;
-  }
-
-  private _applyScrollModifier(amount: number, ev: WheelEvent): number {
-    const modifier = this._optionsService.rawOptions.fastScrollModifier;
-    // Multiply the scroll speed when the modifier is down
-    if ((modifier === 'alt' && ev.altKey) ||
-      (modifier === 'ctrl' && ev.ctrlKey) ||
-      (modifier === 'shift' && ev.shiftKey)) {
-      return amount * this._optionsService.rawOptions.fastScrollSensitivity * this._optionsService.rawOptions.scrollSensitivity;
+    if (this._isHandlingScroll || this._suppressOnScrollHandler) {
+      return;
     }
-
-    return amount * this._optionsService.rawOptions.scrollSensitivity;
-  }
-
-  /**
-   * Handles the touchstart event, recording the touch occurred.
-   * @param ev The touch event.
-   */
-  public handleTouchStart(ev: TouchEvent): void {
-    this._lastTouchY = ev.touches[0].pageY;
-  }
-
-  /**
-   * Handles the touchmove event, scrolling the viewport if the position shifted.
-   * @param ev The touch event.
-   */
-  public handleTouchMove(ev: TouchEvent): boolean {
-    const deltaY = this._lastTouchY - ev.touches[0].pageY;
-    this._lastTouchY = ev.touches[0].pageY;
-    if (deltaY === 0) {
-      return false;
+    this._isHandlingScroll = true;
+    const newRow = Math.round(e.scrollTop / this._renderService.dimensions.css.cell.height);
+    const diff = newRow - this._bufferService.buffer.ydisp;
+    if (diff !== 0) {
+      this._latestYDisp = newRow;
+      this._onRequestScrollLines.fire(diff);
     }
-    this._viewportElement.scrollTop += deltaY;
-    return this._bubbleScroll(ev, deltaY);
-  }
-
-  public dispose(): void {
-    clearTimeout(this._ensureTimeout);
+    this._isHandlingScroll = false;
   }
 }
diff --git a/src/browser/decorations/OverviewRulerRenderer.ts b/src/browser/decorations/OverviewRulerRenderer.ts
index 103d5d9a61..8009522220 100644
--- a/src/browser/decorations/OverviewRulerRenderer.ts
+++ b/src/browser/decorations/OverviewRulerRenderer.ts
@@ -4,10 +4,14 @@
  *--------------------------------------------------------------------------------------------*/
 
 import { ColorZoneStore, IColorZone, IColorZoneStore } from 'browser/decorations/ColorZoneStore';
-import { ICoreBrowserService, IRenderService } from 'browser/services/Services';
+import { ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
 import { Disposable, toDisposable } from 'common/Lifecycle';
 import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
 
+const enum Constants {
+  OVERVIEW_RULER_BORDER_WIDTH = 1
+}
+
 // Helper objects to avoid excessive calculation and garbage collection during rendering. These are
 // static values for each render and can be accessed using the decoration position as the key.
 const drawHeight = {
@@ -51,6 +55,7 @@ export class OverviewRulerRenderer extends Disposable {
     @IDecorationService private readonly _decorationService: IDecorationService,
     @IRenderService private readonly _renderService: IRenderService,
     @IOptionsService private readonly _optionsService: IOptionsService,
+    @IThemeService private readonly _themeService: IThemeService,
     @ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService
   ) {
     super();
@@ -58,33 +63,18 @@ export class OverviewRulerRenderer extends Disposable {
     this._canvas.classList.add('xterm-decoration-overview-ruler');
     this._refreshCanvasDimensions();
     this._viewportElement.parentElement?.insertBefore(this._canvas, this._viewportElement);
+    this.register(toDisposable(() => this._canvas?.remove()));
+
     const ctx = this._canvas.getContext('2d');
     if (!ctx) {
       throw new Error('Ctx cannot be null');
     } else {
       this._ctx = ctx;
     }
-    this._registerDecorationListeners();
-    this._registerBufferChangeListeners();
-    this._registerDimensionChangeListeners();
-    this.register(toDisposable(() => {
-      this._canvas?.remove();
-    }));
-  }
 
-  /**
-   * On decoration add or remove, redraw
-   */
-  private _registerDecorationListeners(): void {
     this.register(this._decorationService.onDecorationRegistered(() => this._queueRefresh(undefined, true)));
     this.register(this._decorationService.onDecorationRemoved(() => this._queueRefresh(undefined, true)));
-  }
 
-  /**
-   * On buffer change, redraw
-   * and hide the canvas if the alt buffer is active
-   */
-  private _registerBufferChangeListeners(): void {
     this.register(this._renderService.onRenderedViewportChange(() => this._queueRefresh()));
     this.register(this._bufferService.buffers.onBufferActivate(() => {
       this._canvas!.style.display = this._bufferService.buffer === this._bufferService.buffers.alt ? 'none' : 'block';
@@ -95,31 +85,25 @@ export class OverviewRulerRenderer extends Disposable {
         this._refreshColorZonePadding();
       }
     }));
-  }
-  /**
-   * On dimension change, update canvas dimensions
-   * and then redraw
-   */
-  private _registerDimensionChangeListeners(): void {
-    // container height changed
+
+    // Container height changed
     this.register(this._renderService.onRender((): void => {
       if (!this._containerHeight || this._containerHeight !== this._screenElement.clientHeight) {
         this._queueRefresh(true);
         this._containerHeight = this._screenElement.clientHeight;
       }
     }));
-    // overview ruler width changed
-    this.register(this._optionsService.onSpecificOptionChange('overviewRulerWidth', () => this._queueRefresh(true)));
-    // device pixel ratio changed
+
     this.register(this._coreBrowserService.onDprChange(() => this._queueRefresh(true)));
-    // set the canvas dimensions
+    this.register(this._optionsService.onSpecificOptionChange('overviewRulerWidth', () => this._queueRefresh(true)));
+    this.register(this._themeService.onChangeColors(() => this._queueRefresh()));
     this._queueRefresh(true);
   }
 
   private _refreshDrawConstants(): void {
     // width
-    const outerWidth = Math.floor(this._canvas.width / 3);
-    const innerWidth = Math.ceil(this._canvas.width / 3);
+    const outerWidth = Math.floor((this._canvas.width - Constants.OVERVIEW_RULER_BORDER_WIDTH) / 3);
+    const innerWidth = Math.ceil((this._canvas.width - Constants.OVERVIEW_RULER_BORDER_WIDTH) / 3);
     drawWidth.full = this._canvas.width;
     drawWidth.left = outerWidth;
     drawWidth.center = innerWidth;
@@ -127,10 +111,10 @@ export class OverviewRulerRenderer extends Disposable {
     // height
     this._refreshDrawHeightConstants();
     // x
-    drawX.full = 0;
-    drawX.left = 0;
-    drawX.center = drawWidth.left;
-    drawX.right = drawWidth.left + drawWidth.center;
+    drawX.full = Constants.OVERVIEW_RULER_BORDER_WIDTH;
+    drawX.left = Constants.OVERVIEW_RULER_BORDER_WIDTH;
+    drawX.center = Constants.OVERVIEW_RULER_BORDER_WIDTH + drawWidth.left;
+    drawX.right = Constants.OVERVIEW_RULER_BORDER_WIDTH + drawWidth.left + drawWidth.center;
   }
 
   private _refreshDrawHeightConstants(): void {
@@ -173,6 +157,7 @@ export class OverviewRulerRenderer extends Disposable {
       this._colorZoneStore.addDecoration(decoration);
     }
     this._ctx.lineWidth = 1;
+    this._renderRulerOutline();
     const zones = this._colorZoneStore.zones;
     for (const zone of zones) {
       if (zone.position !== 'full') {
@@ -188,6 +173,11 @@ export class OverviewRulerRenderer extends Disposable {
     this._shouldUpdateAnchor = false;
   }
 
+  private _renderRulerOutline(): void {
+    this._ctx.fillStyle = this._themeService.colors.overviewRulerBorder.css;
+    this._ctx.fillRect(0, 0, Constants.OVERVIEW_RULER_BORDER_WIDTH, this._canvas.height);
+  }
+
   private _renderColorZone(zone: IColorZone): void {
     this._ctx.fillStyle = zone.color;
     this._ctx.fillRect(
diff --git a/src/browser/renderer/shared/CharAtlasUtils.ts b/src/browser/renderer/shared/CharAtlasUtils.ts
index f8fe9104fb..fb3fae88f4 100644
--- a/src/browser/renderer/shared/CharAtlasUtils.ts
+++ b/src/browser/renderer/shared/CharAtlasUtils.ts
@@ -21,6 +21,10 @@ export function generateConfig(deviceCellWidth: number, deviceCellHeight: number
     selectionBackgroundOpaque: NULL_COLOR,
     selectionInactiveBackgroundTransparent: NULL_COLOR,
     selectionInactiveBackgroundOpaque: NULL_COLOR,
+    overviewRulerBorder: NULL_COLOR,
+    scrollbarSliderBackground: NULL_COLOR,
+    scrollbarSliderHoverBackground: NULL_COLOR,
+    scrollbarSliderActiveBackground: NULL_COLOR,
     // For the static char atlas, we only use the first 16 colors, but we need all 256 for the
     // dynamic character atlas.
     ansi: colors.ansi.slice(),
diff --git a/src/browser/services/ThemeService.ts b/src/browser/services/ThemeService.ts
index 31b1192c82..3e6dd4811c 100644
--- a/src/browser/services/ThemeService.ts
+++ b/src/browser/services/ThemeService.ts
@@ -23,11 +23,12 @@ interface IRestoreColorSet {
 const DEFAULT_FOREGROUND = css.toColor('#ffffff');
 const DEFAULT_BACKGROUND = css.toColor('#000000');
 const DEFAULT_CURSOR = css.toColor('#ffffff');
-const DEFAULT_CURSOR_ACCENT = css.toColor('#000000');
+const DEFAULT_CURSOR_ACCENT = DEFAULT_BACKGROUND;
 const DEFAULT_SELECTION = {
   css: 'rgba(255, 255, 255, 0.3)',
   rgba: 0xFFFFFF4D
 };
+const DEFAULT_OVERVIEW_RULER_BORDER = DEFAULT_FOREGROUND;
 
 export class ThemeService extends Disposable implements IThemeService {
   public serviceBrand: undefined;
@@ -57,6 +58,10 @@ export class ThemeService extends Disposable implements IThemeService {
       selectionBackgroundOpaque: color.blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION),
       selectionInactiveBackgroundTransparent: DEFAULT_SELECTION,
       selectionInactiveBackgroundOpaque: color.blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION),
+      scrollbarSliderBackground: color.opacity(DEFAULT_FOREGROUND, 0.2),
+      scrollbarSliderHoverBackground: color.opacity(DEFAULT_FOREGROUND, 0.4),
+      scrollbarSliderActiveBackground: color.opacity(DEFAULT_FOREGROUND, 0.5),
+      overviewRulerBorder: DEFAULT_FOREGROUND,
       ansi: DEFAULT_ANSI_COLORS.slice(),
       contrastCache: this._contrastCache,
       halfContrastCache: this._halfContrastCache
@@ -100,6 +105,10 @@ export class ThemeService extends Disposable implements IThemeService {
       const opacity = 0.3;
       colors.selectionInactiveBackgroundTransparent = color.opacity(colors.selectionInactiveBackgroundTransparent, opacity);
     }
+    colors.scrollbarSliderBackground = parseColor(theme.scrollbarSliderBackground, color.opacity(colors.foreground, 0.2));
+    colors.scrollbarSliderHoverBackground = parseColor(theme.scrollbarSliderHoverBackground, color.opacity(colors.foreground, 0.4));
+    colors.scrollbarSliderActiveBackground = parseColor(theme.scrollbarSliderActiveBackground, color.opacity(colors.foreground, 0.5));
+    colors.overviewRulerBorder = parseColor(theme.overviewRulerBorder, DEFAULT_OVERVIEW_RULER_BORDER);
     colors.ansi = DEFAULT_ANSI_COLORS.slice();
     colors.ansi[0] = parseColor(theme.black, DEFAULT_ANSI_COLORS[0]);
     colors.ansi[1] = parseColor(theme.red, DEFAULT_ANSI_COLORS[1]);
diff --git a/src/browser/shared/Constants.ts b/src/browser/shared/Constants.ts
new file mode 100644
index 0000000000..58b7d2f73a
--- /dev/null
+++ b/src/browser/shared/Constants.ts
@@ -0,0 +1,8 @@
+/**
+ * Copyright (c) 2024 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+export const enum ViewportConstants {
+  DEFAULT_SCROLL_BAR_WIDTH = 14
+}
diff --git a/src/browser/tsconfig.json b/src/browser/tsconfig.json
index 673a55a6cb..38854e260f 100644
--- a/src/browser/tsconfig.json
+++ b/src/browser/tsconfig.json
@@ -7,11 +7,13 @@
     ],
     "outDir": "../../out",
     "types": [
-      "../../node_modules/@types/mocha"
+      "../../node_modules/@types/mocha",
+      "../vs/typings/thenable.d.ts"
     ],
     "baseUrl": "..",
     "paths": {
-      "common/*": [ "./common/*" ]
+      "common/*": [ "./common/*" ],
+      "vs/*": [ "./vs/*" ]
     }
   },
   "include": [
@@ -19,6 +21,7 @@
     "../../typings/xterm.d.ts"
   ],
   "references": [
-    { "path": "../common" }
+    { "path": "../common" },
+    { "path": "../vs" }
   ]
 }
diff --git a/src/common/CoreTerminal.ts b/src/common/CoreTerminal.ts
index 327b8bc2f7..132be05bcb 100644
--- a/src/common/CoreTerminal.ts
+++ b/src/common/CoreTerminal.ts
@@ -27,7 +27,7 @@ import { InstantiationService } from 'common/services/InstantiationService';
 import { LogService } from 'common/services/LogService';
 import { BufferService, MINIMUM_COLS, MINIMUM_ROWS } from 'common/services/BufferService';
 import { OptionsService } from 'common/services/OptionsService';
-import { IDisposable, IAttributeData, ICoreTerminal, IScrollEvent, ScrollSource } from 'common/Types';
+import { IDisposable, IAttributeData, ICoreTerminal, IScrollEvent } from 'common/Types';
 import { CoreService } from 'common/services/CoreService';
 import { EventEmitter, IEvent, forwardEvent } from 'common/EventEmitter';
 import { CoreMouseService } from 'common/services/CoreMouseService';
@@ -130,18 +130,13 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
     this.register(forwardEvent(this._bufferService.onResize, this._onResize));
     this.register(forwardEvent(this.coreService.onData, this._onData));
     this.register(forwardEvent(this.coreService.onBinary, this._onBinary));
-    this.register(this.coreService.onRequestScrollToBottom(() => this.scrollToBottom()));
+    this.register(this.coreService.onRequestScrollToBottom(() => this.scrollToBottom(true)));
     this.register(this.coreService.onUserInput(() =>  this._writeBuffer.handleUserInput()));
     this.register(this.optionsService.onMultipleOptionChange(['windowsMode', 'windowsPty'], () => this._handleWindowsPtyOptionChange()));
-    this.register(this._bufferService.onScroll(event => {
-      this._onScroll.fire({ position: this._bufferService.buffer.ydisp, source: ScrollSource.TERMINAL });
+    this.register(this._bufferService.onScroll(() => {
+      this._onScroll.fire({ position: this._bufferService.buffer.ydisp });
       this._inputHandler.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom);
     }));
-    this.register(this._inputHandler.onScroll(event => {
-      this._onScroll.fire({ position: this._bufferService.buffer.ydisp, source: ScrollSource.TERMINAL });
-      this._inputHandler.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom);
-    }));
-
     // Setup WriteBuffer
     this._writeBuffer = this.register(new WriteBuffer((data, promiseResult) => this._inputHandler.parse(data, promiseResult)));
     this.register(forwardEvent(this._writeBuffer.onWriteParsed, this._onWriteParsed));
@@ -198,10 +193,9 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
    * @param suppressScrollEvent Don't emit the scroll event as scrollLines. This is used to avoid
    * unwanted events being handled by the viewport when the event was triggered from the viewport
    * originally.
-   * @param source Which component the event came from.
    */
-  public scrollLines(disp: number, suppressScrollEvent?: boolean, source?: ScrollSource): void {
-    this._bufferService.scrollLines(disp, suppressScrollEvent, source);
+  public scrollLines(disp: number, suppressScrollEvent?: boolean): void {
+    this._bufferService.scrollLines(disp, suppressScrollEvent);
   }
 
   public scrollPages(pageCount: number): void {
@@ -212,7 +206,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
     this.scrollLines(-this._bufferService.buffer.ydisp);
   }
 
-  public scrollToBottom(): void {
+  public scrollToBottom(disableSmoothScroll?: boolean): void {
     this.scrollLines(this._bufferService.buffer.ybase - this._bufferService.buffer.ydisp);
   }
 
diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts
index 39e13fbac3..fbf3ebcd35 100644
--- a/src/common/TestUtils.test.ts
+++ b/src/common/TestUtils.test.ts
@@ -54,6 +54,7 @@ export class MockBufferService implements IBufferService {
 }
 
 export class MockCoreMouseService implements ICoreMouseService {
+  public serviceBrand: any;
   public areMouseEventsActive: boolean = false;
   public activeEncoding: string = '';
   public activeProtocol: string = '';
diff --git a/src/common/Types.ts b/src/common/Types.ts
index 8b32067e9c..f98a7d3eb7 100644
--- a/src/common/Types.ts
+++ b/src/common/Types.ts
@@ -60,12 +60,6 @@ export interface IKeyboardEvent {
 
 export interface IScrollEvent {
   position: number;
-  source: ScrollSource;
-}
-
-export const enum ScrollSource {
-  TERMINAL,
-  VIEWPORT,
 }
 
 export interface ICircularList<T> {
diff --git a/src/common/services/BufferService.ts b/src/common/services/BufferService.ts
index d20d0ceac0..77af447a69 100644
--- a/src/common/services/BufferService.ts
+++ b/src/common/services/BufferService.ts
@@ -5,7 +5,7 @@
 
 import { EventEmitter } from 'common/EventEmitter';
 import { Disposable } from 'common/Lifecycle';
-import { IAttributeData, IBufferLine, ScrollSource } from 'common/Types';
+import { IAttributeData, IBufferLine } from 'common/Types';
 import { BufferSet } from 'common/buffer/BufferSet';
 import { IBuffer, IBufferSet } from 'common/buffer/Types';
 import { IBufferService, IOptionsService } from 'common/services/Services';
@@ -125,7 +125,7 @@ export class BufferService extends Disposable implements IBufferService {
    * to avoid unwanted events being handled by the viewport when the event was triggered from the
    * viewport originally.
    */
-  public scrollLines(disp: number, suppressScrollEvent?: boolean, source?: ScrollSource): void {
+  public scrollLines(disp: number, suppressScrollEvent?: boolean): void {
     const buffer = this.buffer;
     if (disp < 0) {
       if (buffer.ydisp === 0) {
diff --git a/src/common/services/CoreMouseService.ts b/src/common/services/CoreMouseService.ts
index fd880a5d94..052353bcab 100644
--- a/src/common/services/CoreMouseService.ts
+++ b/src/common/services/CoreMouseService.ts
@@ -167,6 +167,8 @@ const DEFAULT_ENCODINGS: { [key: string]: CoreMouseEncoding } = {
  * To send a mouse event call `triggerMouseEvent`.
  */
 export class CoreMouseService extends Disposable implements ICoreMouseService {
+  public serviceBrand: any;
+
   private _protocols: { [name: string]: ICoreMouseProtocol } = {};
   private _encodings: { [name: string]: CoreMouseEncoding } = {};
   private _activeProtocol: string = '';
diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts
index 210a0afb08..4b3c4e3f1a 100644
--- a/src/common/services/Services.ts
+++ b/src/common/services/Services.ts
@@ -3,11 +3,11 @@
  * @license MIT
  */
 
+import { IDecoration, IDecorationOptions, ILinkHandler, ILogger, IWindowsPty } from '@xterm/xterm';
 import { IEvent, IEventEmitter } from 'common/EventEmitter';
+import { CoreMouseEncoding, CoreMouseEventType, CursorInactiveStyle, CursorStyle, IAttributeData, ICharset, IColor, ICoreMouseEvent, ICoreMouseProtocol, IDecPrivateModes, IDisposable, IModes, IOscLinkData, IWindowOptions } from 'common/Types';
 import { IBuffer, IBufferSet } from 'common/buffer/Types';
-import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions, IModes, IAttributeData, ScrollSource, IDisposable, IColor, CursorStyle, CursorInactiveStyle, IOscLinkData } from 'common/Types';
 import { createDecorator } from 'common/services/ServiceRegistry';
-import { IDecorationOptions, IDecoration, ILinkHandler, IWindowsPty, ILogger } from '@xterm/xterm';
 
 export const IBufferService = createDecorator<IBufferService>('BufferService');
 export interface IBufferService {
@@ -21,13 +21,15 @@ export interface IBufferService {
   onResize: IEvent<{ cols: number, rows: number }>;
   onScroll: IEvent<number>;
   scroll(eraseAttr: IAttributeData, isWrapped?: boolean): void;
-  scrollLines(disp: number, suppressScrollEvent?: boolean, source?: ScrollSource): void;
+  scrollLines(disp: number, suppressScrollEvent?: boolean): void;
   resize(cols: number, rows: number): void;
   reset(): void;
 }
 
 export const ICoreMouseService = createDecorator<ICoreMouseService>('CoreMouseService');
 export interface ICoreMouseService {
+  serviceBrand: undefined;
+
   activeProtocol: string;
   activeEncoding: string;
   areMouseEventsActive: boolean;
@@ -219,6 +221,7 @@ export interface ITerminalOptions {
   disableStdin?: boolean;
   documentOverride?: any | null;
   drawBoldTextInBrightColors?: boolean;
+  /** @deprecated No longer supported */
   fastScrollModifier?: 'none' | 'alt' | 'ctrl' | 'shift';
   fastScrollSensitivity?: number;
   fontSize?: number;
@@ -263,6 +266,10 @@ export interface ITheme {
   selectionForeground?: string;
   selectionBackground?: string;
   selectionInactiveBackground?: string;
+  scrollbarSliderBackground?: string;
+  scrollbarSliderHoverBackground?: string;
+  scrollbarSliderActiveBackground?: string;
+  overviewRulerBorder?: string;
   black?: string;
   red?: string;
   green?: string;
diff --git a/src/headless/Terminal.ts b/src/headless/Terminal.ts
index 66040756f6..0b078ba8f6 100644
--- a/src/headless/Terminal.ts
+++ b/src/headless/Terminal.ts
@@ -25,7 +25,7 @@ import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
 import { IBuffer } from 'common/buffer/Types';
 import { CoreTerminal } from 'common/CoreTerminal';
 import { EventEmitter, forwardEvent } from 'common/EventEmitter';
-import { IMarker, ITerminalOptions, ScrollSource } from 'common/Types';
+import { IMarker, ITerminalOptions } from 'common/Types';
 
 export class Terminal extends CoreTerminal {
   private readonly _onBell = this.register(new EventEmitter<void>());
@@ -115,7 +115,7 @@ export class Terminal extends CoreTerminal {
     for (let i = 1; i < this.rows; i++) {
       this.buffer.lines.push(this.buffer.getBlankLine(DEFAULT_ATTR_DATA));
     }
-    this._onScroll.fire({ position: this.buffer.ydisp, source: ScrollSource.TERMINAL });
+    this._onScroll.fire({ position: this.buffer.ydisp });
   }
 
   /**
diff --git a/src/vs/.gitignore b/src/vs/.gitignore
new file mode 100644
index 0000000000..83e8188bf7
--- /dev/null
+++ b/src/vs/.gitignore
@@ -0,0 +1,13 @@
+temp/
+
+# Tests
+base/test/
+base/**/test/
+
+# Unwanted modules
+base/node/
+base/parts/
+base/worker/
+
+# Binary files
+base/browser/ui/codicons/codicon/codicon.ttf
diff --git a/src/vs/README.md b/src/vs/README.md
new file mode 100644
index 0000000000..860ba53e3a
--- /dev/null
+++ b/src/vs/README.md
@@ -0,0 +1,23 @@
+This folder contains the `base/` module from the [Visual Studio Code repository](https://github.com/microsoft/vscode) which has many helpers that are useful to xterm.js.
+
+Rarely we want to update these sources when an important bug is fixed upstream or when there is a new feature we want to leverage. To update against upstream:
+
+```
+./bin/vs_base_update.ps1
+```
+
+If new functions are being used from the project then import them from another project.
+
+Before committing we need to clean up the diff so that files that aren't being used are not inlcuded. The following script uses the typescript compiler to find any files that are not being imported into the project:
+
+```
+node ./bin/vs_base_find_unused.js
+```
+
+The last step is to do a once over of the resulting bundled xterm.js file to ensure it isn't too large:
+
+1. Run `yarn esbuild`
+2. Open up `xterm.mjs`
+3. Search for `src/vs/base/`
+
+This will show you all the parts of base that will be included in the final minified bundle. Unfortunately tree shaking doesn't find everything, be on the lookout for large arrays or classes that aren't being used. If your editor has find decorations in the scroll bar it's easy to find which parts of base are consuming a lot of lines.
diff --git a/src/vs/base/browser/browser.ts b/src/vs/base/browser/browser.ts
new file mode 100644
index 0000000000..a34f4791d2
--- /dev/null
+++ b/src/vs/base/browser/browser.ts
@@ -0,0 +1,141 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { CodeWindow, mainWindow } from 'vs/base/browser/window';
+import { Emitter } from 'vs/base/common/event';
+
+class WindowManager {
+
+	static readonly INSTANCE = new WindowManager();
+
+	// --- Zoom Level
+
+	private readonly mapWindowIdToZoomLevel = new Map<number, number>();
+
+	private readonly _onDidChangeZoomLevel = new Emitter<number>();
+	readonly onDidChangeZoomLevel = this._onDidChangeZoomLevel.event;
+
+	getZoomLevel(targetWindow: Window): number {
+		return this.mapWindowIdToZoomLevel.get(this.getWindowId(targetWindow)) ?? 0;
+	}
+	setZoomLevel(zoomLevel: number, targetWindow: Window): void {
+		if (this.getZoomLevel(targetWindow) === zoomLevel) {
+			return;
+		}
+
+		const targetWindowId = this.getWindowId(targetWindow);
+		this.mapWindowIdToZoomLevel.set(targetWindowId, zoomLevel);
+		this._onDidChangeZoomLevel.fire(targetWindowId);
+	}
+
+	// --- Zoom Factor
+
+	private readonly mapWindowIdToZoomFactor = new Map<number, number>();
+
+	getZoomFactor(targetWindow: Window): number {
+		return this.mapWindowIdToZoomFactor.get(this.getWindowId(targetWindow)) ?? 1;
+	}
+	setZoomFactor(zoomFactor: number, targetWindow: Window): void {
+		this.mapWindowIdToZoomFactor.set(this.getWindowId(targetWindow), zoomFactor);
+	}
+
+	// --- Fullscreen
+
+	private readonly _onDidChangeFullscreen = new Emitter<number>();
+	readonly onDidChangeFullscreen = this._onDidChangeFullscreen.event;
+
+	private readonly mapWindowIdToFullScreen = new Map<number, boolean>();
+
+	setFullscreen(fullscreen: boolean, targetWindow: Window): void {
+		if (this.isFullscreen(targetWindow) === fullscreen) {
+			return;
+		}
+
+		const windowId = this.getWindowId(targetWindow);
+		this.mapWindowIdToFullScreen.set(windowId, fullscreen);
+		this._onDidChangeFullscreen.fire(windowId);
+	}
+	isFullscreen(targetWindow: Window): boolean {
+		return !!this.mapWindowIdToFullScreen.get(this.getWindowId(targetWindow));
+	}
+
+	private getWindowId(targetWindow: Window): number {
+		return (targetWindow as CodeWindow).vscodeWindowId;
+	}
+}
+
+export function addMatchMediaChangeListener(targetWindow: Window, query: string | MediaQueryList, callback: (this: MediaQueryList, ev: MediaQueryListEvent) => any): void {
+	if (typeof query === 'string') {
+		query = targetWindow.matchMedia(query);
+	}
+	query.addEventListener('change', callback);
+}
+
+/** A zoom index, e.g. 1, 2, 3 */
+export function setZoomLevel(zoomLevel: number, targetWindow: Window): void {
+	WindowManager.INSTANCE.setZoomLevel(zoomLevel, targetWindow);
+}
+export function getZoomLevel(targetWindow: Window): number {
+	return WindowManager.INSTANCE.getZoomLevel(targetWindow);
+}
+export const onDidChangeZoomLevel = WindowManager.INSTANCE.onDidChangeZoomLevel;
+
+/** The zoom scale for an index, e.g. 1, 1.2, 1.4 */
+export function getZoomFactor(targetWindow: Window): number {
+	return WindowManager.INSTANCE.getZoomFactor(targetWindow);
+}
+export function setZoomFactor(zoomFactor: number, targetWindow: Window): void {
+	WindowManager.INSTANCE.setZoomFactor(zoomFactor, targetWindow);
+}
+
+export function setFullscreen(fullscreen: boolean, targetWindow: Window): void {
+	WindowManager.INSTANCE.setFullscreen(fullscreen, targetWindow);
+}
+export function isFullscreen(targetWindow: Window): boolean {
+	return WindowManager.INSTANCE.isFullscreen(targetWindow);
+}
+export const onDidChangeFullscreen = WindowManager.INSTANCE.onDidChangeFullscreen;
+
+const userAgent = typeof navigator === 'object' ? navigator.userAgent : '';
+
+export const isFirefox = (userAgent.indexOf('Firefox') >= 0);
+export const isWebKit = (userAgent.indexOf('AppleWebKit') >= 0);
+export const isChrome = (userAgent.indexOf('Chrome') >= 0);
+export const isSafari = (!isChrome && (userAgent.indexOf('Safari') >= 0));
+export const isWebkitWebView = (!isChrome && !isSafari && isWebKit);
+export const isElectron = (userAgent.indexOf('Electron/') >= 0);
+export const isAndroid = (userAgent.indexOf('Android') >= 0);
+
+let standalone = false;
+if (typeof mainWindow.matchMedia === 'function') {
+	const standaloneMatchMedia = mainWindow.matchMedia('(display-mode: standalone) or (display-mode: window-controls-overlay)');
+	const fullScreenMatchMedia = mainWindow.matchMedia('(display-mode: fullscreen)');
+	standalone = standaloneMatchMedia.matches;
+	addMatchMediaChangeListener(mainWindow, standaloneMatchMedia, ({ matches }) => {
+		// entering fullscreen would change standaloneMatchMedia.matches to false
+		// if standalone is true (running as PWA) and entering fullscreen, skip this change
+		if (standalone && fullScreenMatchMedia.matches) {
+			return;
+		}
+		// otherwise update standalone (browser to PWA or PWA to browser)
+		standalone = matches;
+	});
+}
+export function isStandalone(): boolean {
+	return standalone;
+}
+
+// Visible means that the feature is enabled, not necessarily being rendered
+// e.g. visible is true even in fullscreen mode where the controls are hidden
+// See docs at https://developer.mozilla.org/en-US/docs/Web/API/WindowControlsOverlay/visible
+export function isWCOEnabled(): boolean {
+	return (navigator as any)?.windowControlsOverlay?.visible;
+}
+
+// Returns the bounding rect of the titlebar area if it is supported and defined
+// See docs at https://developer.mozilla.org/en-US/docs/Web/API/WindowControlsOverlay/getTitlebarAreaRect
+export function getWCOBoundingRect(): DOMRect | undefined {
+	return (navigator as any)?.windowControlsOverlay?.getTitlebarAreaRect();
+}
diff --git a/src/vs/base/browser/canIUse.ts b/src/vs/base/browser/canIUse.ts
new file mode 100644
index 0000000000..b5b648f6c5
--- /dev/null
+++ b/src/vs/base/browser/canIUse.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 * as browser from 'vs/base/browser/browser';
+import { mainWindow } from 'vs/base/browser/window';
+import * as platform from 'vs/base/common/platform';
+
+export const enum KeyboardSupport {
+	Always,
+	FullScreen,
+	None
+}
+
+const safeNavigator = typeof navigator === 'object' ? navigator : {} as { [key: string]: any };
+
+/**
+ * Browser feature we can support in current platform, browser and environment.
+ */
+export const BrowserFeatures = {
+	clipboard: {
+		writeText: (
+			platform.isNative
+			|| (document.queryCommandSupported && document.queryCommandSupported('copy'))
+			|| !!(safeNavigator && safeNavigator.clipboard && safeNavigator.clipboard.writeText)
+		),
+		readText: (
+			platform.isNative
+			|| !!(safeNavigator && safeNavigator.clipboard && safeNavigator.clipboard.readText)
+		)
+	},
+	keyboard: (() => {
+		if (platform.isNative || browser.isStandalone()) {
+			return KeyboardSupport.Always;
+		}
+
+		if ((<any>safeNavigator).keyboard || browser.isSafari) {
+			return KeyboardSupport.FullScreen;
+		}
+
+		return KeyboardSupport.None;
+	})(),
+
+	// 'ontouchstart' in window always evaluates to true with typescript's modern typings. This causes `window` to be
+	// `never` later in `window.navigator`. That's why we need the explicit `window as Window` cast
+	touch: 'ontouchstart' in mainWindow || safeNavigator.maxTouchPoints > 0,
+	pointerEvents: mainWindow.PointerEvent && ('ontouchstart' in mainWindow || navigator.maxTouchPoints > 0)
+};
diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts
new file mode 100644
index 0000000000..eb6d73c32c
--- /dev/null
+++ b/src/vs/base/browser/dom.ts
@@ -0,0 +1,2369 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import * as browser from 'vs/base/browser/browser';
+import { BrowserFeatures } from 'vs/base/browser/canIUse';
+import { IKeyboardEvent, StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
+import { IMouseEvent, StandardMouseEvent } from 'vs/base/browser/mouseEvent';
+import { AbstractIdleValue, IntervalTimer, TimeoutTimer, _runWhenIdle, IdleDeadline } from 'vs/base/common/async';
+import { onUnexpectedError } from 'vs/base/common/errors';
+import * as event from 'vs/base/common/event';
+import { KeyCode } from 'vs/base/common/keyCodes';
+import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
+import * as platform from 'vs/base/common/platform';
+import { hash } from 'vs/base/common/hash';
+import { CodeWindow, ensureCodeWindow, mainWindow } from 'vs/base/browser/window';
+import { isPointWithinTriangle } from 'vs/base/common/numbers';
+
+export interface IRegisteredCodeWindow {
+	readonly window: CodeWindow;
+	readonly disposables: DisposableStore;
+}
+
+//# region Multi-Window Support Utilities
+
+export const {
+	registerWindow,
+	getWindow,
+	getDocument,
+	getWindows,
+	getWindowsCount,
+	getWindowId,
+	getWindowById,
+	hasWindow,
+	onDidRegisterWindow,
+	onWillUnregisterWindow,
+	onDidUnregisterWindow
+} = (function () {
+	const windows = new Map<number, IRegisteredCodeWindow>();
+
+	ensureCodeWindow(mainWindow, 1);
+	const mainWindowRegistration = { window: mainWindow, disposables: new DisposableStore() };
+	windows.set(mainWindow.vscodeWindowId, mainWindowRegistration);
+
+	const onDidRegisterWindow = new event.Emitter<IRegisteredCodeWindow>();
+	const onDidUnregisterWindow = new event.Emitter<CodeWindow>();
+	const onWillUnregisterWindow = new event.Emitter<CodeWindow>();
+
+	function getWindowById(windowId: number): IRegisteredCodeWindow | undefined;
+	function getWindowById(windowId: number | undefined, fallbackToMain: true): IRegisteredCodeWindow;
+	function getWindowById(windowId: number | undefined, fallbackToMain?: boolean): IRegisteredCodeWindow | undefined {
+		const window = typeof windowId === 'number' ? windows.get(windowId) : undefined;
+
+		return window ?? (fallbackToMain ? mainWindowRegistration : undefined);
+	}
+
+	return {
+		onDidRegisterWindow: onDidRegisterWindow.event,
+		onWillUnregisterWindow: onWillUnregisterWindow.event,
+		onDidUnregisterWindow: onDidUnregisterWindow.event,
+		registerWindow(window: CodeWindow): IDisposable {
+			if (windows.has(window.vscodeWindowId)) {
+				return Disposable.None;
+			}
+
+			const disposables = new DisposableStore();
+
+			const registeredWindow = {
+				window,
+				disposables: disposables.add(new DisposableStore())
+			};
+			windows.set(window.vscodeWindowId, registeredWindow);
+
+			disposables.add(toDisposable(() => {
+				windows.delete(window.vscodeWindowId);
+				onDidUnregisterWindow.fire(window);
+			}));
+
+			disposables.add(addDisposableListener(window, EventType.BEFORE_UNLOAD, () => {
+				onWillUnregisterWindow.fire(window);
+			}));
+
+			onDidRegisterWindow.fire(registeredWindow);
+
+			return disposables;
+		},
+		getWindows(): Iterable<IRegisteredCodeWindow> {
+			return windows.values();
+		},
+		getWindowsCount(): number {
+			return windows.size;
+		},
+		getWindowId(targetWindow: Window): number {
+			return (targetWindow as CodeWindow).vscodeWindowId;
+		},
+		hasWindow(windowId: number): boolean {
+			return windows.has(windowId);
+		},
+		getWindowById,
+		getWindow(e: Node | UIEvent | undefined | null): CodeWindow {
+			const candidateNode = e as Node | undefined | null;
+			if (candidateNode?.ownerDocument?.defaultView) {
+				return candidateNode.ownerDocument.defaultView.window as CodeWindow;
+			}
+
+			const candidateEvent = e as UIEvent | undefined | null;
+			if (candidateEvent?.view) {
+				return candidateEvent.view.window as CodeWindow;
+			}
+
+			return mainWindow;
+		},
+		getDocument(e: Node | UIEvent | undefined | null): Document {
+			const candidateNode = e as Node | undefined | null;
+			return getWindow(candidateNode).document;
+		}
+	};
+})();
+
+//#endregion
+
+export function clearNode(node: HTMLElement): void {
+	while (node.firstChild) {
+		node.firstChild.remove();
+	}
+}
+
+
+export function clearNodeRecursively(domNode: ChildNode) {
+	while (domNode.firstChild) {
+		const element = domNode.firstChild;
+		element.remove();
+		clearNodeRecursively(element);
+	}
+}
+
+
+class DomListener implements IDisposable {
+
+	private _handler: (e: any) => void;
+	private _node: EventTarget;
+	private readonly _type: string;
+	private readonly _options: boolean | AddEventListenerOptions;
+
+	constructor(node: EventTarget, type: string, handler: (e: any) => void, options?: boolean | AddEventListenerOptions) {
+		this._node = node;
+		this._type = type;
+		this._handler = handler;
+		this._options = (options || false);
+		this._node.addEventListener(this._type, this._handler, this._options);
+	}
+
+	dispose(): void {
+		if (!this._handler) {
+			// Already disposed
+			return;
+		}
+
+		this._node.removeEventListener(this._type, this._handler, this._options);
+
+		// Prevent leakers from holding on to the dom or handler func
+		this._node = null!;
+		this._handler = null!;
+	}
+}
+
+export function addDisposableListener<K extends keyof GlobalEventHandlersEventMap>(node: EventTarget, type: K, handler: (event: GlobalEventHandlersEventMap[K]) => void, useCapture?: boolean): IDisposable;
+export function addDisposableListener(node: EventTarget, type: string, handler: (event: any) => void, useCapture?: boolean): IDisposable;
+export function addDisposableListener(node: EventTarget, type: string, handler: (event: any) => void, options: AddEventListenerOptions): IDisposable;
+export function addDisposableListener(node: EventTarget, type: string, handler: (event: any) => void, useCaptureOrOptions?: boolean | AddEventListenerOptions): IDisposable {
+	return new DomListener(node, type, handler, useCaptureOrOptions);
+}
+
+export interface IAddStandardDisposableListenerSignature {
+	(node: HTMLElement, type: 'click', handler: (event: IMouseEvent) => void, useCapture?: boolean): IDisposable;
+	(node: HTMLElement, type: 'mousedown', handler: (event: IMouseEvent) => void, useCapture?: boolean): IDisposable;
+	(node: HTMLElement, type: 'keydown', handler: (event: IKeyboardEvent) => void, useCapture?: boolean): IDisposable;
+	(node: HTMLElement, type: 'keypress', handler: (event: IKeyboardEvent) => void, useCapture?: boolean): IDisposable;
+	(node: HTMLElement, type: 'keyup', handler: (event: IKeyboardEvent) => void, useCapture?: boolean): IDisposable;
+	(node: HTMLElement, type: 'pointerdown', handler: (event: PointerEvent) => void, useCapture?: boolean): IDisposable;
+	(node: HTMLElement, type: 'pointermove', handler: (event: PointerEvent) => void, useCapture?: boolean): IDisposable;
+	(node: HTMLElement, type: 'pointerup', handler: (event: PointerEvent) => void, useCapture?: boolean): IDisposable;
+	(node: HTMLElement, type: string, handler: (event: any) => void, useCapture?: boolean): IDisposable;
+}
+function _wrapAsStandardMouseEvent(targetWindow: Window, handler: (e: IMouseEvent) => void): (e: MouseEvent) => void {
+	return function (e: MouseEvent) {
+		return handler(new StandardMouseEvent(targetWindow, e));
+	};
+}
+function _wrapAsStandardKeyboardEvent(handler: (e: IKeyboardEvent) => void): (e: KeyboardEvent) => void {
+	return function (e: KeyboardEvent) {
+		return handler(new StandardKeyboardEvent(e));
+	};
+}
+export const addStandardDisposableListener: IAddStandardDisposableListenerSignature = function addStandardDisposableListener(node: HTMLElement, type: string, handler: (event: any) => void, useCapture?: boolean): IDisposable {
+	let wrapHandler = handler;
+
+	if (type === 'click' || type === 'mousedown' || type === 'contextmenu') {
+		wrapHandler = _wrapAsStandardMouseEvent(getWindow(node), handler);
+	} else if (type === 'keydown' || type === 'keypress' || type === 'keyup') {
+		wrapHandler = _wrapAsStandardKeyboardEvent(handler);
+	}
+
+	return addDisposableListener(node, type, wrapHandler, useCapture);
+};
+
+export const addStandardDisposableGenericMouseDownListener = function addStandardDisposableListener(node: HTMLElement, handler: (event: any) => void, useCapture?: boolean): IDisposable {
+	const wrapHandler = _wrapAsStandardMouseEvent(getWindow(node), handler);
+
+	return addDisposableGenericMouseDownListener(node, wrapHandler, useCapture);
+};
+
+export const addStandardDisposableGenericMouseUpListener = function addStandardDisposableListener(node: HTMLElement, handler: (event: any) => void, useCapture?: boolean): IDisposable {
+	const wrapHandler = _wrapAsStandardMouseEvent(getWindow(node), handler);
+
+	return addDisposableGenericMouseUpListener(node, wrapHandler, useCapture);
+};
+export function addDisposableGenericMouseDownListener(node: EventTarget, handler: (event: any) => void, useCapture?: boolean): IDisposable {
+	return addDisposableListener(node, platform.isIOS && BrowserFeatures.pointerEvents ? EventType.POINTER_DOWN : EventType.MOUSE_DOWN, handler, useCapture);
+}
+
+export function addDisposableGenericMouseMoveListener(node: EventTarget, handler: (event: any) => void, useCapture?: boolean): IDisposable {
+	return addDisposableListener(node, platform.isIOS && BrowserFeatures.pointerEvents ? EventType.POINTER_MOVE : EventType.MOUSE_MOVE, handler, useCapture);
+}
+
+export function addDisposableGenericMouseUpListener(node: EventTarget, handler: (event: any) => void, useCapture?: boolean): IDisposable {
+	return addDisposableListener(node, platform.isIOS && BrowserFeatures.pointerEvents ? EventType.POINTER_UP : EventType.MOUSE_UP, handler, useCapture);
+}
+
+/**
+ * Execute the callback the next time the browser is idle, returning an
+ * {@link IDisposable} that will cancel the callback when disposed. This wraps
+ * [requestIdleCallback] so it will fallback to [setTimeout] if the environment
+ * doesn't support it.
+ *
+ * @param targetWindow The window for which to run the idle callback
+ * @param callback The callback to run when idle, this includes an
+ * [IdleDeadline] that provides the time alloted for the idle callback by the
+ * browser. Not respecting this deadline will result in a degraded user
+ * experience.
+ * @param timeout A timeout at which point to queue no longer wait for an idle
+ * callback but queue it on the regular event loop (like setTimeout). Typically
+ * this should not be used.
+ *
+ * [IdleDeadline]: https://developer.mozilla.org/en-US/docs/Web/API/IdleDeadline
+ * [requestIdleCallback]: https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback
+ * [setTimeout]: https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout
+ */
+export function runWhenWindowIdle(targetWindow: Window | typeof globalThis, callback: (idle: IdleDeadline) => void, timeout?: number): IDisposable {
+	return _runWhenIdle(targetWindow, callback, timeout);
+}
+
+/**
+ * An implementation of the "idle-until-urgent"-strategy as introduced
+ * here: https://philipwalton.com/articles/idle-until-urgent/
+ */
+export class WindowIdleValue<T> extends AbstractIdleValue<T> {
+	constructor(targetWindow: Window | typeof globalThis, executor: () => T) {
+		super(targetWindow, executor);
+	}
+}
+
+/**
+ * Schedule a callback to be run at the next animation frame.
+ * This allows multiple parties to register callbacks that should run at the next animation frame.
+ * If currently in an animation frame, `runner` will be executed immediately.
+ * @return token that can be used to cancel the scheduled runner (only if `runner` was not executed immediately).
+ */
+export let runAtThisOrScheduleAtNextAnimationFrame: (targetWindow: Window, runner: () => void, priority?: number) => IDisposable;
+/**
+ * Schedule a callback to be run at the next animation frame.
+ * This allows multiple parties to register callbacks that should run at the next animation frame.
+ * If currently in an animation frame, `runner` will be executed at the next animation frame.
+ * @return token that can be used to cancel the scheduled runner.
+ */
+export let scheduleAtNextAnimationFrame: (targetWindow: Window, runner: () => void, priority?: number) => IDisposable;
+
+export function disposableWindowInterval(targetWindow: Window, handler: () => void | boolean /* stop interval */ | Promise<unknown>, interval: number, iterations?: number): IDisposable {
+	let iteration = 0;
+	const timer = targetWindow.setInterval(() => {
+		iteration++;
+		if ((typeof iterations === 'number' && iteration >= iterations) || handler() === true) {
+			disposable.dispose();
+		}
+	}, interval);
+	const disposable = toDisposable(() => {
+		targetWindow.clearInterval(timer);
+	});
+	return disposable;
+}
+
+export class WindowIntervalTimer extends IntervalTimer {
+
+	private readonly defaultTarget?: Window & typeof globalThis;
+
+	/**
+	 *
+	 * @param node The optional node from which the target window is determined
+	 */
+	constructor(node?: Node) {
+		super();
+		this.defaultTarget = node && getWindow(node);
+	}
+
+	override cancelAndSet(runner: () => void, interval: number, targetWindow?: Window & typeof globalThis): void {
+		return super.cancelAndSet(runner, interval, targetWindow ?? this.defaultTarget);
+	}
+}
+
+class AnimationFrameQueueItem implements IDisposable {
+
+	private _runner: () => void;
+	public priority: number;
+	private _canceled: boolean;
+
+	constructor(runner: () => void, priority: number = 0) {
+		this._runner = runner;
+		this.priority = priority;
+		this._canceled = false;
+	}
+
+	dispose(): void {
+		this._canceled = true;
+	}
+
+	execute(): void {
+		if (this._canceled) {
+			return;
+		}
+
+		try {
+			this._runner();
+		} catch (e) {
+			onUnexpectedError(e);
+		}
+	}
+
+	// Sort by priority (largest to lowest)
+	static sort(a: AnimationFrameQueueItem, b: AnimationFrameQueueItem): number {
+		return b.priority - a.priority;
+	}
+}
+
+(function () {
+	/**
+	 * The runners scheduled at the next animation frame
+	 */
+	const NEXT_QUEUE = new Map<number /* window ID */, AnimationFrameQueueItem[]>();
+	/**
+	 * The runners scheduled at the current animation frame
+	 */
+	const CURRENT_QUEUE = new Map<number /* window ID */, AnimationFrameQueueItem[]>();
+	/**
+	 * A flag to keep track if the native requestAnimationFrame was already called
+	 */
+	const animFrameRequested = new Map<number /* window ID */, boolean>();
+	/**
+	 * A flag to indicate if currently handling a native requestAnimationFrame callback
+	 */
+	const inAnimationFrameRunner = new Map<number /* window ID */, boolean>();
+
+	const animationFrameRunner = (targetWindowId: number) => {
+		animFrameRequested.set(targetWindowId, false);
+
+		const currentQueue = NEXT_QUEUE.get(targetWindowId) ?? [];
+		CURRENT_QUEUE.set(targetWindowId, currentQueue);
+		NEXT_QUEUE.set(targetWindowId, []);
+
+		inAnimationFrameRunner.set(targetWindowId, true);
+		while (currentQueue.length > 0) {
+			currentQueue.sort(AnimationFrameQueueItem.sort);
+			const top = currentQueue.shift()!;
+			top.execute();
+		}
+		inAnimationFrameRunner.set(targetWindowId, false);
+	};
+
+	scheduleAtNextAnimationFrame = (targetWindow: Window, runner: () => void, priority: number = 0) => {
+		const targetWindowId = getWindowId(targetWindow);
+		const item = new AnimationFrameQueueItem(runner, priority);
+
+		let nextQueue = NEXT_QUEUE.get(targetWindowId);
+		if (!nextQueue) {
+			nextQueue = [];
+			NEXT_QUEUE.set(targetWindowId, nextQueue);
+		}
+		nextQueue.push(item);
+
+		if (!animFrameRequested.get(targetWindowId)) {
+			animFrameRequested.set(targetWindowId, true);
+			targetWindow.requestAnimationFrame(() => animationFrameRunner(targetWindowId));
+		}
+
+		return item;
+	};
+
+	runAtThisOrScheduleAtNextAnimationFrame = (targetWindow: Window, runner: () => void, priority?: number) => {
+		const targetWindowId = getWindowId(targetWindow);
+		if (inAnimationFrameRunner.get(targetWindowId)) {
+			const item = new AnimationFrameQueueItem(runner, priority);
+			let currentQueue = CURRENT_QUEUE.get(targetWindowId);
+			if (!currentQueue) {
+				currentQueue = [];
+				CURRENT_QUEUE.set(targetWindowId, currentQueue);
+			}
+			currentQueue.push(item);
+			return item;
+		} else {
+			return scheduleAtNextAnimationFrame(targetWindow, runner, priority);
+		}
+	};
+})();
+
+export function measure(targetWindow: Window, callback: () => void): IDisposable {
+	return scheduleAtNextAnimationFrame(targetWindow, callback, 10000 /* must be early */);
+}
+
+export function modify(targetWindow: Window, callback: () => void): IDisposable {
+	return scheduleAtNextAnimationFrame(targetWindow, callback, -10000 /* must be late */);
+}
+
+/**
+ * Add a throttled listener. `handler` is fired at most every 8.33333ms or with the next animation frame (if browser supports it).
+ */
+export interface IEventMerger<R, E> {
+	(lastEvent: R | null, currentEvent: E): R;
+}
+
+const MINIMUM_TIME_MS = 8;
+const DEFAULT_EVENT_MERGER: IEventMerger<Event, Event> = function (lastEvent: Event | null, currentEvent: Event) {
+	return currentEvent;
+};
+
+class TimeoutThrottledDomListener<R, E extends Event> extends Disposable {
+
+	constructor(node: any, type: string, handler: (event: R) => void, eventMerger: IEventMerger<R, E> = <any>DEFAULT_EVENT_MERGER, minimumTimeMs: number = MINIMUM_TIME_MS) {
+		super();
+
+		let lastEvent: R | null = null;
+		let lastHandlerTime = 0;
+		const timeout = this._register(new TimeoutTimer());
+
+		const invokeHandler = () => {
+			lastHandlerTime = (new Date()).getTime();
+			handler(<R>lastEvent);
+			lastEvent = null;
+		};
+
+		this._register(addDisposableListener(node, type, (e) => {
+
+			lastEvent = eventMerger(lastEvent, e);
+			const elapsedTime = (new Date()).getTime() - lastHandlerTime;
+
+			if (elapsedTime >= minimumTimeMs) {
+				timeout.cancel();
+				invokeHandler();
+			} else {
+				timeout.setIfNotSet(invokeHandler, minimumTimeMs - elapsedTime);
+			}
+		}));
+	}
+}
+
+export function addDisposableThrottledListener<R, E extends Event = Event>(node: any, type: string, handler: (event: R) => void, eventMerger?: IEventMerger<R, E>, minimumTimeMs?: number): IDisposable {
+	return new TimeoutThrottledDomListener<R, E>(node, type, handler, eventMerger, minimumTimeMs);
+}
+
+export function getComputedStyle(el: HTMLElement): CSSStyleDeclaration {
+	return getWindow(el).getComputedStyle(el, null);
+}
+
+export function getClientArea(element: HTMLElement, fallback?: HTMLElement): Dimension {
+	const elWindow = getWindow(element);
+	const elDocument = elWindow.document;
+
+	// Try with DOM clientWidth / clientHeight
+	if (element !== elDocument.body) {
+		return new Dimension(element.clientWidth, element.clientHeight);
+	}
+
+	// If visual view port exits and it's on mobile, it should be used instead of window innerWidth / innerHeight, or document.body.clientWidth / document.body.clientHeight
+	if (platform.isIOS && elWindow?.visualViewport) {
+		return new Dimension(elWindow.visualViewport.width, elWindow.visualViewport.height);
+	}
+
+	// Try innerWidth / innerHeight
+	if (elWindow?.innerWidth && elWindow.innerHeight) {
+		return new Dimension(elWindow.innerWidth, elWindow.innerHeight);
+	}
+
+	// Try with document.body.clientWidth / document.body.clientHeight
+	if (elDocument.body && elDocument.body.clientWidth && elDocument.body.clientHeight) {
+		return new Dimension(elDocument.body.clientWidth, elDocument.body.clientHeight);
+	}
+
+	// Try with document.documentElement.clientWidth / document.documentElement.clientHeight
+	if (elDocument.documentElement && elDocument.documentElement.clientWidth && elDocument.documentElement.clientHeight) {
+		return new Dimension(elDocument.documentElement.clientWidth, elDocument.documentElement.clientHeight);
+	}
+
+	if (fallback) {
+		return getClientArea(fallback);
+	}
+
+	throw new Error('Unable to figure out browser width and height');
+}
+
+class SizeUtils {
+	// Adapted from WinJS
+	// Converts a CSS positioning string for the specified element to pixels.
+	private static convertToPixels(element: HTMLElement, value: string): number {
+		return parseFloat(value) || 0;
+	}
+
+	private static getDimension(element: HTMLElement, cssPropertyName: string, jsPropertyName: string): number {
+		const computedStyle = getComputedStyle(element);
+		const value = computedStyle ? computedStyle.getPropertyValue(cssPropertyName) : '0';
+		return SizeUtils.convertToPixels(element, value);
+	}
+
+	static getBorderLeftWidth(element: HTMLElement): number {
+		return SizeUtils.getDimension(element, 'border-left-width', 'borderLeftWidth');
+	}
+	static getBorderRightWidth(element: HTMLElement): number {
+		return SizeUtils.getDimension(element, 'border-right-width', 'borderRightWidth');
+	}
+	static getBorderTopWidth(element: HTMLElement): number {
+		return SizeUtils.getDimension(element, 'border-top-width', 'borderTopWidth');
+	}
+	static getBorderBottomWidth(element: HTMLElement): number {
+		return SizeUtils.getDimension(element, 'border-bottom-width', 'borderBottomWidth');
+	}
+
+	static getPaddingLeft(element: HTMLElement): number {
+		return SizeUtils.getDimension(element, 'padding-left', 'paddingLeft');
+	}
+	static getPaddingRight(element: HTMLElement): number {
+		return SizeUtils.getDimension(element, 'padding-right', 'paddingRight');
+	}
+	static getPaddingTop(element: HTMLElement): number {
+		return SizeUtils.getDimension(element, 'padding-top', 'paddingTop');
+	}
+	static getPaddingBottom(element: HTMLElement): number {
+		return SizeUtils.getDimension(element, 'padding-bottom', 'paddingBottom');
+	}
+
+	static getMarginLeft(element: HTMLElement): number {
+		return SizeUtils.getDimension(element, 'margin-left', 'marginLeft');
+	}
+	static getMarginTop(element: HTMLElement): number {
+		return SizeUtils.getDimension(element, 'margin-top', 'marginTop');
+	}
+	static getMarginRight(element: HTMLElement): number {
+		return SizeUtils.getDimension(element, 'margin-right', 'marginRight');
+	}
+	static getMarginBottom(element: HTMLElement): number {
+		return SizeUtils.getDimension(element, 'margin-bottom', 'marginBottom');
+	}
+}
+
+// ----------------------------------------------------------------------------------------
+// Position & Dimension
+
+export interface IDimension {
+	readonly width: number;
+	readonly height: number;
+}
+
+export class Dimension implements IDimension {
+
+	static readonly None = new Dimension(0, 0);
+
+	constructor(
+		readonly width: number,
+		readonly height: number,
+	) { }
+
+	with(width: number = this.width, height: number = this.height): Dimension {
+		if (width !== this.width || height !== this.height) {
+			return new Dimension(width, height);
+		} else {
+			return this;
+		}
+	}
+
+	static is(obj: unknown): obj is IDimension {
+		return typeof obj === 'object' && typeof (<IDimension>obj).height === 'number' && typeof (<IDimension>obj).width === 'number';
+	}
+
+	static lift(obj: IDimension): Dimension {
+		if (obj instanceof Dimension) {
+			return obj;
+		} else {
+			return new Dimension(obj.width, obj.height);
+		}
+	}
+
+	static equals(a: Dimension | undefined, b: Dimension | undefined): boolean {
+		if (a === b) {
+			return true;
+		}
+		if (!a || !b) {
+			return false;
+		}
+		return a.width === b.width && a.height === b.height;
+	}
+}
+
+export interface IDomPosition {
+	readonly left: number;
+	readonly top: number;
+}
+
+export function getTopLeftOffset(element: HTMLElement): IDomPosition {
+	// Adapted from WinJS.Utilities.getPosition
+	// and added borders to the mix
+
+	let offsetParent = element.offsetParent;
+	let top = element.offsetTop;
+	let left = element.offsetLeft;
+
+	while (
+		(element = <HTMLElement>element.parentNode) !== null
+		&& element !== element.ownerDocument.body
+		&& element !== element.ownerDocument.documentElement
+	) {
+		top -= element.scrollTop;
+		const c = isShadowRoot(element) ? null : getComputedStyle(element);
+		if (c) {
+			left -= c.direction !== 'rtl' ? element.scrollLeft : -element.scrollLeft;
+		}
+
+		if (element === offsetParent) {
+			left += SizeUtils.getBorderLeftWidth(element);
+			top += SizeUtils.getBorderTopWidth(element);
+			top += element.offsetTop;
+			left += element.offsetLeft;
+			offsetParent = element.offsetParent;
+		}
+	}
+
+	return {
+		left: left,
+		top: top
+	};
+}
+
+export interface IDomNodePagePosition {
+	left: number;
+	top: number;
+	width: number;
+	height: number;
+}
+
+export function size(element: HTMLElement, width: number | null, height: number | null): void {
+	if (typeof width === 'number') {
+		element.style.width = `${width}px`;
+	}
+
+	if (typeof height === 'number') {
+		element.style.height = `${height}px`;
+	}
+}
+
+export function position(element: HTMLElement, top: number, right?: number, bottom?: number, left?: number, position: string = 'absolute'): void {
+	if (typeof top === 'number') {
+		element.style.top = `${top}px`;
+	}
+
+	if (typeof right === 'number') {
+		element.style.right = `${right}px`;
+	}
+
+	if (typeof bottom === 'number') {
+		element.style.bottom = `${bottom}px`;
+	}
+
+	if (typeof left === 'number') {
+		element.style.left = `${left}px`;
+	}
+
+	element.style.position = position;
+}
+
+/**
+ * Returns the position of a dom node relative to the entire page.
+ */
+export function getDomNodePagePosition(domNode: HTMLElement): IDomNodePagePosition {
+	const bb = domNode.getBoundingClientRect();
+	const window = getWindow(domNode);
+	return {
+		left: bb.left + window.scrollX,
+		top: bb.top + window.scrollY,
+		width: bb.width,
+		height: bb.height
+	};
+}
+
+/**
+ * Returns the effective zoom on a given element before window zoom level is applied
+ */
+export function getDomNodeZoomLevel(domNode: HTMLElement): number {
+	let testElement: HTMLElement | null = domNode;
+	let zoom = 1.0;
+	do {
+		const elementZoomLevel = (getComputedStyle(testElement) as any).zoom;
+		if (elementZoomLevel !== null && elementZoomLevel !== undefined && elementZoomLevel !== '1') {
+			zoom *= elementZoomLevel;
+		}
+
+		testElement = testElement.parentElement;
+	} while (testElement !== null && testElement !== testElement.ownerDocument.documentElement);
+
+	return zoom;
+}
+
+
+// Adapted from WinJS
+// Gets the width of the element, including margins.
+export function getTotalWidth(element: HTMLElement): number {
+	const margin = SizeUtils.getMarginLeft(element) + SizeUtils.getMarginRight(element);
+	return element.offsetWidth + margin;
+}
+
+export function getContentWidth(element: HTMLElement): number {
+	const border = SizeUtils.getBorderLeftWidth(element) + SizeUtils.getBorderRightWidth(element);
+	const padding = SizeUtils.getPaddingLeft(element) + SizeUtils.getPaddingRight(element);
+	return element.offsetWidth - border - padding;
+}
+
+export function getTotalScrollWidth(element: HTMLElement): number {
+	const margin = SizeUtils.getMarginLeft(element) + SizeUtils.getMarginRight(element);
+	return element.scrollWidth + margin;
+}
+
+// Adapted from WinJS
+// Gets the height of the content of the specified element. The content height does not include borders or padding.
+export function getContentHeight(element: HTMLElement): number {
+	const border = SizeUtils.getBorderTopWidth(element) + SizeUtils.getBorderBottomWidth(element);
+	const padding = SizeUtils.getPaddingTop(element) + SizeUtils.getPaddingBottom(element);
+	return element.offsetHeight - border - padding;
+}
+
+// Adapted from WinJS
+// Gets the height of the element, including its margins.
+export function getTotalHeight(element: HTMLElement): number {
+	const margin = SizeUtils.getMarginTop(element) + SizeUtils.getMarginBottom(element);
+	return element.offsetHeight + margin;
+}
+
+// Gets the left coordinate of the specified element relative to the specified parent.
+function getRelativeLeft(element: HTMLElement, parent: HTMLElement): number {
+	if (element === null) {
+		return 0;
+	}
+
+	const elementPosition = getTopLeftOffset(element);
+	const parentPosition = getTopLeftOffset(parent);
+	return elementPosition.left - parentPosition.left;
+}
+
+export function getLargestChildWidth(parent: HTMLElement, children: HTMLElement[]): number {
+	const childWidths = children.map((child) => {
+		return Math.max(getTotalScrollWidth(child), getTotalWidth(child)) + getRelativeLeft(child, parent) || 0;
+	});
+	const maxWidth = Math.max(...childWidths);
+	return maxWidth;
+}
+
+// ----------------------------------------------------------------------------------------
+
+export function isAncestor(testChild: Node | null, testAncestor: Node | null): boolean {
+	return Boolean(testAncestor?.contains(testChild));
+}
+
+const parentFlowToDataKey = 'parentFlowToElementId';
+
+/**
+ * Set an explicit parent to use for nodes that are not part of the
+ * regular dom structure.
+ */
+export function setParentFlowTo(fromChildElement: HTMLElement, toParentElement: Element): void {
+	fromChildElement.dataset[parentFlowToDataKey] = toParentElement.id;
+}
+
+function getParentFlowToElement(node: HTMLElement): HTMLElement | null {
+	const flowToParentId = node.dataset[parentFlowToDataKey];
+	if (typeof flowToParentId === 'string') {
+		return node.ownerDocument.getElementById(flowToParentId);
+	}
+	return null;
+}
+
+/**
+ * Check if `testAncestor` is an ancestor of `testChild`, observing the explicit
+ * parents set by `setParentFlowTo`.
+ */
+export function isAncestorUsingFlowTo(testChild: Node, testAncestor: Node): boolean {
+	let node: Node | null = testChild;
+	while (node) {
+		if (node === testAncestor) {
+			return true;
+		}
+
+		if (isHTMLElement(node)) {
+			const flowToParentElement = getParentFlowToElement(node);
+			if (flowToParentElement) {
+				node = flowToParentElement;
+				continue;
+			}
+		}
+		node = node.parentNode;
+	}
+
+	return false;
+}
+
+export function findParentWithClass(node: HTMLElement, clazz: string, stopAtClazzOrNode?: string | HTMLElement): HTMLElement | null {
+	while (node && node.nodeType === node.ELEMENT_NODE) {
+		if (node.classList.contains(clazz)) {
+			return node;
+		}
+
+		if (stopAtClazzOrNode) {
+			if (typeof stopAtClazzOrNode === 'string') {
+				if (node.classList.contains(stopAtClazzOrNode)) {
+					return null;
+				}
+			} else {
+				if (node === stopAtClazzOrNode) {
+					return null;
+				}
+			}
+		}
+
+		node = <HTMLElement>node.parentNode;
+	}
+
+	return null;
+}
+
+export function hasParentWithClass(node: HTMLElement, clazz: string, stopAtClazzOrNode?: string | HTMLElement): boolean {
+	return !!findParentWithClass(node, clazz, stopAtClazzOrNode);
+}
+
+export function isShadowRoot(node: Node): node is ShadowRoot {
+	return (
+		node && !!(<ShadowRoot>node).host && !!(<ShadowRoot>node).mode
+	);
+}
+
+export function isInShadowDOM(domNode: Node): boolean {
+	return !!getShadowRoot(domNode);
+}
+
+export function getShadowRoot(domNode: Node): ShadowRoot | null {
+	while (domNode.parentNode) {
+		if (domNode === domNode.ownerDocument?.body) {
+			// reached the body
+			return null;
+		}
+		domNode = domNode.parentNode;
+	}
+	return isShadowRoot(domNode) ? domNode : null;
+}
+
+/**
+ * Returns the active element across all child windows
+ * based on document focus. Falls back to the main
+ * window if no window has focus.
+ */
+export function getActiveElement(): Element | null {
+	let result = getActiveDocument().activeElement;
+
+	while (result?.shadowRoot) {
+		result = result.shadowRoot.activeElement;
+	}
+
+	return result;
+}
+
+/**
+ * Returns true if the focused window active element matches
+ * the provided element. Falls back to the main window if no
+ * window has focus.
+ */
+export function isActiveElement(element: Element): boolean {
+	return getActiveElement() === element;
+}
+
+/**
+ * Returns true if the focused window active element is contained in
+ * `ancestor`. Falls back to the main window if no window has focus.
+ */
+export function isAncestorOfActiveElement(ancestor: Element): boolean {
+	return isAncestor(getActiveElement(), ancestor);
+}
+
+/**
+ * Returns whether the element is in the active `document`. The active
+ * document has focus or will be the main windows document.
+ */
+export function isActiveDocument(element: Element): boolean {
+	return element.ownerDocument === getActiveDocument();
+}
+
+/**
+ * Returns the active document across main and child windows.
+ * Prefers the window with focus, otherwise falls back to
+ * the main windows document.
+ */
+export function getActiveDocument(): Document {
+	if (getWindowsCount() <= 1) {
+		return mainWindow.document;
+	}
+
+	const documents = Array.from(getWindows()).map(({ window }) => window.document);
+	return documents.find(doc => doc.hasFocus()) ?? mainWindow.document;
+}
+
+/**
+ * Returns the active window across main and child windows.
+ * Prefers the window with focus, otherwise falls back to
+ * the main window.
+ */
+export function getActiveWindow(): CodeWindow {
+	const document = getActiveDocument();
+	return (document.defaultView?.window ?? mainWindow) as CodeWindow;
+}
+
+const globalStylesheets = new Map<HTMLStyleElement /* main stylesheet */, Set<HTMLStyleElement /* aux window clones that track the main stylesheet */>>();
+
+export function isGlobalStylesheet(node: Node): boolean {
+	return globalStylesheets.has(node as HTMLStyleElement);
+}
+
+/**
+ * A version of createStyleSheet which has a unified API to initialize/set the style content.
+ */
+export function createStyleSheet2(): WrappedStyleElement {
+	return new WrappedStyleElement();
+}
+
+class WrappedStyleElement {
+	private _currentCssStyle = '';
+	private _styleSheet: HTMLStyleElement | undefined = undefined;
+
+	public setStyle(cssStyle: string): void {
+		if (cssStyle === this._currentCssStyle) {
+			return;
+		}
+		this._currentCssStyle = cssStyle;
+
+		if (!this._styleSheet) {
+			this._styleSheet = createStyleSheet(mainWindow.document.head, (s) => s.innerText = cssStyle);
+		} else {
+			this._styleSheet.innerText = cssStyle;
+		}
+	}
+
+	public dispose(): void {
+		if (this._styleSheet) {
+			this._styleSheet.remove();
+			this._styleSheet = undefined;
+		}
+	}
+}
+
+export function createStyleSheet(container: HTMLElement = mainWindow.document.head, beforeAppend?: (style: HTMLStyleElement) => void, disposableStore?: DisposableStore): HTMLStyleElement {
+	const style = document.createElement('style');
+	style.type = 'text/css';
+	style.media = 'screen';
+	beforeAppend?.(style);
+	container.appendChild(style);
+
+	if (disposableStore) {
+		disposableStore.add(toDisposable(() => style.remove()));
+	}
+
+	// With <head> as container, the stylesheet becomes global and is tracked
+	// to support auxiliary windows to clone the stylesheet.
+	if (container === mainWindow.document.head) {
+		const globalStylesheetClones = new Set<HTMLStyleElement>();
+		globalStylesheets.set(style, globalStylesheetClones);
+
+		for (const { window: targetWindow, disposables } of getWindows()) {
+			if (targetWindow === mainWindow) {
+				continue; // main window is already tracked
+			}
+
+			const cloneDisposable = disposables.add(cloneGlobalStyleSheet(style, globalStylesheetClones, targetWindow));
+			disposableStore?.add(cloneDisposable);
+		}
+	}
+
+	return style;
+}
+
+export function cloneGlobalStylesheets(targetWindow: Window): IDisposable {
+	const disposables = new DisposableStore();
+
+	for (const [globalStylesheet, clonedGlobalStylesheets] of globalStylesheets) {
+		disposables.add(cloneGlobalStyleSheet(globalStylesheet, clonedGlobalStylesheets, targetWindow));
+	}
+
+	return disposables;
+}
+
+function cloneGlobalStyleSheet(globalStylesheet: HTMLStyleElement, globalStylesheetClones: Set<HTMLStyleElement>, targetWindow: Window): IDisposable {
+	const disposables = new DisposableStore();
+
+	const clone = globalStylesheet.cloneNode(true) as HTMLStyleElement;
+	targetWindow.document.head.appendChild(clone);
+	disposables.add(toDisposable(() => clone.remove()));
+
+	for (const rule of getDynamicStyleSheetRules(globalStylesheet)) {
+		clone.sheet?.insertRule(rule.cssText, clone.sheet?.cssRules.length);
+	}
+
+	disposables.add(sharedMutationObserver.observe(globalStylesheet, disposables, { childList: true })(() => {
+		clone.textContent = globalStylesheet.textContent;
+	}));
+
+	globalStylesheetClones.add(clone);
+	disposables.add(toDisposable(() => globalStylesheetClones.delete(clone)));
+
+	return disposables;
+}
+
+interface IMutationObserver {
+	users: number;
+	readonly observer: MutationObserver;
+	readonly onDidMutate: event.Event<MutationRecord[]>;
+}
+
+export const sharedMutationObserver = new class {
+
+	readonly mutationObservers = new Map<Node, Map<number, IMutationObserver>>();
+
+	observe(target: Node, disposables: DisposableStore, options?: MutationObserverInit): event.Event<MutationRecord[]> {
+		let mutationObserversPerTarget = this.mutationObservers.get(target);
+		if (!mutationObserversPerTarget) {
+			mutationObserversPerTarget = new Map<number, IMutationObserver>();
+			this.mutationObservers.set(target, mutationObserversPerTarget);
+		}
+
+		const optionsHash = hash(options);
+		let mutationObserverPerOptions = mutationObserversPerTarget.get(optionsHash);
+		if (!mutationObserverPerOptions) {
+			const onDidMutate = new event.Emitter<MutationRecord[]>();
+			const observer = new MutationObserver(mutations => onDidMutate.fire(mutations));
+			observer.observe(target, options);
+
+			const resolvedMutationObserverPerOptions = mutationObserverPerOptions = {
+				users: 1,
+				observer,
+				onDidMutate: onDidMutate.event
+			};
+
+			disposables.add(toDisposable(() => {
+				resolvedMutationObserverPerOptions.users -= 1;
+
+				if (resolvedMutationObserverPerOptions.users === 0) {
+					onDidMutate.dispose();
+					observer.disconnect();
+
+					mutationObserversPerTarget?.delete(optionsHash);
+					if (mutationObserversPerTarget?.size === 0) {
+						this.mutationObservers.delete(target);
+					}
+				}
+			}));
+
+			mutationObserversPerTarget.set(optionsHash, mutationObserverPerOptions);
+		} else {
+			mutationObserverPerOptions.users += 1;
+		}
+
+		return mutationObserverPerOptions.onDidMutate;
+	}
+};
+
+export function createMetaElement(container: HTMLElement = mainWindow.document.head): HTMLMetaElement {
+	return createHeadElement('meta', container) as HTMLMetaElement;
+}
+
+export function createLinkElement(container: HTMLElement = mainWindow.document.head): HTMLLinkElement {
+	return createHeadElement('link', container) as HTMLLinkElement;
+}
+
+function createHeadElement(tagName: string, container: HTMLElement = mainWindow.document.head): HTMLElement {
+	const element = document.createElement(tagName);
+	container.appendChild(element);
+	return element;
+}
+
+let _sharedStyleSheet: HTMLStyleElement | null = null;
+function getSharedStyleSheet(): HTMLStyleElement {
+	if (!_sharedStyleSheet) {
+		_sharedStyleSheet = createStyleSheet();
+	}
+	return _sharedStyleSheet;
+}
+
+function getDynamicStyleSheetRules(style: HTMLStyleElement) {
+	if (style?.sheet?.rules) {
+		// Chrome, IE
+		return style.sheet.rules;
+	}
+	if (style?.sheet?.cssRules) {
+		// FF
+		return style.sheet.cssRules;
+	}
+	return [];
+}
+
+export function createCSSRule(selector: string, cssText: string, style = getSharedStyleSheet()): void {
+	if (!style || !cssText) {
+		return;
+	}
+
+	style.sheet?.insertRule(`${selector} {${cssText}}`, 0);
+
+	// Apply rule also to all cloned global stylesheets
+	for (const clonedGlobalStylesheet of globalStylesheets.get(style) ?? []) {
+		createCSSRule(selector, cssText, clonedGlobalStylesheet);
+	}
+}
+
+export function removeCSSRulesContainingSelector(ruleName: string, style = getSharedStyleSheet()): void {
+	if (!style) {
+		return;
+	}
+
+	const rules = getDynamicStyleSheetRules(style);
+	const toDelete: number[] = [];
+	for (let i = 0; i < rules.length; i++) {
+		const rule = rules[i];
+		if (isCSSStyleRule(rule) && rule.selectorText.indexOf(ruleName) !== -1) {
+			toDelete.push(i);
+		}
+	}
+
+	for (let i = toDelete.length - 1; i >= 0; i--) {
+		style.sheet?.deleteRule(toDelete[i]);
+	}
+
+	// Remove rules also from all cloned global stylesheets
+	for (const clonedGlobalStylesheet of globalStylesheets.get(style) ?? []) {
+		removeCSSRulesContainingSelector(ruleName, clonedGlobalStylesheet);
+	}
+}
+
+function isCSSStyleRule(rule: CSSRule): rule is CSSStyleRule {
+	return typeof (rule as CSSStyleRule).selectorText === 'string';
+}
+
+export function isHTMLElement(e: unknown): e is HTMLElement {
+	// eslint-disable-next-line no-restricted-syntax
+	return e instanceof HTMLElement || e instanceof getWindow(e as Node).HTMLElement;
+}
+
+export function isHTMLAnchorElement(e: unknown): e is HTMLAnchorElement {
+	// eslint-disable-next-line no-restricted-syntax
+	return e instanceof HTMLAnchorElement || e instanceof getWindow(e as Node).HTMLAnchorElement;
+}
+
+export function isHTMLSpanElement(e: unknown): e is HTMLSpanElement {
+	// eslint-disable-next-line no-restricted-syntax
+	return e instanceof HTMLSpanElement || e instanceof getWindow(e as Node).HTMLSpanElement;
+}
+
+export function isHTMLTextAreaElement(e: unknown): e is HTMLTextAreaElement {
+	// eslint-disable-next-line no-restricted-syntax
+	return e instanceof HTMLTextAreaElement || e instanceof getWindow(e as Node).HTMLTextAreaElement;
+}
+
+export function isHTMLInputElement(e: unknown): e is HTMLInputElement {
+	// eslint-disable-next-line no-restricted-syntax
+	return e instanceof HTMLInputElement || e instanceof getWindow(e as Node).HTMLInputElement;
+}
+
+export function isHTMLButtonElement(e: unknown): e is HTMLButtonElement {
+	// eslint-disable-next-line no-restricted-syntax
+	return e instanceof HTMLButtonElement || e instanceof getWindow(e as Node).HTMLButtonElement;
+}
+
+export function isHTMLDivElement(e: unknown): e is HTMLDivElement {
+	// eslint-disable-next-line no-restricted-syntax
+	return e instanceof HTMLDivElement || e instanceof getWindow(e as Node).HTMLDivElement;
+}
+
+export function isSVGElement(e: unknown): e is SVGElement {
+	// eslint-disable-next-line no-restricted-syntax
+	return e instanceof SVGElement || e instanceof getWindow(e as Node).SVGElement;
+}
+
+export function isMouseEvent(e: unknown): e is MouseEvent {
+	// eslint-disable-next-line no-restricted-syntax
+	return e instanceof MouseEvent || e instanceof getWindow(e as UIEvent).MouseEvent;
+}
+
+export function isKeyboardEvent(e: unknown): e is KeyboardEvent {
+	// eslint-disable-next-line no-restricted-syntax
+	return e instanceof KeyboardEvent || e instanceof getWindow(e as UIEvent).KeyboardEvent;
+}
+
+export function isPointerEvent(e: unknown): e is PointerEvent {
+	// eslint-disable-next-line no-restricted-syntax
+	return e instanceof PointerEvent || e instanceof getWindow(e as UIEvent).PointerEvent;
+}
+
+export function isDragEvent(e: unknown): e is DragEvent {
+	// eslint-disable-next-line no-restricted-syntax
+	return e instanceof DragEvent || e instanceof getWindow(e as UIEvent).DragEvent;
+}
+
+export const EventType = {
+	// Mouse
+	CLICK: 'click',
+	AUXCLICK: 'auxclick',
+	DBLCLICK: 'dblclick',
+	MOUSE_UP: 'mouseup',
+	MOUSE_DOWN: 'mousedown',
+	MOUSE_OVER: 'mouseover',
+	MOUSE_MOVE: 'mousemove',
+	MOUSE_OUT: 'mouseout',
+	MOUSE_ENTER: 'mouseenter',
+	MOUSE_LEAVE: 'mouseleave',
+	MOUSE_WHEEL: 'wheel',
+	POINTER_UP: 'pointerup',
+	POINTER_DOWN: 'pointerdown',
+	POINTER_MOVE: 'pointermove',
+	POINTER_LEAVE: 'pointerleave',
+	CONTEXT_MENU: 'contextmenu',
+	WHEEL: 'wheel',
+	// Keyboard
+	KEY_DOWN: 'keydown',
+	KEY_PRESS: 'keypress',
+	KEY_UP: 'keyup',
+	// HTML Document
+	LOAD: 'load',
+	BEFORE_UNLOAD: 'beforeunload',
+	UNLOAD: 'unload',
+	PAGE_SHOW: 'pageshow',
+	PAGE_HIDE: 'pagehide',
+	PASTE: 'paste',
+	ABORT: 'abort',
+	ERROR: 'error',
+	RESIZE: 'resize',
+	SCROLL: 'scroll',
+	FULLSCREEN_CHANGE: 'fullscreenchange',
+	WK_FULLSCREEN_CHANGE: 'webkitfullscreenchange',
+	// Form
+	SELECT: 'select',
+	CHANGE: 'change',
+	SUBMIT: 'submit',
+	RESET: 'reset',
+	FOCUS: 'focus',
+	FOCUS_IN: 'focusin',
+	FOCUS_OUT: 'focusout',
+	BLUR: 'blur',
+	INPUT: 'input',
+	// Local Storage
+	STORAGE: 'storage',
+	// Drag
+	DRAG_START: 'dragstart',
+	DRAG: 'drag',
+	DRAG_ENTER: 'dragenter',
+	DRAG_LEAVE: 'dragleave',
+	DRAG_OVER: 'dragover',
+	DROP: 'drop',
+	DRAG_END: 'dragend',
+	// Animation
+	ANIMATION_START: browser.isWebKit ? 'webkitAnimationStart' : 'animationstart',
+	ANIMATION_END: browser.isWebKit ? 'webkitAnimationEnd' : 'animationend',
+	ANIMATION_ITERATION: browser.isWebKit ? 'webkitAnimationIteration' : 'animationiteration'
+} as const;
+
+export interface EventLike {
+	preventDefault(): void;
+	stopPropagation(): void;
+}
+
+export function isEventLike(obj: unknown): obj is EventLike {
+	const candidate = obj as EventLike | undefined;
+
+	return !!(candidate && typeof candidate.preventDefault === 'function' && typeof candidate.stopPropagation === 'function');
+}
+
+export const EventHelper = {
+	stop: <T extends EventLike>(e: T, cancelBubble?: boolean): T => {
+		e.preventDefault();
+		if (cancelBubble) {
+			e.stopPropagation();
+		}
+		return e;
+	}
+};
+
+export interface IFocusTracker extends Disposable {
+	readonly onDidFocus: event.Event<void>;
+	readonly onDidBlur: event.Event<void>;
+	refreshState(): void;
+}
+
+export function saveParentsScrollTop(node: Element): number[] {
+	const r: number[] = [];
+	for (let i = 0; node && node.nodeType === node.ELEMENT_NODE; i++) {
+		r[i] = node.scrollTop;
+		node = <Element>node.parentNode;
+	}
+	return r;
+}
+
+export function restoreParentsScrollTop(node: Element, state: number[]): void {
+	for (let i = 0; node && node.nodeType === node.ELEMENT_NODE; i++) {
+		if (node.scrollTop !== state[i]) {
+			node.scrollTop = state[i];
+		}
+		node = <Element>node.parentNode;
+	}
+}
+
+class FocusTracker extends Disposable implements IFocusTracker {
+
+	private readonly _onDidFocus = this._register(new event.Emitter<void>());
+	readonly onDidFocus = this._onDidFocus.event;
+
+	private readonly _onDidBlur = this._register(new event.Emitter<void>());
+	readonly onDidBlur = this._onDidBlur.event;
+
+	private _refreshStateHandler: () => void;
+
+	private static hasFocusWithin(element: HTMLElement | Window): boolean {
+		if (isHTMLElement(element)) {
+			const shadowRoot = getShadowRoot(element);
+			const activeElement = (shadowRoot ? shadowRoot.activeElement : element.ownerDocument.activeElement);
+			return isAncestor(activeElement, element);
+		} else {
+			const window = element;
+			return isAncestor(window.document.activeElement, window.document);
+		}
+	}
+
+	constructor(element: HTMLElement | Window) {
+		super();
+		let hasFocus = FocusTracker.hasFocusWithin(element);
+		let loosingFocus = false;
+
+		const onFocus = () => {
+			loosingFocus = false;
+			if (!hasFocus) {
+				hasFocus = true;
+				this._onDidFocus.fire();
+			}
+		};
+
+		const onBlur = () => {
+			if (hasFocus) {
+				loosingFocus = true;
+				(isHTMLElement(element) ? getWindow(element) : element).setTimeout(() => {
+					if (loosingFocus) {
+						loosingFocus = false;
+						hasFocus = false;
+						this._onDidBlur.fire();
+					}
+				}, 0);
+			}
+		};
+
+		this._refreshStateHandler = () => {
+			const currentNodeHasFocus = FocusTracker.hasFocusWithin(<HTMLElement>element);
+			if (currentNodeHasFocus !== hasFocus) {
+				if (hasFocus) {
+					onBlur();
+				} else {
+					onFocus();
+				}
+			}
+		};
+
+		this._register(addDisposableListener(element, EventType.FOCUS, onFocus, true));
+		this._register(addDisposableListener(element, EventType.BLUR, onBlur, true));
+		if (isHTMLElement(element)) {
+			this._register(addDisposableListener(element, EventType.FOCUS_IN, () => this._refreshStateHandler()));
+			this._register(addDisposableListener(element, EventType.FOCUS_OUT, () => this._refreshStateHandler()));
+		}
+
+	}
+
+	refreshState() {
+		this._refreshStateHandler();
+	}
+}
+
+/**
+ * Creates a new `IFocusTracker` instance that tracks focus changes on the given `element` and its descendants.
+ *
+ * @param element The `HTMLElement` or `Window` to track focus changes on.
+ * @returns An `IFocusTracker` instance.
+ */
+export function trackFocus(element: HTMLElement | Window): IFocusTracker {
+	return new FocusTracker(element);
+}
+
+export function after<T extends Node>(sibling: HTMLElement, child: T): T {
+	sibling.after(child);
+	return child;
+}
+
+export function append<T extends Node>(parent: HTMLElement, child: T): T;
+export function append<T extends Node>(parent: HTMLElement, ...children: (T | string)[]): void;
+export function append<T extends Node>(parent: HTMLElement, ...children: (T | string)[]): T | void {
+	parent.append(...children);
+	if (children.length === 1 && typeof children[0] !== 'string') {
+		return <T>children[0];
+	}
+}
+
+export function prepend<T extends Node>(parent: HTMLElement, child: T): T {
+	parent.insertBefore(child, parent.firstChild);
+	return child;
+}
+
+/**
+ * Removes all children from `parent` and appends `children`
+ */
+export function reset(parent: HTMLElement, ...children: Array<Node | string>): void {
+	parent.innerText = '';
+	append(parent, ...children);
+}
+
+const SELECTOR_REGEX = /([\w\-]+)?(#([\w\-]+))?((\.([\w\-]+))*)/;
+
+export enum Namespace {
+	HTML = 'http://www.w3.org/1999/xhtml',
+	SVG = 'http://www.w3.org/2000/svg'
+}
+
+function _$<T extends Element>(namespace: Namespace, description: string, attrs?: { [key: string]: any }, ...children: Array<Node | string>): T {
+	const match = SELECTOR_REGEX.exec(description);
+
+	if (!match) {
+		throw new Error('Bad use of emmet');
+	}
+
+	const tagName = match[1] || 'div';
+	let result: T;
+
+	if (namespace !== Namespace.HTML) {
+		result = document.createElementNS(namespace as string, tagName) as T;
+	} else {
+		result = document.createElement(tagName) as unknown as T;
+	}
+
+	if (match[3]) {
+		result.id = match[3];
+	}
+	if (match[4]) {
+		result.className = match[4].replace(/\./g, ' ').trim();
+	}
+
+	if (attrs) {
+		Object.entries(attrs).forEach(([name, value]) => {
+			if (typeof value === 'undefined') {
+				return;
+			}
+
+			if (/^on\w+$/.test(name)) {
+				(<any>result)[name] = value;
+			} else if (name === 'selected') {
+				if (value) {
+					result.setAttribute(name, 'true');
+				}
+
+			} else {
+				result.setAttribute(name, value);
+			}
+		});
+	}
+
+	result.append(...children);
+
+	return result as T;
+}
+
+export function $<T extends HTMLElement>(description: string, attrs?: { [key: string]: any }, ...children: Array<Node | string>): T {
+	return _$(Namespace.HTML, description, attrs, ...children);
+}
+
+$.SVG = function <T extends SVGElement>(description: string, attrs?: { [key: string]: any }, ...children: Array<Node | string>): T {
+	return _$(Namespace.SVG, description, attrs, ...children);
+};
+
+export function join(nodes: Node[], separator: Node | string): Node[] {
+	const result: Node[] = [];
+
+	nodes.forEach((node, index) => {
+		if (index > 0) {
+			if (separator instanceof Node) {
+				result.push(separator.cloneNode());
+			} else {
+				result.push(document.createTextNode(separator));
+			}
+		}
+
+		result.push(node);
+	});
+
+	return result;
+}
+
+export function setVisibility(visible: boolean, ...elements: HTMLElement[]): void {
+	if (visible) {
+		show(...elements);
+	} else {
+		hide(...elements);
+	}
+}
+
+export function show(...elements: HTMLElement[]): void {
+	for (const element of elements) {
+		element.style.display = '';
+		element.removeAttribute('aria-hidden');
+	}
+}
+
+export function hide(...elements: HTMLElement[]): void {
+	for (const element of elements) {
+		element.style.display = 'none';
+		element.setAttribute('aria-hidden', 'true');
+	}
+}
+
+function findParentWithAttribute(node: Node | null, attribute: string): HTMLElement | null {
+	while (node && node.nodeType === node.ELEMENT_NODE) {
+		if (isHTMLElement(node) && node.hasAttribute(attribute)) {
+			return node;
+		}
+
+		node = node.parentNode;
+	}
+
+	return null;
+}
+
+export function removeTabIndexAndUpdateFocus(node: HTMLElement): void {
+	if (!node || !node.hasAttribute('tabIndex')) {
+		return;
+	}
+
+	// If we are the currently focused element and tabIndex is removed,
+	// standard DOM behavior is to move focus to the <body> element. We
+	// typically never want that, rather put focus to the closest element
+	// in the hierarchy of the parent DOM nodes.
+	if (node.ownerDocument.activeElement === node) {
+		const parentFocusable = findParentWithAttribute(node.parentElement, 'tabIndex');
+		parentFocusable?.focus();
+	}
+
+	node.removeAttribute('tabindex');
+}
+
+export function finalHandler<T extends Event>(fn: (event: T) => any): (event: T) => any {
+	return e => {
+		e.preventDefault();
+		e.stopPropagation();
+		fn(e);
+	};
+}
+
+export function domContentLoaded(targetWindow: Window): Promise<void> {
+	return new Promise<void>(resolve => {
+		const readyState = targetWindow.document.readyState;
+		if (readyState === 'complete' || (targetWindow.document && targetWindow.document.body !== null)) {
+			resolve(undefined);
+		} else {
+			const listener = () => {
+				targetWindow.window.removeEventListener('DOMContentLoaded', listener, false);
+				resolve();
+			};
+
+			targetWindow.window.addEventListener('DOMContentLoaded', listener, false);
+		}
+	});
+}
+
+/**
+ * Find a value usable for a dom node size such that the likelihood that it would be
+ * displayed with constant screen pixels size is as high as possible.
+ *
+ * e.g. We would desire for the cursors to be 2px (CSS px) wide. Under a devicePixelRatio
+ * of 1.25, the cursor will be 2.5 screen pixels wide. Depending on how the dom node aligns/"snaps"
+ * with the screen pixels, it will sometimes be rendered with 2 screen pixels, and sometimes with 3 screen pixels.
+ */
+export function computeScreenAwareSize(window: Window, cssPx: number): number {
+	const screenPx = window.devicePixelRatio * cssPx;
+	return Math.max(1, Math.floor(screenPx)) / window.devicePixelRatio;
+}
+
+/**
+ * Open safely a new window. This is the best way to do so, but you cannot tell
+ * if the window was opened or if it was blocked by the browser's popup blocker.
+ * If you want to tell if the browser blocked the new window, use {@link windowOpenWithSuccess}.
+ *
+ * See https://github.com/microsoft/monaco-editor/issues/601
+ * To protect against malicious code in the linked site, particularly phishing attempts,
+ * the window.opener should be set to null to prevent the linked site from having access
+ * to change the location of the current page.
+ * See https://mathiasbynens.github.io/rel-noopener/
+ */
+export function windowOpenNoOpener(url: string): void {
+	// By using 'noopener' in the `windowFeatures` argument, the newly created window will
+	// not be able to use `window.opener` to reach back to the current page.
+	// See https://stackoverflow.com/a/46958731
+	// See https://developer.mozilla.org/en-US/docs/Web/API/Window/open#noopener
+	// However, this also doesn't allow us to realize if the browser blocked
+	// the creation of the window.
+	mainWindow.open(url, '_blank', 'noopener');
+}
+
+/**
+ * Open a new window in a popup. This is the best way to do so, but you cannot tell
+ * if the window was opened or if it was blocked by the browser's popup blocker.
+ * If you want to tell if the browser blocked the new window, use {@link windowOpenWithSuccess}.
+ *
+ * Note: this does not set {@link window.opener} to null. This is to allow the opened popup to
+ * be able to use {@link window.close} to close itself. Because of this, you should only use
+ * this function on urls that you trust.
+ *
+ * In otherwords, you should almost always use {@link windowOpenNoOpener} instead of this function.
+ */
+const popupWidth = 780, popupHeight = 640;
+export function windowOpenPopup(url: string): void {
+	const left = Math.floor(mainWindow.screenLeft + mainWindow.innerWidth / 2 - popupWidth / 2);
+	const top = Math.floor(mainWindow.screenTop + mainWindow.innerHeight / 2 - popupHeight / 2);
+	mainWindow.open(
+		url,
+		'_blank',
+		`width=${popupWidth},height=${popupHeight},top=${top},left=${left}`
+	);
+}
+
+/**
+ * Attempts to open a window and returns whether it succeeded. This technique is
+ * not appropriate in certain contexts, like for example when the JS context is
+ * executing inside a sandboxed iframe. If it is not necessary to know if the
+ * browser blocked the new window, use {@link windowOpenNoOpener}.
+ *
+ * See https://github.com/microsoft/monaco-editor/issues/601
+ * See https://github.com/microsoft/monaco-editor/issues/2474
+ * See https://mathiasbynens.github.io/rel-noopener/
+ *
+ * @param url the url to open
+ * @param noOpener whether or not to set the {@link window.opener} to null. You should leave the default
+ * (true) unless you trust the url that is being opened.
+ * @returns boolean indicating if the {@link window.open} call succeeded
+ */
+export function windowOpenWithSuccess(url: string, noOpener = true): boolean {
+	const newTab = mainWindow.open();
+	if (newTab) {
+		if (noOpener) {
+			// see `windowOpenNoOpener` for details on why this is important
+			(newTab as any).opener = null;
+		}
+		newTab.location.href = url;
+		return true;
+	}
+	return false;
+}
+
+export function animate(targetWindow: Window, fn: () => void): IDisposable {
+	const step = () => {
+		fn();
+		stepDisposable = scheduleAtNextAnimationFrame(targetWindow, step);
+	};
+
+	let stepDisposable = scheduleAtNextAnimationFrame(targetWindow, step);
+	return toDisposable(() => stepDisposable.dispose());
+}
+
+export function asCSSPropertyValue(value: string) {
+	return `'${value.replace(/'/g, '%27')}'`;
+}
+
+export function asCssValueWithDefault(cssPropertyValue: string | undefined, dflt: string): string {
+	if (cssPropertyValue !== undefined) {
+		const variableMatch = cssPropertyValue.match(/^\s*var\((.+)\)$/);
+		if (variableMatch) {
+			const varArguments = variableMatch[1].split(',', 2);
+			if (varArguments.length === 2) {
+				dflt = asCssValueWithDefault(varArguments[1].trim(), dflt);
+			}
+			return `var(${varArguments[0]}, ${dflt})`;
+		}
+		return cssPropertyValue;
+	}
+	return dflt;
+}
+
+export enum DetectedFullscreenMode {
+
+	/**
+	 * The document is fullscreen, e.g. because an element
+	 * in the document requested to be fullscreen.
+	 */
+	DOCUMENT = 1,
+
+	/**
+	 * The browser is fullscreen, e.g. because the user enabled
+	 * native window fullscreen for it.
+	 */
+	BROWSER
+}
+
+export interface IDetectedFullscreen {
+
+	/**
+	 * Figure out if the document is fullscreen or the browser.
+	 */
+	mode: DetectedFullscreenMode;
+
+	/**
+	 * Whether we know for sure that we are in fullscreen mode or
+	 * it is a guess.
+	 */
+	guess: boolean;
+}
+
+export function detectFullscreen(targetWindow: Window): IDetectedFullscreen | null {
+
+	// Browser fullscreen: use DOM APIs to detect
+	if (targetWindow.document.fullscreenElement || (<any>targetWindow.document).webkitFullscreenElement || (<any>targetWindow.document).webkitIsFullScreen) {
+		return { mode: DetectedFullscreenMode.DOCUMENT, guess: false };
+	}
+
+	// There is no standard way to figure out if the browser
+	// is using native fullscreen. Via checking on screen
+	// height and comparing that to window height, we can guess
+	// it though.
+
+	if (targetWindow.innerHeight === targetWindow.screen.height) {
+		// if the height of the window matches the screen height, we can
+		// safely assume that the browser is fullscreen because no browser
+		// chrome is taking height away (e.g. like toolbars).
+		return { mode: DetectedFullscreenMode.BROWSER, guess: false };
+	}
+
+	if (platform.isMacintosh || platform.isLinux) {
+		// macOS and Linux do not properly report `innerHeight`, only Windows does
+		if (targetWindow.outerHeight === targetWindow.screen.height && targetWindow.outerWidth === targetWindow.screen.width) {
+			// if the height of the browser matches the screen height, we can
+			// only guess that we are in fullscreen. It is also possible that
+			// the user has turned off taskbars in the OS and the browser is
+			// simply able to span the entire size of the screen.
+			return { mode: DetectedFullscreenMode.BROWSER, guess: true };
+		}
+	}
+
+	// Not in fullscreen
+	return null;
+}
+
+/**
+ * Convert a Unicode string to a string in which each 16-bit unit occupies only one byte
+ *
+ * From https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/btoa
+ */
+function toBinary(str: string): string {
+	const codeUnits = new Uint16Array(str.length);
+	for (let i = 0; i < codeUnits.length; i++) {
+		codeUnits[i] = str.charCodeAt(i);
+	}
+	let binary = '';
+	const uint8array = new Uint8Array(codeUnits.buffer);
+	for (let i = 0; i < uint8array.length; i++) {
+		binary += String.fromCharCode(uint8array[i]);
+	}
+	return binary;
+}
+
+/**
+ * Version of the global `btoa` function that handles multi-byte characters instead
+ * of throwing an exception.
+ */
+export function multibyteAwareBtoa(str: string): string {
+	return btoa(toBinary(str));
+}
+
+type ModifierKey = 'alt' | 'ctrl' | 'shift' | 'meta';
+
+export interface IModifierKeyStatus {
+	altKey: boolean;
+	shiftKey: boolean;
+	ctrlKey: boolean;
+	metaKey: boolean;
+	lastKeyPressed?: ModifierKey;
+	lastKeyReleased?: ModifierKey;
+	event?: KeyboardEvent;
+}
+
+export class ModifierKeyEmitter extends event.Emitter<IModifierKeyStatus> {
+
+	private readonly _subscriptions = new DisposableStore();
+	private _keyStatus: IModifierKeyStatus;
+	private static instance: ModifierKeyEmitter;
+
+	private constructor() {
+		super();
+
+		this._keyStatus = {
+			altKey: false,
+			shiftKey: false,
+			ctrlKey: false,
+			metaKey: false
+		};
+
+		this._subscriptions.add(event.Event.runAndSubscribe(onDidRegisterWindow, ({ window, disposables }) => this.registerListeners(window, disposables), { window: mainWindow, disposables: this._subscriptions }));
+	}
+
+	private registerListeners(window: Window, disposables: DisposableStore): void {
+		disposables.add(addDisposableListener(window, 'keydown', e => {
+			if (e.defaultPrevented) {
+				return;
+			}
+
+			const event = new StandardKeyboardEvent(e);
+			// If Alt-key keydown event is repeated, ignore it #112347
+			// Only known to be necessary for Alt-Key at the moment #115810
+			if (event.keyCode === KeyCode.Alt && e.repeat) {
+				return;
+			}
+
+			if (e.altKey && !this._keyStatus.altKey) {
+				this._keyStatus.lastKeyPressed = 'alt';
+			} else if (e.ctrlKey && !this._keyStatus.ctrlKey) {
+				this._keyStatus.lastKeyPressed = 'ctrl';
+			} else if (e.metaKey && !this._keyStatus.metaKey) {
+				this._keyStatus.lastKeyPressed = 'meta';
+			} else if (e.shiftKey && !this._keyStatus.shiftKey) {
+				this._keyStatus.lastKeyPressed = 'shift';
+			} else if (event.keyCode !== KeyCode.Alt) {
+				this._keyStatus.lastKeyPressed = undefined;
+			} else {
+				return;
+			}
+
+			this._keyStatus.altKey = e.altKey;
+			this._keyStatus.ctrlKey = e.ctrlKey;
+			this._keyStatus.metaKey = e.metaKey;
+			this._keyStatus.shiftKey = e.shiftKey;
+
+			if (this._keyStatus.lastKeyPressed) {
+				this._keyStatus.event = e;
+				this.fire(this._keyStatus);
+			}
+		}, true));
+
+		disposables.add(addDisposableListener(window, 'keyup', e => {
+			if (e.defaultPrevented) {
+				return;
+			}
+
+			if (!e.altKey && this._keyStatus.altKey) {
+				this._keyStatus.lastKeyReleased = 'alt';
+			} else if (!e.ctrlKey && this._keyStatus.ctrlKey) {
+				this._keyStatus.lastKeyReleased = 'ctrl';
+			} else if (!e.metaKey && this._keyStatus.metaKey) {
+				this._keyStatus.lastKeyReleased = 'meta';
+			} else if (!e.shiftKey && this._keyStatus.shiftKey) {
+				this._keyStatus.lastKeyReleased = 'shift';
+			} else {
+				this._keyStatus.lastKeyReleased = undefined;
+			}
+
+			if (this._keyStatus.lastKeyPressed !== this._keyStatus.lastKeyReleased) {
+				this._keyStatus.lastKeyPressed = undefined;
+			}
+
+			this._keyStatus.altKey = e.altKey;
+			this._keyStatus.ctrlKey = e.ctrlKey;
+			this._keyStatus.metaKey = e.metaKey;
+			this._keyStatus.shiftKey = e.shiftKey;
+
+			if (this._keyStatus.lastKeyReleased) {
+				this._keyStatus.event = e;
+				this.fire(this._keyStatus);
+			}
+		}, true));
+
+		disposables.add(addDisposableListener(window.document.body, 'mousedown', () => {
+			this._keyStatus.lastKeyPressed = undefined;
+		}, true));
+
+		disposables.add(addDisposableListener(window.document.body, 'mouseup', () => {
+			this._keyStatus.lastKeyPressed = undefined;
+		}, true));
+
+		disposables.add(addDisposableListener(window.document.body, 'mousemove', e => {
+			if (e.buttons) {
+				this._keyStatus.lastKeyPressed = undefined;
+			}
+		}, true));
+
+		disposables.add(addDisposableListener(window, 'blur', () => {
+			this.resetKeyStatus();
+		}));
+	}
+
+	get keyStatus(): IModifierKeyStatus {
+		return this._keyStatus;
+	}
+
+	get isModifierPressed(): boolean {
+		return this._keyStatus.altKey || this._keyStatus.ctrlKey || this._keyStatus.metaKey || this._keyStatus.shiftKey;
+	}
+
+	/**
+	 * Allows to explicitly reset the key status based on more knowledge (#109062)
+	 */
+	resetKeyStatus(): void {
+		this.doResetKeyStatus();
+		this.fire(this._keyStatus);
+	}
+
+	private doResetKeyStatus(): void {
+		this._keyStatus = {
+			altKey: false,
+			shiftKey: false,
+			ctrlKey: false,
+			metaKey: false
+		};
+	}
+
+	static getInstance() {
+		if (!ModifierKeyEmitter.instance) {
+			ModifierKeyEmitter.instance = new ModifierKeyEmitter();
+		}
+
+		return ModifierKeyEmitter.instance;
+	}
+
+	override dispose() {
+		super.dispose();
+		this._subscriptions.dispose();
+	}
+}
+
+export function getCookieValue(name: string): string | undefined {
+	const match = document.cookie.match('(^|[^;]+)\\s*' + name + '\\s*=\\s*([^;]+)'); // See https://stackoverflow.com/a/25490531
+
+	return match ? match.pop() : undefined;
+}
+
+export interface IDragAndDropObserverCallbacks {
+	readonly onDragEnter?: (e: DragEvent) => void;
+	readonly onDragLeave?: (e: DragEvent) => void;
+	readonly onDrop?: (e: DragEvent) => void;
+	readonly onDragEnd?: (e: DragEvent) => void;
+	readonly onDragStart?: (e: DragEvent) => void;
+	readonly onDrag?: (e: DragEvent) => void;
+	readonly onDragOver?: (e: DragEvent, dragDuration: number) => void;
+}
+
+export class DragAndDropObserver extends Disposable {
+
+	// A helper to fix issues with repeated DRAG_ENTER / DRAG_LEAVE
+	// calls see https://github.com/microsoft/vscode/issues/14470
+	// when the element has child elements where the events are fired
+	// repeadedly.
+	private counter: number = 0;
+
+	// Allows to measure the duration of the drag operation.
+	private dragStartTime = 0;
+
+	constructor(private readonly element: HTMLElement, private readonly callbacks: IDragAndDropObserverCallbacks) {
+		super();
+
+		this.registerListeners();
+	}
+
+	private registerListeners(): void {
+		if (this.callbacks.onDragStart) {
+			this._register(addDisposableListener(this.element, EventType.DRAG_START, (e: DragEvent) => {
+				this.callbacks.onDragStart?.(e);
+			}));
+		}
+
+		if (this.callbacks.onDrag) {
+			this._register(addDisposableListener(this.element, EventType.DRAG, (e: DragEvent) => {
+				this.callbacks.onDrag?.(e);
+			}));
+		}
+
+		this._register(addDisposableListener(this.element, EventType.DRAG_ENTER, (e: DragEvent) => {
+			this.counter++;
+			this.dragStartTime = e.timeStamp;
+
+			this.callbacks.onDragEnter?.(e);
+		}));
+
+		this._register(addDisposableListener(this.element, EventType.DRAG_OVER, (e: DragEvent) => {
+			e.preventDefault(); // needed so that the drop event fires (https://stackoverflow.com/questions/21339924/drop-event-not-firing-in-chrome)
+
+			this.callbacks.onDragOver?.(e, e.timeStamp - this.dragStartTime);
+		}));
+
+		this._register(addDisposableListener(this.element, EventType.DRAG_LEAVE, (e: DragEvent) => {
+			this.counter--;
+
+			if (this.counter === 0) {
+				this.dragStartTime = 0;
+
+				this.callbacks.onDragLeave?.(e);
+			}
+		}));
+
+		this._register(addDisposableListener(this.element, EventType.DRAG_END, (e: DragEvent) => {
+			this.counter = 0;
+			this.dragStartTime = 0;
+
+			this.callbacks.onDragEnd?.(e);
+		}));
+
+		this._register(addDisposableListener(this.element, EventType.DROP, (e: DragEvent) => {
+			this.counter = 0;
+			this.dragStartTime = 0;
+
+			this.callbacks.onDrop?.(e);
+		}));
+	}
+}
+
+type HTMLElementAttributeKeys<T> = Partial<{ [K in keyof T]: T[K] extends Function ? never : T[K] extends object ? HTMLElementAttributeKeys<T[K]> : T[K] }>;
+type ElementAttributes<T> = HTMLElementAttributeKeys<T> & Record<string, any>;
+type RemoveHTMLElement<T> = T extends HTMLElement ? never : T;
+type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
+type ArrayToObj<T extends readonly any[]> = UnionToIntersection<RemoveHTMLElement<T[number]>>;
+type HHTMLElementTagNameMap = HTMLElementTagNameMap & { '': HTMLDivElement };
+
+type TagToElement<T> = T extends `${infer TStart}#${string}`
+	? TStart extends keyof HHTMLElementTagNameMap
+	? HHTMLElementTagNameMap[TStart]
+	: HTMLElement
+	: T extends `${infer TStart}.${string}`
+	? TStart extends keyof HHTMLElementTagNameMap
+	? HHTMLElementTagNameMap[TStart]
+	: HTMLElement
+	: T extends keyof HTMLElementTagNameMap
+	? HTMLElementTagNameMap[T]
+	: HTMLElement;
+
+type TagToElementAndId<TTag> = TTag extends `${infer TTag}@${infer TId}`
+	? { element: TagToElement<TTag>; id: TId }
+	: { element: TagToElement<TTag>; id: 'root' };
+
+type TagToRecord<TTag> = TagToElementAndId<TTag> extends { element: infer TElement; id: infer TId }
+	? Record<(TId extends string ? TId : never) | 'root', TElement>
+	: never;
+
+type Child = HTMLElement | string | Record<string, HTMLElement>;
+
+const H_REGEX = /(?<tag>[\w\-]+)?(?:#(?<id>[\w\-]+))?(?<class>(?:\.(?:[\w\-]+))*)(?:@(?<name>(?:[\w\_])+))?/;
+
+/**
+ * A helper function to create nested dom nodes.
+ *
+ *
+ * ```ts
+ * const elements = h('div.code-view', [
+ * 	h('div.title@title'),
+ * 	h('div.container', [
+ * 		h('div.gutter@gutterDiv'),
+ * 		h('div@editor'),
+ * 	]),
+ * ]);
+ * const editor = createEditor(elements.editor);
+ * ```
+*/
+export function h<TTag extends string>
+	(tag: TTag):
+	TagToRecord<TTag> extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
+
+export function h<TTag extends string, T extends Child[]>
+	(tag: TTag, children: [...T]):
+	(ArrayToObj<T> & TagToRecord<TTag>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
+
+export function h<TTag extends string>
+	(tag: TTag, attributes: Partial<ElementAttributes<TagToElement<TTag>>>):
+	TagToRecord<TTag> extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
+
+export function h<TTag extends string, T extends Child[]>
+	(tag: TTag, attributes: Partial<ElementAttributes<TagToElement<TTag>>>, children: [...T]):
+	(ArrayToObj<T> & TagToRecord<TTag>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
+
+export function h(tag: string, ...args: [] | [attributes: { $: string } & Partial<ElementAttributes<HTMLElement>> | Record<string, any>, children?: any[]] | [children: any[]]): Record<string, HTMLElement> {
+	let attributes: { $?: string } & Partial<ElementAttributes<HTMLElement>>;
+	let children: (Record<string, HTMLElement> | HTMLElement)[] | undefined;
+
+	if (Array.isArray(args[0])) {
+		attributes = {};
+		children = args[0];
+	} else {
+		attributes = args[0] as any || {};
+		children = args[1];
+	}
+
+	const match = H_REGEX.exec(tag);
+
+	if (!match || !match.groups) {
+		throw new Error('Bad use of h');
+	}
+
+	const tagName = match.groups['tag'] || 'div';
+	const el = document.createElement(tagName);
+
+	if (match.groups['id']) {
+		el.id = match.groups['id'];
+	}
+
+	const classNames = [];
+	if (match.groups['class']) {
+		for (const className of match.groups['class'].split('.')) {
+			if (className !== '') {
+				classNames.push(className);
+			}
+		}
+	}
+	if (attributes.className !== undefined) {
+		for (const className of attributes.className.split('.')) {
+			if (className !== '') {
+				classNames.push(className);
+			}
+		}
+	}
+	if (classNames.length > 0) {
+		el.className = classNames.join(' ');
+	}
+
+	const result: Record<string, HTMLElement> = {};
+
+	if (match.groups['name']) {
+		result[match.groups['name']] = el;
+	}
+
+	if (children) {
+		for (const c of children) {
+			if (isHTMLElement(c)) {
+				el.appendChild(c);
+			} else if (typeof c === 'string') {
+				el.append(c);
+			} else if ('root' in c) {
+				Object.assign(result, c);
+				el.appendChild(c.root);
+			}
+		}
+	}
+
+	for (const [key, value] of Object.entries(attributes)) {
+		if (key === 'className') {
+			continue;
+		} else if (key === 'style') {
+			for (const [cssKey, cssValue] of Object.entries(value)) {
+				el.style.setProperty(
+					camelCaseToHyphenCase(cssKey),
+					typeof cssValue === 'number' ? cssValue + 'px' : '' + cssValue
+				);
+			}
+		} else if (key === 'tabIndex') {
+			el.tabIndex = value;
+		} else {
+			el.setAttribute(camelCaseToHyphenCase(key), value.toString());
+		}
+	}
+
+	result['root'] = el;
+
+	return result;
+}
+
+export function svgElem<TTag extends string>
+	(tag: TTag):
+	TagToRecord<TTag> extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
+
+export function svgElem<TTag extends string, T extends Child[]>
+	(tag: TTag, children: [...T]):
+	(ArrayToObj<T> & TagToRecord<TTag>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
+
+export function svgElem<TTag extends string>
+	(tag: TTag, attributes: Partial<ElementAttributes<TagToElement<TTag>>>):
+	TagToRecord<TTag> extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
+
+export function svgElem<TTag extends string, T extends Child[]>
+	(tag: TTag, attributes: Partial<ElementAttributes<TagToElement<TTag>>>, children: [...T]):
+	(ArrayToObj<T> & TagToRecord<TTag>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
+
+export function svgElem(tag: string, ...args: [] | [attributes: { $: string } & Partial<ElementAttributes<HTMLElement>> | Record<string, any>, children?: any[]] | [children: any[]]): Record<string, HTMLElement> {
+	let attributes: { $?: string } & Partial<ElementAttributes<HTMLElement>>;
+	let children: (Record<string, HTMLElement> | HTMLElement)[] | undefined;
+
+	if (Array.isArray(args[0])) {
+		attributes = {};
+		children = args[0];
+	} else {
+		attributes = args[0] as any || {};
+		children = args[1];
+	}
+
+	const match = H_REGEX.exec(tag);
+
+	if (!match || !match.groups) {
+		throw new Error('Bad use of h');
+	}
+
+	const tagName = match.groups['tag'] || 'div';
+	const el = document.createElementNS('http://www.w3.org/2000/svg', tagName) as any as HTMLElement;
+
+	if (match.groups['id']) {
+		el.id = match.groups['id'];
+	}
+
+	const classNames = [];
+	if (match.groups['class']) {
+		for (const className of match.groups['class'].split('.')) {
+			if (className !== '') {
+				classNames.push(className);
+			}
+		}
+	}
+	if (attributes.className !== undefined) {
+		for (const className of attributes.className.split('.')) {
+			if (className !== '') {
+				classNames.push(className);
+			}
+		}
+	}
+	if (classNames.length > 0) {
+		el.className = classNames.join(' ');
+	}
+
+	const result: Record<string, HTMLElement> = {};
+
+	if (match.groups['name']) {
+		result[match.groups['name']] = el;
+	}
+
+	if (children) {
+		for (const c of children) {
+			if (isHTMLElement(c)) {
+				el.appendChild(c);
+			} else if (typeof c === 'string') {
+				el.append(c);
+			} else if ('root' in c) {
+				Object.assign(result, c);
+				el.appendChild(c.root);
+			}
+		}
+	}
+
+	for (const [key, value] of Object.entries(attributes)) {
+		if (key === 'className') {
+			continue;
+		} else if (key === 'style') {
+			for (const [cssKey, cssValue] of Object.entries(value)) {
+				el.style.setProperty(
+					camelCaseToHyphenCase(cssKey),
+					typeof cssValue === 'number' ? cssValue + 'px' : '' + cssValue
+				);
+			}
+		} else if (key === 'tabIndex') {
+			el.tabIndex = value;
+		} else {
+			el.setAttribute(camelCaseToHyphenCase(key), value.toString());
+		}
+	}
+
+	result['root'] = el;
+
+	return result;
+}
+
+function camelCaseToHyphenCase(str: string) {
+	return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
+}
+
+export function copyAttributes(from: Element, to: Element, filter?: string[]): void {
+	for (const { name, value } of from.attributes) {
+		if (!filter || filter.includes(name)) {
+			to.setAttribute(name, value);
+		}
+	}
+}
+
+function copyAttribute(from: Element, to: Element, name: string): void {
+	const value = from.getAttribute(name);
+	if (value) {
+		to.setAttribute(name, value);
+	} else {
+		to.removeAttribute(name);
+	}
+}
+
+export function trackAttributes(from: Element, to: Element, filter?: string[]): IDisposable {
+	copyAttributes(from, to, filter);
+
+	const disposables = new DisposableStore();
+
+	disposables.add(sharedMutationObserver.observe(from, disposables, { attributes: true, attributeFilter: filter })(mutations => {
+		for (const mutation of mutations) {
+			if (mutation.type === 'attributes' && mutation.attributeName) {
+				copyAttribute(from, to, mutation.attributeName);
+			}
+		}
+	}));
+
+	return disposables;
+}
+
+/**
+ * Helper for calculating the "safe triangle" occluded by hovers to avoid early dismissal.
+ * @see https://www.smashingmagazine.com/2023/08/better-context-menus-safe-triangles/ for example
+ */
+export class SafeTriangle {
+	// 4 triangles, 2 points (x, y) stored for each
+	private triangles: number[] = [];
+
+	constructor(
+		private readonly originX: number,
+		private readonly originY: number,
+		target: HTMLElement
+	) {
+		const { top, left, right, bottom } = target.getBoundingClientRect();
+		const t = this.triangles;
+		let i = 0;
+
+		t[i++] = left;
+		t[i++] = top;
+		t[i++] = right;
+		t[i++] = top;
+
+		t[i++] = left;
+		t[i++] = top;
+		t[i++] = left;
+		t[i++] = bottom;
+
+		t[i++] = right;
+		t[i++] = top;
+		t[i++] = right;
+		t[i++] = bottom;
+
+		t[i++] = left;
+		t[i++] = bottom;
+		t[i++] = right;
+		t[i++] = bottom;
+	}
+
+	public contains(x: number, y: number) {
+		const { triangles, originX, originY } = this;
+		for (let i = 0; i < 4; i++) {
+			if (isPointWithinTriangle(x, y, originX, originY, triangles[2 * i], triangles[2 * i + 1], triangles[2 * i + 2], triangles[2 * i + 3])) {
+				return true;
+			}
+		}
+
+		return false;
+	}
+}
diff --git a/src/vs/base/browser/fastDomNode.ts b/src/vs/base/browser/fastDomNode.ts
new file mode 100644
index 0000000000..1ef17e48cd
--- /dev/null
+++ b/src/vs/base/browser/fastDomNode.ts
@@ -0,0 +1,316 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+export class FastDomNode<T extends HTMLElement> {
+
+	private _maxWidth: string = '';
+	private _width: string = '';
+	private _height: string = '';
+	private _top: string = '';
+	private _left: string = '';
+	private _bottom: string = '';
+	private _right: string = '';
+	private _paddingTop: string = '';
+	private _paddingLeft: string = '';
+	private _paddingBottom: string = '';
+	private _paddingRight: string = '';
+	private _fontFamily: string = '';
+	private _fontWeight: string = '';
+	private _fontSize: string = '';
+	private _fontStyle: string = '';
+	private _fontFeatureSettings: string = '';
+	private _fontVariationSettings: string = '';
+	private _textDecoration: string = '';
+	private _lineHeight: string = '';
+	private _letterSpacing: string = '';
+	private _className: string = '';
+	private _display: string = '';
+	private _position: string = '';
+	private _visibility: string = '';
+	private _color: string = '';
+	private _backgroundColor: string = '';
+	private _layerHint: boolean = false;
+	private _contain: 'none' | 'strict' | 'content' | 'size' | 'layout' | 'style' | 'paint' = 'none';
+	private _boxShadow: string = '';
+
+	constructor(
+		public readonly domNode: T
+	) { }
+
+	public setMaxWidth(_maxWidth: number | string): void {
+		const maxWidth = numberAsPixels(_maxWidth);
+		if (this._maxWidth === maxWidth) {
+			return;
+		}
+		this._maxWidth = maxWidth;
+		this.domNode.style.maxWidth = this._maxWidth;
+	}
+
+	public setWidth(_width: number | string): void {
+		const width = numberAsPixels(_width);
+		if (this._width === width) {
+			return;
+		}
+		this._width = width;
+		this.domNode.style.width = this._width;
+	}
+
+	public setHeight(_height: number | string): void {
+		const height = numberAsPixels(_height);
+		if (this._height === height) {
+			return;
+		}
+		this._height = height;
+		this.domNode.style.height = this._height;
+	}
+
+	public setTop(_top: number | string): void {
+		const top = numberAsPixels(_top);
+		if (this._top === top) {
+			return;
+		}
+		this._top = top;
+		this.domNode.style.top = this._top;
+	}
+
+	public setLeft(_left: number | string): void {
+		const left = numberAsPixels(_left);
+		if (this._left === left) {
+			return;
+		}
+		this._left = left;
+		this.domNode.style.left = this._left;
+	}
+
+	public setBottom(_bottom: number | string): void {
+		const bottom = numberAsPixels(_bottom);
+		if (this._bottom === bottom) {
+			return;
+		}
+		this._bottom = bottom;
+		this.domNode.style.bottom = this._bottom;
+	}
+
+	public setRight(_right: number | string): void {
+		const right = numberAsPixels(_right);
+		if (this._right === right) {
+			return;
+		}
+		this._right = right;
+		this.domNode.style.right = this._right;
+	}
+
+	public setPaddingTop(_paddingTop: number | string): void {
+		const paddingTop = numberAsPixels(_paddingTop);
+		if (this._paddingTop === paddingTop) {
+			return;
+		}
+		this._paddingTop = paddingTop;
+		this.domNode.style.paddingTop = this._paddingTop;
+	}
+
+	public setPaddingLeft(_paddingLeft: number | string): void {
+		const paddingLeft = numberAsPixels(_paddingLeft);
+		if (this._paddingLeft === paddingLeft) {
+			return;
+		}
+		this._paddingLeft = paddingLeft;
+		this.domNode.style.paddingLeft = this._paddingLeft;
+	}
+
+	public setPaddingBottom(_paddingBottom: number | string): void {
+		const paddingBottom = numberAsPixels(_paddingBottom);
+		if (this._paddingBottom === paddingBottom) {
+			return;
+		}
+		this._paddingBottom = paddingBottom;
+		this.domNode.style.paddingBottom = this._paddingBottom;
+	}
+
+	public setPaddingRight(_paddingRight: number | string): void {
+		const paddingRight = numberAsPixels(_paddingRight);
+		if (this._paddingRight === paddingRight) {
+			return;
+		}
+		this._paddingRight = paddingRight;
+		this.domNode.style.paddingRight = this._paddingRight;
+	}
+
+	public setFontFamily(fontFamily: string): void {
+		if (this._fontFamily === fontFamily) {
+			return;
+		}
+		this._fontFamily = fontFamily;
+		this.domNode.style.fontFamily = this._fontFamily;
+	}
+
+	public setFontWeight(fontWeight: string): void {
+		if (this._fontWeight === fontWeight) {
+			return;
+		}
+		this._fontWeight = fontWeight;
+		this.domNode.style.fontWeight = this._fontWeight;
+	}
+
+	public setFontSize(_fontSize: number | string): void {
+		const fontSize = numberAsPixels(_fontSize);
+		if (this._fontSize === fontSize) {
+			return;
+		}
+		this._fontSize = fontSize;
+		this.domNode.style.fontSize = this._fontSize;
+	}
+
+	public setFontStyle(fontStyle: string): void {
+		if (this._fontStyle === fontStyle) {
+			return;
+		}
+		this._fontStyle = fontStyle;
+		this.domNode.style.fontStyle = this._fontStyle;
+	}
+
+	public setFontFeatureSettings(fontFeatureSettings: string): void {
+		if (this._fontFeatureSettings === fontFeatureSettings) {
+			return;
+		}
+		this._fontFeatureSettings = fontFeatureSettings;
+		this.domNode.style.fontFeatureSettings = this._fontFeatureSettings;
+	}
+
+	public setFontVariationSettings(fontVariationSettings: string): void {
+		if (this._fontVariationSettings === fontVariationSettings) {
+			return;
+		}
+		this._fontVariationSettings = fontVariationSettings;
+		this.domNode.style.fontVariationSettings = this._fontVariationSettings;
+	}
+
+	public setTextDecoration(textDecoration: string): void {
+		if (this._textDecoration === textDecoration) {
+			return;
+		}
+		this._textDecoration = textDecoration;
+		this.domNode.style.textDecoration = this._textDecoration;
+	}
+
+	public setLineHeight(_lineHeight: number | string): void {
+		const lineHeight = numberAsPixels(_lineHeight);
+		if (this._lineHeight === lineHeight) {
+			return;
+		}
+		this._lineHeight = lineHeight;
+		this.domNode.style.lineHeight = this._lineHeight;
+	}
+
+	public setLetterSpacing(_letterSpacing: number | string): void {
+		const letterSpacing = numberAsPixels(_letterSpacing);
+		if (this._letterSpacing === letterSpacing) {
+			return;
+		}
+		this._letterSpacing = letterSpacing;
+		this.domNode.style.letterSpacing = this._letterSpacing;
+	}
+
+	public setClassName(className: string): void {
+		if (this._className === className) {
+			return;
+		}
+		this._className = className;
+		this.domNode.className = this._className;
+	}
+
+	public toggleClassName(className: string, shouldHaveIt?: boolean): void {
+		this.domNode.classList.toggle(className, shouldHaveIt);
+		this._className = this.domNode.className;
+	}
+
+	public setDisplay(display: string): void {
+		if (this._display === display) {
+			return;
+		}
+		this._display = display;
+		this.domNode.style.display = this._display;
+	}
+
+	public setPosition(position: string): void {
+		if (this._position === position) {
+			return;
+		}
+		this._position = position;
+		this.domNode.style.position = this._position;
+	}
+
+	public setVisibility(visibility: string): void {
+		if (this._visibility === visibility) {
+			return;
+		}
+		this._visibility = visibility;
+		this.domNode.style.visibility = this._visibility;
+	}
+
+	public setColor(color: string): void {
+		if (this._color === color) {
+			return;
+		}
+		this._color = color;
+		this.domNode.style.color = this._color;
+	}
+
+	public setBackgroundColor(backgroundColor: string): void {
+		if (this._backgroundColor === backgroundColor) {
+			return;
+		}
+		this._backgroundColor = backgroundColor;
+		this.domNode.style.backgroundColor = this._backgroundColor;
+	}
+
+	public setLayerHinting(layerHint: boolean): void {
+		if (this._layerHint === layerHint) {
+			return;
+		}
+		this._layerHint = layerHint;
+		this.domNode.style.transform = this._layerHint ? 'translate3d(0px, 0px, 0px)' : '';
+	}
+
+	public setBoxShadow(boxShadow: string): void {
+		if (this._boxShadow === boxShadow) {
+			return;
+		}
+		this._boxShadow = boxShadow;
+		this.domNode.style.boxShadow = boxShadow;
+	}
+
+	public setContain(contain: 'none' | 'strict' | 'content' | 'size' | 'layout' | 'style' | 'paint'): void {
+		if (this._contain === contain) {
+			return;
+		}
+		this._contain = contain;
+		(<any>this.domNode.style).contain = this._contain;
+	}
+
+	public setAttribute(name: string, value: string): void {
+		this.domNode.setAttribute(name, value);
+	}
+
+	public removeAttribute(name: string): void {
+		this.domNode.removeAttribute(name);
+	}
+
+	public appendChild(child: FastDomNode<T>): void {
+		this.domNode.appendChild(child.domNode);
+	}
+
+	public removeChild(child: FastDomNode<T>): void {
+		this.domNode.removeChild(child.domNode);
+	}
+}
+
+function numberAsPixels(value: number | string): string {
+	return (typeof value === 'number' ? `${value}px` : value);
+}
+
+export function createFastDomNode<T extends HTMLElement>(domNode: T): FastDomNode<T> {
+	return new FastDomNode(domNode);
+}
diff --git a/src/vs/base/browser/globalPointerMoveMonitor.ts b/src/vs/base/browser/globalPointerMoveMonitor.ts
new file mode 100644
index 0000000000..9841596cdd
--- /dev/null
+++ b/src/vs/base/browser/globalPointerMoveMonitor.ts
@@ -0,0 +1,112 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import * as dom from 'vs/base/browser/dom';
+import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
+
+export interface IPointerMoveCallback {
+	(event: PointerEvent): void;
+}
+
+export interface IOnStopCallback {
+	(browserEvent?: PointerEvent | KeyboardEvent): void;
+}
+
+export class GlobalPointerMoveMonitor implements IDisposable {
+
+	private readonly _hooks = new DisposableStore();
+	private _pointerMoveCallback: IPointerMoveCallback | null = null;
+	private _onStopCallback: IOnStopCallback | null = null;
+
+	public dispose(): void {
+		this.stopMonitoring(false);
+		this._hooks.dispose();
+	}
+
+	public stopMonitoring(invokeStopCallback: boolean, browserEvent?: PointerEvent | KeyboardEvent): void {
+		if (!this.isMonitoring()) {
+			// Not monitoring
+			return;
+		}
+
+		// Unhook
+		this._hooks.clear();
+		this._pointerMoveCallback = null;
+		const onStopCallback = this._onStopCallback;
+		this._onStopCallback = null;
+
+		if (invokeStopCallback && onStopCallback) {
+			onStopCallback(browserEvent);
+		}
+	}
+
+	public isMonitoring(): boolean {
+		return !!this._pointerMoveCallback;
+	}
+
+	public startMonitoring(
+		initialElement: Element,
+		pointerId: number,
+		initialButtons: number,
+		pointerMoveCallback: IPointerMoveCallback,
+		onStopCallback: IOnStopCallback
+	): void {
+		if (this.isMonitoring()) {
+			this.stopMonitoring(false);
+		}
+		this._pointerMoveCallback = pointerMoveCallback;
+		this._onStopCallback = onStopCallback;
+
+		let eventSource: Element | Window = initialElement;
+
+		try {
+			initialElement.setPointerCapture(pointerId);
+			this._hooks.add(toDisposable(() => {
+				try {
+					initialElement.releasePointerCapture(pointerId);
+				} catch (err) {
+					// See https://github.com/microsoft/vscode/issues/161731
+					//
+					// `releasePointerCapture` sometimes fails when being invoked with the exception:
+					//     DOMException: Failed to execute 'releasePointerCapture' on 'Element':
+					//     No active pointer with the given id is found.
+					//
+					// There's no need to do anything in case of failure
+				}
+			}));
+		} catch (err) {
+			// See https://github.com/microsoft/vscode/issues/144584
+			// See https://github.com/microsoft/vscode/issues/146947
+			// `setPointerCapture` sometimes fails when being invoked
+			// from a `mousedown` listener on macOS and Windows
+			// and it always fails on Linux with the exception:
+			//     DOMException: Failed to execute 'setPointerCapture' on 'Element':
+			//     No active pointer with the given id is found.
+			// In case of failure, we bind the listeners on the window
+			eventSource = dom.getWindow(initialElement);
+		}
+
+		this._hooks.add(dom.addDisposableListener(
+			eventSource,
+			dom.EventType.POINTER_MOVE,
+			(e) => {
+				if (e.buttons !== initialButtons) {
+					// Buttons state has changed in the meantime
+					this.stopMonitoring(true);
+					return;
+				}
+
+				e.preventDefault();
+				this._pointerMoveCallback!(e);
+			}
+		));
+
+		this._hooks.add(dom.addDisposableListener(
+			eventSource,
+			dom.EventType.POINTER_UP,
+			(e: PointerEvent) => this.stopMonitoring(true)
+		));
+	}
+}
diff --git a/src/vs/base/browser/iframe.ts b/src/vs/base/browser/iframe.ts
new file mode 100644
index 0000000000..e8522e0311
--- /dev/null
+++ b/src/vs/base/browser/iframe.ts
@@ -0,0 +1,135 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+/**
+ * Represents a window in a possible chain of iframes
+ */
+interface IWindowChainElement {
+	/**
+	 * The window object for it
+	 */
+	readonly window: WeakRef<Window>;
+	/**
+	 * The iframe element inside the window.parent corresponding to window
+	 */
+	readonly iframeElement: Element | null;
+}
+
+const sameOriginWindowChainCache = new WeakMap<Window, IWindowChainElement[] | null>();
+
+function getParentWindowIfSameOrigin(w: Window): Window | null {
+	if (!w.parent || w.parent === w) {
+		return null;
+	}
+
+	// Cannot really tell if we have access to the parent window unless we try to access something in it
+	try {
+		const location = w.location;
+		const parentLocation = w.parent.location;
+		if (location.origin !== 'null' && parentLocation.origin !== 'null' && location.origin !== parentLocation.origin) {
+			return null;
+		}
+	} catch (e) {
+		return null;
+	}
+
+	return w.parent;
+}
+
+export class IframeUtils {
+
+	/**
+	 * Returns a chain of embedded windows with the same origin (which can be accessed programmatically).
+	 * Having a chain of length 1 might mean that the current execution environment is running outside of an iframe or inside an iframe embedded in a window with a different origin.
+	 */
+	private static getSameOriginWindowChain(targetWindow: Window): IWindowChainElement[] {
+		let windowChainCache = sameOriginWindowChainCache.get(targetWindow);
+		if (!windowChainCache) {
+			windowChainCache = [];
+			sameOriginWindowChainCache.set(targetWindow, windowChainCache);
+			let w: Window | null = targetWindow;
+			let parent: Window | null;
+			do {
+				parent = getParentWindowIfSameOrigin(w);
+				if (parent) {
+					windowChainCache.push({
+						window: new WeakRef(w),
+						iframeElement: w.frameElement || null
+					});
+				} else {
+					windowChainCache.push({
+						window: new WeakRef(w),
+						iframeElement: null
+					});
+				}
+				w = parent;
+			} while (w);
+		}
+		return windowChainCache.slice(0);
+	}
+
+	/**
+	 * Returns the position of `childWindow` relative to `ancestorWindow`
+	 */
+	public static getPositionOfChildWindowRelativeToAncestorWindow(childWindow: Window, ancestorWindow: Window | null) {
+
+		if (!ancestorWindow || childWindow === ancestorWindow) {
+			return {
+				top: 0,
+				left: 0
+			};
+		}
+
+		let top = 0, left = 0;
+
+		const windowChain = this.getSameOriginWindowChain(childWindow);
+
+		for (const windowChainEl of windowChain) {
+			const windowInChain = windowChainEl.window.deref();
+			top += windowInChain?.scrollY ?? 0;
+			left += windowInChain?.scrollX ?? 0;
+
+			if (windowInChain === ancestorWindow) {
+				break;
+			}
+
+			if (!windowChainEl.iframeElement) {
+				break;
+			}
+
+			const boundingRect = windowChainEl.iframeElement.getBoundingClientRect();
+			top += boundingRect.top;
+			left += boundingRect.left;
+		}
+
+		return {
+			top: top,
+			left: left
+		};
+	}
+}
+
+/**
+ * Returns a sha-256 composed of `parentOrigin` and `salt` converted to base 32
+ */
+export async function parentOriginHash(parentOrigin: string, salt: string): Promise<string> {
+	// 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/keyboardEvent.ts b/src/vs/base/browser/keyboardEvent.ts
new file mode 100644
index 0000000000..6aa5bf530f
--- /dev/null
+++ b/src/vs/base/browser/keyboardEvent.ts
@@ -0,0 +1,213 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import * as browser from 'vs/base/browser/browser';
+import { EVENT_KEY_CODE_MAP, KeyCode, KeyCodeUtils, KeyMod } from 'vs/base/common/keyCodes';
+import { KeyCodeChord } from 'vs/base/common/keybindings';
+import * as platform from 'vs/base/common/platform';
+
+
+
+function extractKeyCode(e: KeyboardEvent): KeyCode {
+	if (e.charCode) {
+		// "keypress" events mostly
+		const char = String.fromCharCode(e.charCode).toUpperCase();
+		return KeyCodeUtils.fromString(char);
+	}
+
+	const keyCode = e.keyCode;
+
+	// browser quirks
+	if (keyCode === 3) {
+		return KeyCode.PauseBreak;
+	} else if (browser.isFirefox) {
+		switch (keyCode) {
+			case 59: return KeyCode.Semicolon;
+			case 60:
+				if (platform.isLinux) { return KeyCode.IntlBackslash; }
+				break;
+			case 61: return KeyCode.Equal;
+			// based on: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode#numpad_keys
+			case 107: return KeyCode.NumpadAdd;
+			case 109: return KeyCode.NumpadSubtract;
+			case 173: return KeyCode.Minus;
+			case 224:
+				if (platform.isMacintosh) { return KeyCode.Meta; }
+				break;
+		}
+	} else if (browser.isWebKit) {
+		if (platform.isMacintosh && keyCode === 93) {
+			// the two meta keys in the Mac have different key codes (91 and 93)
+			return KeyCode.Meta;
+		} else if (!platform.isMacintosh && keyCode === 92) {
+			return KeyCode.Meta;
+		}
+	}
+
+	// cross browser keycodes:
+	return EVENT_KEY_CODE_MAP[keyCode] || KeyCode.Unknown;
+}
+
+export interface IKeyboardEvent {
+
+	readonly _standardKeyboardEventBrand: true;
+
+	readonly browserEvent: KeyboardEvent;
+	readonly target: HTMLElement;
+
+	readonly ctrlKey: boolean;
+	readonly shiftKey: boolean;
+	readonly altKey: boolean;
+	readonly metaKey: boolean;
+	readonly altGraphKey: boolean;
+	readonly keyCode: KeyCode;
+	readonly code: string;
+
+	/**
+	 * @internal
+	 */
+	toKeyCodeChord(): KeyCodeChord;
+	equals(keybinding: number): boolean;
+
+	preventDefault(): void;
+	stopPropagation(): void;
+}
+
+const ctrlKeyMod = (platform.isMacintosh ? KeyMod.WinCtrl : KeyMod.CtrlCmd);
+const altKeyMod = KeyMod.Alt;
+const shiftKeyMod = KeyMod.Shift;
+const metaKeyMod = (platform.isMacintosh ? KeyMod.CtrlCmd : KeyMod.WinCtrl);
+
+export function printKeyboardEvent(e: KeyboardEvent): string {
+	const modifiers: string[] = [];
+	if (e.ctrlKey) {
+		modifiers.push(`ctrl`);
+	}
+	if (e.shiftKey) {
+		modifiers.push(`shift`);
+	}
+	if (e.altKey) {
+		modifiers.push(`alt`);
+	}
+	if (e.metaKey) {
+		modifiers.push(`meta`);
+	}
+	return `modifiers: [${modifiers.join(',')}], code: ${e.code}, keyCode: ${e.keyCode}, key: ${e.key}`;
+}
+
+export function printStandardKeyboardEvent(e: StandardKeyboardEvent): string {
+	const modifiers: string[] = [];
+	if (e.ctrlKey) {
+		modifiers.push(`ctrl`);
+	}
+	if (e.shiftKey) {
+		modifiers.push(`shift`);
+	}
+	if (e.altKey) {
+		modifiers.push(`alt`);
+	}
+	if (e.metaKey) {
+		modifiers.push(`meta`);
+	}
+	return `modifiers: [${modifiers.join(',')}], code: ${e.code}, keyCode: ${e.keyCode} ('${KeyCodeUtils.toString(e.keyCode)}')`;
+}
+
+export class StandardKeyboardEvent implements IKeyboardEvent {
+
+	readonly _standardKeyboardEventBrand = true;
+
+	public readonly browserEvent: KeyboardEvent;
+	public readonly target: HTMLElement;
+
+	public readonly ctrlKey: boolean;
+	public readonly shiftKey: boolean;
+	public readonly altKey: boolean;
+	public readonly metaKey: boolean;
+	public readonly altGraphKey: boolean;
+	public readonly keyCode: KeyCode;
+	public readonly code: string;
+
+	private _asKeybinding: number;
+	private _asKeyCodeChord: KeyCodeChord;
+
+	constructor(source: KeyboardEvent) {
+		const e = source;
+
+		this.browserEvent = e;
+		this.target = <HTMLElement>e.target;
+
+		this.ctrlKey = e.ctrlKey;
+		this.shiftKey = e.shiftKey;
+		this.altKey = e.altKey;
+		this.metaKey = e.metaKey;
+		this.altGraphKey = e.getModifierState?.('AltGraph');
+		this.keyCode = extractKeyCode(e);
+		this.code = e.code;
+
+		// console.info(e.type + ": keyCode: " + e.keyCode + ", which: " + e.which + ", charCode: " + e.charCode + ", detail: " + e.detail + " ====> " + this.keyCode + ' -- ' + KeyCode[this.keyCode]);
+
+		this.ctrlKey = this.ctrlKey || this.keyCode === KeyCode.Ctrl;
+		this.altKey = this.altKey || this.keyCode === KeyCode.Alt;
+		this.shiftKey = this.shiftKey || this.keyCode === KeyCode.Shift;
+		this.metaKey = this.metaKey || this.keyCode === KeyCode.Meta;
+
+		this._asKeybinding = this._computeKeybinding();
+		this._asKeyCodeChord = this._computeKeyCodeChord();
+
+		// console.log(`code: ${e.code}, keyCode: ${e.keyCode}, key: ${e.key}`);
+	}
+
+	public preventDefault(): void {
+		if (this.browserEvent && this.browserEvent.preventDefault) {
+			this.browserEvent.preventDefault();
+		}
+	}
+
+	public stopPropagation(): void {
+		if (this.browserEvent && this.browserEvent.stopPropagation) {
+			this.browserEvent.stopPropagation();
+		}
+	}
+
+	public toKeyCodeChord(): KeyCodeChord {
+		return this._asKeyCodeChord;
+	}
+
+	public equals(other: number): boolean {
+		return this._asKeybinding === other;
+	}
+
+	private _computeKeybinding(): number {
+		let key = KeyCode.Unknown;
+		if (this.keyCode !== KeyCode.Ctrl && this.keyCode !== KeyCode.Shift && this.keyCode !== KeyCode.Alt && this.keyCode !== KeyCode.Meta) {
+			key = this.keyCode;
+		}
+
+		let result = 0;
+		if (this.ctrlKey) {
+			result |= ctrlKeyMod;
+		}
+		if (this.altKey) {
+			result |= altKeyMod;
+		}
+		if (this.shiftKey) {
+			result |= shiftKeyMod;
+		}
+		if (this.metaKey) {
+			result |= metaKeyMod;
+		}
+		result |= key;
+
+		return result;
+	}
+
+	private _computeKeyCodeChord(): KeyCodeChord {
+		let key = KeyCode.Unknown;
+		if (this.keyCode !== KeyCode.Ctrl && this.keyCode !== KeyCode.Shift && this.keyCode !== KeyCode.Alt && this.keyCode !== KeyCode.Meta) {
+			key = this.keyCode;
+		}
+		return new KeyCodeChord(this.ctrlKey, this.shiftKey, this.altKey, this.metaKey, key);
+	}
+}
diff --git a/src/vs/base/browser/mouseEvent.ts b/src/vs/base/browser/mouseEvent.ts
new file mode 100644
index 0000000000..51de0152e2
--- /dev/null
+++ b/src/vs/base/browser/mouseEvent.ts
@@ -0,0 +1,229 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import * as browser from 'vs/base/browser/browser';
+import { IframeUtils } from 'vs/base/browser/iframe';
+import * as platform from 'vs/base/common/platform';
+
+export interface IMouseEvent {
+	readonly browserEvent: MouseEvent;
+	readonly leftButton: boolean;
+	readonly middleButton: boolean;
+	readonly rightButton: boolean;
+	readonly buttons: number;
+	readonly target: HTMLElement;
+	readonly detail: number;
+	readonly posx: number;
+	readonly posy: number;
+	readonly ctrlKey: boolean;
+	readonly shiftKey: boolean;
+	readonly altKey: boolean;
+	readonly metaKey: boolean;
+	readonly timestamp: number;
+
+	preventDefault(): void;
+	stopPropagation(): void;
+}
+
+export class StandardMouseEvent implements IMouseEvent {
+
+	public readonly browserEvent: MouseEvent;
+
+	public readonly leftButton: boolean;
+	public readonly middleButton: boolean;
+	public readonly rightButton: boolean;
+	public readonly buttons: number;
+	public readonly target: HTMLElement;
+	public detail: number;
+	public readonly posx: number;
+	public readonly posy: number;
+	public readonly ctrlKey: boolean;
+	public readonly shiftKey: boolean;
+	public readonly altKey: boolean;
+	public readonly metaKey: boolean;
+	public readonly timestamp: number;
+
+	constructor(targetWindow: Window, e: MouseEvent) {
+		this.timestamp = Date.now();
+		this.browserEvent = e;
+		this.leftButton = e.button === 0;
+		this.middleButton = e.button === 1;
+		this.rightButton = e.button === 2;
+		this.buttons = e.buttons;
+
+		this.target = <HTMLElement>e.target;
+
+		this.detail = e.detail || 1;
+		if (e.type === 'dblclick') {
+			this.detail = 2;
+		}
+		this.ctrlKey = e.ctrlKey;
+		this.shiftKey = e.shiftKey;
+		this.altKey = e.altKey;
+		this.metaKey = e.metaKey;
+
+		if (typeof e.pageX === 'number') {
+			this.posx = e.pageX;
+			this.posy = e.pageY;
+		} else {
+			// Probably hit by MSGestureEvent
+			this.posx = e.clientX + this.target.ownerDocument.body.scrollLeft + this.target.ownerDocument.documentElement.scrollLeft;
+			this.posy = e.clientY + this.target.ownerDocument.body.scrollTop + this.target.ownerDocument.documentElement.scrollTop;
+		}
+
+		// Find the position of the iframe this code is executing in relative to the iframe where the event was captured.
+		const iframeOffsets = IframeUtils.getPositionOfChildWindowRelativeToAncestorWindow(targetWindow, e.view);
+		this.posx -= iframeOffsets.left;
+		this.posy -= iframeOffsets.top;
+	}
+
+	public preventDefault(): void {
+		this.browserEvent.preventDefault();
+	}
+
+	public stopPropagation(): void {
+		this.browserEvent.stopPropagation();
+	}
+}
+
+export class DragMouseEvent extends StandardMouseEvent {
+
+	public readonly dataTransfer: DataTransfer;
+
+	constructor(targetWindow: Window, e: MouseEvent) {
+		super(targetWindow, e);
+		this.dataTransfer = (<any>e).dataTransfer;
+	}
+}
+
+export interface IMouseWheelEvent extends MouseEvent {
+	readonly wheelDelta: number;
+	readonly wheelDeltaX: number;
+	readonly wheelDeltaY: number;
+
+	readonly deltaX: number;
+	readonly deltaY: number;
+	readonly deltaZ: number;
+	readonly deltaMode: number;
+}
+
+interface IWebKitMouseWheelEvent {
+	wheelDeltaY: number;
+	wheelDeltaX: number;
+}
+
+interface IGeckoMouseWheelEvent {
+	HORIZONTAL_AXIS: number;
+	VERTICAL_AXIS: number;
+	axis: number;
+	detail: number;
+}
+
+export class StandardWheelEvent {
+
+	public readonly browserEvent: IMouseWheelEvent | null;
+	public readonly deltaY: number;
+	public readonly deltaX: number;
+	public readonly target: Node;
+
+	constructor(e: IMouseWheelEvent | null, deltaX: number = 0, deltaY: number = 0) {
+
+		this.browserEvent = e || null;
+		this.target = e ? (e.target || (<any>e).targetNode || e.srcElement) : null;
+
+		this.deltaY = deltaY;
+		this.deltaX = deltaX;
+
+		let shouldFactorDPR: boolean = false;
+		if (browser.isChrome) {
+			// Chrome version >= 123 contains the fix to factor devicePixelRatio into the wheel event.
+			// See https://chromium.googlesource.com/chromium/src.git/+/be51b448441ff0c9d1f17e0f25c4bf1ab3f11f61
+			const chromeVersionMatch = navigator.userAgent.match(/Chrome\/(\d+)/);
+			const chromeMajorVersion = chromeVersionMatch ? parseInt(chromeVersionMatch[1]) : 123;
+			shouldFactorDPR = chromeMajorVersion <= 122;
+		}
+
+		if (e) {
+			// Old (deprecated) wheel events
+			const e1 = <IWebKitMouseWheelEvent><any>e;
+			const e2 = <IGeckoMouseWheelEvent><any>e;
+			const devicePixelRatio = e.view?.devicePixelRatio || 1;
+
+			// vertical delta scroll
+			if (typeof e1.wheelDeltaY !== 'undefined') {
+				if (shouldFactorDPR) {
+					// Refs https://github.com/microsoft/vscode/issues/146403#issuecomment-1854538928
+					this.deltaY = e1.wheelDeltaY / (120 * devicePixelRatio);
+				} else {
+					this.deltaY = e1.wheelDeltaY / 120;
+				}
+			} else if (typeof e2.VERTICAL_AXIS !== 'undefined' && e2.axis === e2.VERTICAL_AXIS) {
+				this.deltaY = -e2.detail / 3;
+			} else if (e.type === 'wheel') {
+				// Modern wheel event
+				// https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent
+				const ev = <WheelEvent><unknown>e;
+
+				if (ev.deltaMode === ev.DOM_DELTA_LINE) {
+					// the deltas are expressed in lines
+					if (browser.isFirefox && !platform.isMacintosh) {
+						this.deltaY = -e.deltaY / 3;
+					} else {
+						this.deltaY = -e.deltaY;
+					}
+				} else {
+					this.deltaY = -e.deltaY / 40;
+				}
+			}
+
+			// horizontal delta scroll
+			if (typeof e1.wheelDeltaX !== 'undefined') {
+				if (browser.isSafari && platform.isWindows) {
+					this.deltaX = - (e1.wheelDeltaX / 120);
+				} else if (shouldFactorDPR) {
+					// Refs https://github.com/microsoft/vscode/issues/146403#issuecomment-1854538928
+					this.deltaX = e1.wheelDeltaX / (120 * devicePixelRatio);
+				} else {
+					this.deltaX = e1.wheelDeltaX / 120;
+				}
+			} else if (typeof e2.HORIZONTAL_AXIS !== 'undefined' && e2.axis === e2.HORIZONTAL_AXIS) {
+				this.deltaX = -e.detail / 3;
+			} else if (e.type === 'wheel') {
+				// Modern wheel event
+				// https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent
+				const ev = <WheelEvent><unknown>e;
+
+				if (ev.deltaMode === ev.DOM_DELTA_LINE) {
+					// the deltas are expressed in lines
+					if (browser.isFirefox && !platform.isMacintosh) {
+						this.deltaX = -e.deltaX / 3;
+					} else {
+						this.deltaX = -e.deltaX;
+					}
+				} else {
+					this.deltaX = -e.deltaX / 40;
+				}
+			}
+
+			// Assume a vertical scroll if nothing else worked
+			if (this.deltaY === 0 && this.deltaX === 0 && e.wheelDelta) {
+				if (shouldFactorDPR) {
+					// Refs https://github.com/microsoft/vscode/issues/146403#issuecomment-1854538928
+					this.deltaY = e.wheelDelta / (120 * devicePixelRatio);
+				} else {
+					this.deltaY = e.wheelDelta / 120;
+				}
+			}
+		}
+	}
+
+	public preventDefault(): void {
+		this.browserEvent?.preventDefault();
+	}
+
+	public stopPropagation(): void {
+		this.browserEvent?.stopPropagation();
+	}
+}
diff --git a/src/vs/base/browser/touch.ts b/src/vs/base/browser/touch.ts
new file mode 100644
index 0000000000..a4e8dffaad
--- /dev/null
+++ b/src/vs/base/browser/touch.ts
@@ -0,0 +1,372 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import * as DomUtils from 'vs/base/browser/dom';
+import { mainWindow } from 'vs/base/browser/window';
+import * as arrays from 'vs/base/common/arrays';
+import { memoize } from 'vs/base/common/decorators';
+import { Event as EventUtils } from 'vs/base/common/event';
+import { Disposable, IDisposable, markAsSingleton, toDisposable } from 'vs/base/common/lifecycle';
+import { LinkedList } from 'vs/base/common/linkedList';
+
+export namespace EventType {
+	export const Tap = '-xterm-gesturetap';
+	export const Change = '-xterm-gesturechange';
+	export const Start = '-xterm-gesturestart';
+	export const End = '-xterm-gesturesend';
+	export const Contextmenu = '-xterm-gesturecontextmenu';
+}
+
+interface TouchData {
+	id: number;
+	initialTarget: EventTarget;
+	initialTimeStamp: number;
+	initialPageX: number;
+	initialPageY: number;
+	rollingTimestamps: number[];
+	rollingPageX: number[];
+	rollingPageY: number[];
+}
+
+export interface GestureEvent extends MouseEvent {
+	initialTarget: EventTarget | undefined;
+	translationX: number;
+	translationY: number;
+	pageX: number;
+	pageY: number;
+	tapCount: number;
+}
+
+interface Touch {
+	identifier: number;
+	screenX: number;
+	screenY: number;
+	clientX: number;
+	clientY: number;
+	pageX: number;
+	pageY: number;
+	radiusX: number;
+	radiusY: number;
+	rotationAngle: number;
+	force: number;
+	target: Element;
+}
+
+interface TouchList {
+	[i: number]: Touch;
+	length: number;
+	item(index: number): Touch;
+	identifiedTouch(id: number): Touch;
+}
+
+interface TouchEvent extends Event {
+	touches: TouchList;
+	targetTouches: TouchList;
+	changedTouches: TouchList;
+}
+
+export class Gesture extends Disposable {
+
+	private static readonly SCROLL_FRICTION = -0.005;
+	private static INSTANCE: Gesture;
+	private static readonly HOLD_DELAY = 700;
+
+	private dispatched = false;
+	private readonly targets = new LinkedList<HTMLElement>();
+	private readonly ignoreTargets = new LinkedList<HTMLElement>();
+	private handle: IDisposable | null;
+
+	private readonly activeTouches: { [id: number]: TouchData };
+
+	private _lastSetTapCountTime: number;
+
+	private static readonly CLEAR_TAP_COUNT_TIME = 400; // ms
+
+
+	private constructor() {
+		super();
+
+		this.activeTouches = {};
+		this.handle = null;
+		this._lastSetTapCountTime = 0;
+
+		this._register(EventUtils.runAndSubscribe(DomUtils.onDidRegisterWindow, ({ window, disposables }) => {
+			disposables.add(DomUtils.addDisposableListener(window.document, 'touchstart', (e: TouchEvent) => this.onTouchStart(e), { passive: false }));
+			disposables.add(DomUtils.addDisposableListener(window.document, 'touchend', (e: TouchEvent) => this.onTouchEnd(window, e)));
+			disposables.add(DomUtils.addDisposableListener(window.document, 'touchmove', (e: TouchEvent) => this.onTouchMove(e), { passive: false }));
+		}, { window: mainWindow, disposables: this._store }));
+	}
+
+	public static addTarget(element: HTMLElement): IDisposable {
+		if (!Gesture.isTouchDevice()) {
+			return Disposable.None;
+		}
+		if (!Gesture.INSTANCE) {
+			Gesture.INSTANCE = markAsSingleton(new Gesture());
+		}
+
+		const remove = Gesture.INSTANCE.targets.push(element);
+		return toDisposable(remove);
+	}
+
+	public static ignoreTarget(element: HTMLElement): IDisposable {
+		if (!Gesture.isTouchDevice()) {
+			return Disposable.None;
+		}
+		if (!Gesture.INSTANCE) {
+			Gesture.INSTANCE = markAsSingleton(new Gesture());
+		}
+
+		const remove = Gesture.INSTANCE.ignoreTargets.push(element);
+		return toDisposable(remove);
+	}
+
+	@memoize
+	static isTouchDevice(): boolean {
+		// `'ontouchstart' in window` always evaluates to true with typescript's modern typings. This causes `window` to be
+		// `never` later in `window.navigator`. That's why we need the explicit `window as Window` cast
+		return 'ontouchstart' in mainWindow || navigator.maxTouchPoints > 0;
+	}
+
+	public override dispose(): void {
+		if (this.handle) {
+			this.handle.dispose();
+			this.handle = null;
+		}
+
+		super.dispose();
+	}
+
+	private onTouchStart(e: TouchEvent): void {
+		const timestamp = Date.now(); // use Date.now() because on FF e.timeStamp is not epoch based.
+
+		if (this.handle) {
+			this.handle.dispose();
+			this.handle = null;
+		}
+
+		for (let i = 0, len = e.targetTouches.length; i < len; i++) {
+			const touch = e.targetTouches.item(i);
+
+			this.activeTouches[touch.identifier] = {
+				id: touch.identifier,
+				initialTarget: touch.target,
+				initialTimeStamp: timestamp,
+				initialPageX: touch.pageX,
+				initialPageY: touch.pageY,
+				rollingTimestamps: [timestamp],
+				rollingPageX: [touch.pageX],
+				rollingPageY: [touch.pageY]
+			};
+
+			const evt = this.newGestureEvent(EventType.Start, touch.target);
+			evt.pageX = touch.pageX;
+			evt.pageY = touch.pageY;
+			this.dispatchEvent(evt);
+		}
+
+		if (this.dispatched) {
+			e.preventDefault();
+			e.stopPropagation();
+			this.dispatched = false;
+		}
+	}
+
+	private onTouchEnd(targetWindow: Window, e: TouchEvent): void {
+		const timestamp = Date.now(); // use Date.now() because on FF e.timeStamp is not epoch based.
+
+		const activeTouchCount = Object.keys(this.activeTouches).length;
+
+		for (let i = 0, len = e.changedTouches.length; i < len; i++) {
+
+			const touch = e.changedTouches.item(i);
+
+			if (!this.activeTouches.hasOwnProperty(String(touch.identifier))) {
+				console.warn('move of an UNKNOWN touch', touch);
+				continue;
+			}
+
+			const data = this.activeTouches[touch.identifier],
+				holdTime = Date.now() - data.initialTimeStamp;
+
+			if (holdTime < Gesture.HOLD_DELAY
+				&& Math.abs(data.initialPageX - arrays.tail(data.rollingPageX)!) < 30
+				&& Math.abs(data.initialPageY - arrays.tail(data.rollingPageY)!) < 30) {
+
+				const evt = this.newGestureEvent(EventType.Tap, data.initialTarget);
+				evt.pageX = arrays.tail(data.rollingPageX)!;
+				evt.pageY = arrays.tail(data.rollingPageY)!;
+				this.dispatchEvent(evt);
+
+			} else if (holdTime >= Gesture.HOLD_DELAY
+				&& Math.abs(data.initialPageX - arrays.tail(data.rollingPageX)!) < 30
+				&& Math.abs(data.initialPageY - arrays.tail(data.rollingPageY)!) < 30) {
+
+				const evt = this.newGestureEvent(EventType.Contextmenu, data.initialTarget);
+				evt.pageX = arrays.tail(data.rollingPageX)!;
+				evt.pageY = arrays.tail(data.rollingPageY)!;
+				this.dispatchEvent(evt);
+
+			} else if (activeTouchCount === 1) {
+				const finalX = arrays.tail(data.rollingPageX)!;
+				const finalY = arrays.tail(data.rollingPageY)!;
+
+				const deltaT = arrays.tail(data.rollingTimestamps)! - data.rollingTimestamps[0];
+				const deltaX = finalX - data.rollingPageX[0];
+				const deltaY = finalY - data.rollingPageY[0];
+
+				// We need to get all the dispatch targets on the start of the inertia event
+				const dispatchTo = [...this.targets].filter(t => data.initialTarget instanceof Node && t.contains(data.initialTarget));
+				this.inertia(targetWindow, dispatchTo, timestamp,	// time now
+					Math.abs(deltaX) / deltaT,						// speed
+					deltaX > 0 ? 1 : -1,							// x direction
+					finalX,											// x now
+					Math.abs(deltaY) / deltaT,  					// y speed
+					deltaY > 0 ? 1 : -1,							// y direction
+					finalY											// y now
+				);
+			}
+
+
+			this.dispatchEvent(this.newGestureEvent(EventType.End, data.initialTarget));
+			// forget about this touch
+			delete this.activeTouches[touch.identifier];
+		}
+
+		if (this.dispatched) {
+			e.preventDefault();
+			e.stopPropagation();
+			this.dispatched = false;
+		}
+	}
+
+	private newGestureEvent(type: string, initialTarget?: EventTarget): GestureEvent {
+		const event = document.createEvent('CustomEvent') as unknown as GestureEvent;
+		event.initEvent(type, false, true);
+		event.initialTarget = initialTarget;
+		event.tapCount = 0;
+		return event;
+	}
+
+	private dispatchEvent(event: GestureEvent): void {
+		if (event.type === EventType.Tap) {
+			const currentTime = (new Date()).getTime();
+			let setTapCount = 0;
+			if (currentTime - this._lastSetTapCountTime > Gesture.CLEAR_TAP_COUNT_TIME) {
+				setTapCount = 1;
+			} else {
+				setTapCount = 2;
+			}
+
+			this._lastSetTapCountTime = currentTime;
+			event.tapCount = setTapCount;
+		} else if (event.type === EventType.Change || event.type === EventType.Contextmenu) {
+			// tap is canceled by scrolling or context menu
+			this._lastSetTapCountTime = 0;
+		}
+
+		if (event.initialTarget instanceof Node) {
+			for (const ignoreTarget of this.ignoreTargets) {
+				if (ignoreTarget.contains(event.initialTarget)) {
+					return;
+				}
+			}
+
+			const targets: [number, HTMLElement][] = [];
+			for (const target of this.targets) {
+				if (target.contains(event.initialTarget)) {
+					let depth = 0;
+					let now: Node | null = event.initialTarget;
+					while (now && now !== target) {
+						depth++;
+						now = now.parentElement;
+					}
+					targets.push([depth, target]);
+				}
+			}
+
+			targets.sort((a, b) => a[0] - b[0]);
+
+			for (const [_, target] of targets) {
+				target.dispatchEvent(event);
+				this.dispatched = true;
+			}
+		}
+	}
+
+	private inertia(targetWindow: Window, dispatchTo: readonly EventTarget[], t1: number, vX: number, dirX: number, x: number, vY: number, dirY: number, y: number): void {
+		this.handle = DomUtils.scheduleAtNextAnimationFrame(targetWindow, () => {
+			const now = Date.now();
+
+			// velocity: old speed + accel_over_time
+			const deltaT = now - t1;
+			let delta_pos_x = 0, delta_pos_y = 0;
+			let stopped = true;
+
+			vX += Gesture.SCROLL_FRICTION * deltaT;
+			vY += Gesture.SCROLL_FRICTION * deltaT;
+
+			if (vX > 0) {
+				stopped = false;
+				delta_pos_x = dirX * vX * deltaT;
+			}
+
+			if (vY > 0) {
+				stopped = false;
+				delta_pos_y = dirY * vY * deltaT;
+			}
+
+			// dispatch translation event
+			const evt = this.newGestureEvent(EventType.Change);
+			evt.translationX = delta_pos_x;
+			evt.translationY = delta_pos_y;
+			dispatchTo.forEach(d => d.dispatchEvent(evt));
+
+			if (!stopped) {
+				this.inertia(targetWindow, dispatchTo, now, vX, dirX, x + delta_pos_x, vY, dirY, y + delta_pos_y);
+			}
+		});
+	}
+
+	private onTouchMove(e: TouchEvent): void {
+		const timestamp = Date.now(); // use Date.now() because on FF e.timeStamp is not epoch based.
+
+		for (let i = 0, len = e.changedTouches.length; i < len; i++) {
+
+			const touch = e.changedTouches.item(i);
+
+			if (!this.activeTouches.hasOwnProperty(String(touch.identifier))) {
+				console.warn('end of an UNKNOWN touch', touch);
+				continue;
+			}
+
+			const data = this.activeTouches[touch.identifier];
+
+			const evt = this.newGestureEvent(EventType.Change, data.initialTarget);
+			evt.translationX = touch.pageX - arrays.tail(data.rollingPageX)!;
+			evt.translationY = touch.pageY - arrays.tail(data.rollingPageY)!;
+			evt.pageX = touch.pageX;
+			evt.pageY = touch.pageY;
+			this.dispatchEvent(evt);
+
+			// only keep a few data points, to average the final speed
+			if (data.rollingPageX.length > 3) {
+				data.rollingPageX.shift();
+				data.rollingPageY.shift();
+				data.rollingTimestamps.shift();
+			}
+
+			data.rollingPageX.push(touch.pageX);
+			data.rollingPageY.push(touch.pageY);
+			data.rollingTimestamps.push(timestamp);
+		}
+
+		if (this.dispatched) {
+			e.preventDefault();
+			e.stopPropagation();
+			this.dispatched = false;
+		}
+	}
+}
diff --git a/src/vs/base/browser/ui/scrollbar/abstractScrollbar.ts b/src/vs/base/browser/ui/scrollbar/abstractScrollbar.ts
new file mode 100644
index 0000000000..5ab75393d9
--- /dev/null
+++ b/src/vs/base/browser/ui/scrollbar/abstractScrollbar.ts
@@ -0,0 +1,303 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import * as dom from 'vs/base/browser/dom';
+import { createFastDomNode, FastDomNode } from 'vs/base/browser/fastDomNode';
+import { GlobalPointerMoveMonitor } from 'vs/base/browser/globalPointerMoveMonitor';
+import { StandardWheelEvent } from 'vs/base/browser/mouseEvent';
+import { ScrollbarArrow, ScrollbarArrowOptions } from 'vs/base/browser/ui/scrollbar/scrollbarArrow';
+import { ScrollbarState } from 'vs/base/browser/ui/scrollbar/scrollbarState';
+import { ScrollbarVisibilityController } from 'vs/base/browser/ui/scrollbar/scrollbarVisibilityController';
+import { Widget } from 'vs/base/browser/ui/widget';
+import * as platform from 'vs/base/common/platform';
+import { INewScrollPosition, Scrollable, ScrollbarVisibility } from 'vs/base/common/scrollable';
+
+/**
+ * The orthogonal distance to the slider at which dragging "resets". This implements "snapping"
+ */
+const POINTER_DRAG_RESET_DISTANCE = 140;
+
+export interface ISimplifiedPointerEvent {
+	buttons: number;
+	pageX: number;
+	pageY: number;
+}
+
+export interface ScrollbarHost {
+	onMouseWheel(mouseWheelEvent: StandardWheelEvent): void;
+	onDragStart(): void;
+	onDragEnd(): void;
+}
+
+export interface AbstractScrollbarOptions {
+	lazyRender: boolean;
+	host: ScrollbarHost;
+	scrollbarState: ScrollbarState;
+	visibility: ScrollbarVisibility;
+	extraScrollbarClassName: string;
+	scrollable: Scrollable;
+	scrollByPage: boolean;
+}
+
+export abstract class AbstractScrollbar extends Widget {
+
+	protected _host: ScrollbarHost;
+	protected _scrollable: Scrollable;
+	protected _scrollByPage: boolean;
+	private _lazyRender: boolean;
+	protected _scrollbarState: ScrollbarState;
+	protected _visibilityController: ScrollbarVisibilityController;
+	private _pointerMoveMonitor: GlobalPointerMoveMonitor;
+
+	public domNode: FastDomNode<HTMLElement>;
+	public slider!: FastDomNode<HTMLElement>;
+
+	protected _shouldRender: boolean;
+
+	constructor(opts: AbstractScrollbarOptions) {
+		super();
+		this._lazyRender = opts.lazyRender;
+		this._host = opts.host;
+		this._scrollable = opts.scrollable;
+		this._scrollByPage = opts.scrollByPage;
+		this._scrollbarState = opts.scrollbarState;
+		this._visibilityController = this._register(new ScrollbarVisibilityController(opts.visibility, 'visible scrollbar ' + opts.extraScrollbarClassName, 'invisible scrollbar ' + opts.extraScrollbarClassName));
+		this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded());
+		this._pointerMoveMonitor = this._register(new GlobalPointerMoveMonitor());
+		this._shouldRender = true;
+		this.domNode = createFastDomNode(document.createElement('div'));
+		this.domNode.setAttribute('role', 'presentation');
+		this.domNode.setAttribute('aria-hidden', 'true');
+
+		this._visibilityController.setDomNode(this.domNode);
+		this.domNode.setPosition('absolute');
+
+		this._register(dom.addDisposableListener(this.domNode.domNode, dom.EventType.POINTER_DOWN, (e: PointerEvent) => this._domNodePointerDown(e)));
+	}
+
+	// ----------------- creation
+
+	/**
+	 * Creates the dom node for an arrow & adds it to the container
+	 */
+	protected _createArrow(opts: ScrollbarArrowOptions): void {
+		const arrow = this._register(new ScrollbarArrow(opts));
+		this.domNode.domNode.appendChild(arrow.bgDomNode);
+		this.domNode.domNode.appendChild(arrow.domNode);
+	}
+
+	/**
+	 * Creates the slider dom node, adds it to the container & hooks up the events
+	 */
+	protected _createSlider(top: number, left: number, width: number | undefined, height: number | undefined): void {
+		this.slider = createFastDomNode(document.createElement('div'));
+		this.slider.setClassName('slider');
+		this.slider.setPosition('absolute');
+		this.slider.setTop(top);
+		this.slider.setLeft(left);
+		if (typeof width === 'number') {
+			this.slider.setWidth(width);
+		}
+		if (typeof height === 'number') {
+			this.slider.setHeight(height);
+		}
+		this.slider.setLayerHinting(true);
+		this.slider.setContain('strict');
+
+		this.domNode.domNode.appendChild(this.slider.domNode);
+
+		this._register(dom.addDisposableListener(
+			this.slider.domNode,
+			dom.EventType.POINTER_DOWN,
+			(e: PointerEvent) => {
+				if (e.button === 0) {
+					e.preventDefault();
+					this._sliderPointerDown(e);
+				}
+			}
+		));
+
+		this.onclick(this.slider.domNode, e => {
+			if (e.leftButton) {
+				e.stopPropagation();
+			}
+		});
+	}
+
+	// ----------------- Update state
+
+	protected _onElementSize(visibleSize: number): boolean {
+		if (this._scrollbarState.setVisibleSize(visibleSize)) {
+			this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded());
+			this._shouldRender = true;
+			if (!this._lazyRender) {
+				this.render();
+			}
+		}
+		return this._shouldRender;
+	}
+
+	protected _onElementScrollSize(elementScrollSize: number): boolean {
+		if (this._scrollbarState.setScrollSize(elementScrollSize)) {
+			this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded());
+			this._shouldRender = true;
+			if (!this._lazyRender) {
+				this.render();
+			}
+		}
+		return this._shouldRender;
+	}
+
+	protected _onElementScrollPosition(elementScrollPosition: number): boolean {
+		if (this._scrollbarState.setScrollPosition(elementScrollPosition)) {
+			this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded());
+			this._shouldRender = true;
+			if (!this._lazyRender) {
+				this.render();
+			}
+		}
+		return this._shouldRender;
+	}
+
+	// ----------------- rendering
+
+	public beginReveal(): void {
+		this._visibilityController.setShouldBeVisible(true);
+	}
+
+	public beginHide(): void {
+		this._visibilityController.setShouldBeVisible(false);
+	}
+
+	public render(): void {
+		if (!this._shouldRender) {
+			return;
+		}
+		this._shouldRender = false;
+
+		this._renderDomNode(this._scrollbarState.getRectangleLargeSize(), this._scrollbarState.getRectangleSmallSize());
+		this._updateSlider(this._scrollbarState.getSliderSize(), this._scrollbarState.getArrowSize() + this._scrollbarState.getSliderPosition());
+	}
+	// ----------------- DOM events
+
+	private _domNodePointerDown(e: PointerEvent): void {
+		if (e.target !== this.domNode.domNode) {
+			return;
+		}
+		this._onPointerDown(e);
+	}
+
+	public delegatePointerDown(e: PointerEvent): void {
+		const domTop = this.domNode.domNode.getClientRects()[0].top;
+		const sliderStart = domTop + this._scrollbarState.getSliderPosition();
+		const sliderStop = domTop + this._scrollbarState.getSliderPosition() + this._scrollbarState.getSliderSize();
+		const pointerPos = this._sliderPointerPosition(e);
+		if (sliderStart <= pointerPos && pointerPos <= sliderStop) {
+			// Act as if it was a pointer down on the slider
+			if (e.button === 0) {
+				e.preventDefault();
+				this._sliderPointerDown(e);
+			}
+		} else {
+			// Act as if it was a pointer down on the scrollbar
+			this._onPointerDown(e);
+		}
+	}
+
+	private _onPointerDown(e: PointerEvent): void {
+		let offsetX: number;
+		let offsetY: number;
+		if (e.target === this.domNode.domNode && typeof e.offsetX === 'number' && typeof e.offsetY === 'number') {
+			offsetX = e.offsetX;
+			offsetY = e.offsetY;
+		} else {
+			const domNodePosition = dom.getDomNodePagePosition(this.domNode.domNode);
+			offsetX = e.pageX - domNodePosition.left;
+			offsetY = e.pageY - domNodePosition.top;
+		}
+
+		const offset = this._pointerDownRelativePosition(offsetX, offsetY);
+		this._setDesiredScrollPositionNow(
+			this._scrollByPage
+				? this._scrollbarState.getDesiredScrollPositionFromOffsetPaged(offset)
+				: this._scrollbarState.getDesiredScrollPositionFromOffset(offset)
+		);
+
+		if (e.button === 0) {
+			// left button
+			e.preventDefault();
+			this._sliderPointerDown(e);
+		}
+	}
+
+	private _sliderPointerDown(e: PointerEvent): void {
+		if (!e.target || !(e.target instanceof Element)) {
+			return;
+		}
+		const initialPointerPosition = this._sliderPointerPosition(e);
+		const initialPointerOrthogonalPosition = this._sliderOrthogonalPointerPosition(e);
+		const initialScrollbarState = this._scrollbarState.clone();
+		this.slider.toggleClassName('active', true);
+
+		this._pointerMoveMonitor.startMonitoring(
+			e.target,
+			e.pointerId,
+			e.buttons,
+			(pointerMoveData: PointerEvent) => {
+				const pointerOrthogonalPosition = this._sliderOrthogonalPointerPosition(pointerMoveData);
+				const pointerOrthogonalDelta = Math.abs(pointerOrthogonalPosition - initialPointerOrthogonalPosition);
+
+				if (platform.isWindows && pointerOrthogonalDelta > POINTER_DRAG_RESET_DISTANCE) {
+					// The pointer has wondered away from the scrollbar => reset dragging
+					this._setDesiredScrollPositionNow(initialScrollbarState.getScrollPosition());
+					return;
+				}
+
+				const pointerPosition = this._sliderPointerPosition(pointerMoveData);
+				const pointerDelta = pointerPosition - initialPointerPosition;
+				this._setDesiredScrollPositionNow(initialScrollbarState.getDesiredScrollPositionFromDelta(pointerDelta));
+			},
+			() => {
+				this.slider.toggleClassName('active', false);
+				this._host.onDragEnd();
+			}
+		);
+
+		this._host.onDragStart();
+	}
+
+	private _setDesiredScrollPositionNow(_desiredScrollPosition: number): void {
+
+		const desiredScrollPosition: INewScrollPosition = {};
+		this.writeScrollPosition(desiredScrollPosition, _desiredScrollPosition);
+
+		this._scrollable.setScrollPositionNow(desiredScrollPosition);
+	}
+
+	public updateScrollbarSize(scrollbarSize: number): void {
+		this._updateScrollbarSize(scrollbarSize);
+		this._scrollbarState.setScrollbarSize(scrollbarSize);
+		this._shouldRender = true;
+		if (!this._lazyRender) {
+			this.render();
+		}
+	}
+
+	public isNeeded(): boolean {
+		return this._scrollbarState.isNeeded();
+	}
+
+	// ----------------- Overwrite these
+
+	protected abstract _renderDomNode(largeSize: number, smallSize: number): void;
+	protected abstract _updateSlider(sliderSize: number, sliderPosition: number): void;
+
+	protected abstract _pointerDownRelativePosition(offsetX: number, offsetY: number): number;
+	protected abstract _sliderPointerPosition(e: ISimplifiedPointerEvent): number;
+	protected abstract _sliderOrthogonalPointerPosition(e: ISimplifiedPointerEvent): number;
+	protected abstract _updateScrollbarSize(size: number): void;
+
+	public abstract writeScrollPosition(target: INewScrollPosition, scrollPosition: number): void;
+}
diff --git a/src/vs/base/browser/ui/scrollbar/horizontalScrollbar.ts b/src/vs/base/browser/ui/scrollbar/horizontalScrollbar.ts
new file mode 100644
index 0000000000..e64e4226d7
--- /dev/null
+++ b/src/vs/base/browser/ui/scrollbar/horizontalScrollbar.ts
@@ -0,0 +1,114 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { AbstractScrollbar, ISimplifiedPointerEvent, ScrollbarHost } from 'vs/base/browser/ui/scrollbar/abstractScrollbar';
+import { ScrollableElementResolvedOptions } from 'vs/base/browser/ui/scrollbar/scrollableElementOptions';
+import { ScrollbarState } from 'vs/base/browser/ui/scrollbar/scrollbarState';
+import { INewScrollPosition, Scrollable, ScrollbarVisibility, ScrollEvent } from 'vs/base/common/scrollable';
+
+
+
+
+export class HorizontalScrollbar extends AbstractScrollbar {
+
+	constructor(scrollable: Scrollable, options: ScrollableElementResolvedOptions, host: ScrollbarHost) {
+		const scrollDimensions = scrollable.getScrollDimensions();
+		const scrollPosition = scrollable.getCurrentScrollPosition();
+		super({
+			lazyRender: options.lazyRender,
+			host: host,
+			scrollbarState: new ScrollbarState(
+				(options.horizontalHasArrows ? options.arrowSize : 0),
+				(options.horizontal === ScrollbarVisibility.Hidden ? 0 : options.horizontalScrollbarSize),
+				(options.vertical === ScrollbarVisibility.Hidden ? 0 : options.verticalScrollbarSize),
+				scrollDimensions.width,
+				scrollDimensions.scrollWidth,
+				scrollPosition.scrollLeft
+			),
+			visibility: options.horizontal,
+			extraScrollbarClassName: 'horizontal',
+			scrollable: scrollable,
+			scrollByPage: options.scrollByPage
+		});
+
+		if (options.horizontalHasArrows) {
+      throw new Error('horizontalHasArrows is not supported in xterm.js');
+			// const arrowDelta = (options.arrowSize - ARROW_IMG_SIZE) / 2;
+			// const scrollbarDelta = (options.horizontalScrollbarSize - ARROW_IMG_SIZE) / 2;
+
+			// this._createArrow({
+			// 	className: 'scra',
+			// 	icon: Codicon.scrollbarButtonLeft,
+			// 	top: scrollbarDelta,
+			// 	left: arrowDelta,
+			// 	bottom: undefined,
+			// 	right: undefined,
+			// 	bgWidth: options.arrowSize,
+			// 	bgHeight: options.horizontalScrollbarSize,
+			// 	onActivate: () => this._host.onMouseWheel(new StandardWheelEvent(null, 1, 0)),
+			// });
+
+			// this._createArrow({
+			// 	className: 'scra',
+			// 	icon: Codicon.scrollbarButtonRight,
+			// 	top: scrollbarDelta,
+			// 	left: undefined,
+			// 	bottom: undefined,
+			// 	right: arrowDelta,
+			// 	bgWidth: options.arrowSize,
+			// 	bgHeight: options.horizontalScrollbarSize,
+			// 	onActivate: () => this._host.onMouseWheel(new StandardWheelEvent(null, -1, 0)),
+			// });
+		}
+
+		this._createSlider(Math.floor((options.horizontalScrollbarSize - options.horizontalSliderSize) / 2), 0, undefined, options.horizontalSliderSize);
+	}
+
+	protected _updateSlider(sliderSize: number, sliderPosition: number): void {
+		this.slider.setWidth(sliderSize);
+		this.slider.setLeft(sliderPosition);
+	}
+
+	protected _renderDomNode(largeSize: number, smallSize: number): void {
+		this.domNode.setWidth(largeSize);
+		this.domNode.setHeight(smallSize);
+		this.domNode.setLeft(0);
+		this.domNode.setBottom(0);
+	}
+
+	public onDidScroll(e: ScrollEvent): boolean {
+		this._shouldRender = this._onElementScrollSize(e.scrollWidth) || this._shouldRender;
+		this._shouldRender = this._onElementScrollPosition(e.scrollLeft) || this._shouldRender;
+		this._shouldRender = this._onElementSize(e.width) || this._shouldRender;
+		return this._shouldRender;
+	}
+
+	protected _pointerDownRelativePosition(offsetX: number, offsetY: number): number {
+		return offsetX;
+	}
+
+	protected _sliderPointerPosition(e: ISimplifiedPointerEvent): number {
+		return e.pageX;
+	}
+
+	protected _sliderOrthogonalPointerPosition(e: ISimplifiedPointerEvent): number {
+		return e.pageY;
+	}
+
+	protected _updateScrollbarSize(size: number): void {
+		this.slider.setHeight(size);
+	}
+
+	public writeScrollPosition(target: INewScrollPosition, scrollPosition: number): void {
+		target.scrollLeft = scrollPosition;
+	}
+
+	public updateOptions(options: ScrollableElementResolvedOptions): void {
+		this.updateScrollbarSize(options.horizontal === ScrollbarVisibility.Hidden ? 0 : options.horizontalScrollbarSize);
+		this._scrollbarState.setOppositeScrollbarSize(options.vertical === ScrollbarVisibility.Hidden ? 0 : options.verticalScrollbarSize);
+		this._visibilityController.setVisibility(options.horizontal);
+		this._scrollByPage = options.scrollByPage;
+	}
+}
diff --git a/src/vs/base/browser/ui/scrollbar/scrollableElement.ts b/src/vs/base/browser/ui/scrollbar/scrollableElement.ts
new file mode 100644
index 0000000000..d7d77e6f4b
--- /dev/null
+++ b/src/vs/base/browser/ui/scrollbar/scrollableElement.ts
@@ -0,0 +1,718 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { getZoomFactor, isChrome } from 'vs/base/browser/browser';
+import * as dom from 'vs/base/browser/dom';
+import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
+import { IMouseEvent, IMouseWheelEvent, StandardWheelEvent } from 'vs/base/browser/mouseEvent';
+import { ScrollbarHost } from 'vs/base/browser/ui/scrollbar/abstractScrollbar';
+import { HorizontalScrollbar } from 'vs/base/browser/ui/scrollbar/horizontalScrollbar';
+import { ScrollableElementChangeOptions, ScrollableElementCreationOptions, ScrollableElementResolvedOptions } from 'vs/base/browser/ui/scrollbar/scrollableElementOptions';
+import { VerticalScrollbar } from 'vs/base/browser/ui/scrollbar/verticalScrollbar';
+import { Widget } from 'vs/base/browser/ui/widget';
+import { TimeoutTimer } from 'vs/base/common/async';
+import { Emitter, Event } from 'vs/base/common/event';
+import { IDisposable, dispose } from 'vs/base/common/lifecycle';
+import * as platform from 'vs/base/common/platform';
+import { INewScrollDimensions, INewScrollPosition, IScrollDimensions, IScrollPosition, ScrollEvent, Scrollable, ScrollbarVisibility } from 'vs/base/common/scrollable';
+// import 'vs/css!./media/scrollbars';
+
+const HIDE_TIMEOUT = 500;
+const SCROLL_WHEEL_SENSITIVITY = 50;
+const SCROLL_WHEEL_SMOOTH_SCROLL_ENABLED = true;
+
+export interface IOverviewRulerLayoutInfo {
+	parent: HTMLElement;
+	insertBefore: HTMLElement;
+}
+
+class MouseWheelClassifierItem {
+	public timestamp: number;
+	public deltaX: number;
+	public deltaY: number;
+	public score: number;
+
+	constructor(timestamp: number, deltaX: number, deltaY: number) {
+		this.timestamp = timestamp;
+		this.deltaX = deltaX;
+		this.deltaY = deltaY;
+		this.score = 0;
+	}
+}
+
+export class MouseWheelClassifier {
+
+	public static readonly INSTANCE = new MouseWheelClassifier();
+
+	private readonly _capacity: number;
+	private _memory: MouseWheelClassifierItem[];
+	private _front: number;
+	private _rear: number;
+
+	constructor() {
+		this._capacity = 5;
+		this._memory = [];
+		this._front = -1;
+		this._rear = -1;
+	}
+
+	public isPhysicalMouseWheel(): boolean {
+		if (this._front === -1 && this._rear === -1) {
+			// no elements
+			return false;
+		}
+
+		// 0.5 * last + 0.25 * 2nd last + 0.125 * 3rd last + ...
+		let remainingInfluence = 1;
+		let score = 0;
+		let iteration = 1;
+
+		let index = this._rear;
+		do {
+			const influence = (index === this._front ? remainingInfluence : Math.pow(2, -iteration));
+			remainingInfluence -= influence;
+			score += this._memory[index].score * influence;
+
+			if (index === this._front) {
+				break;
+			}
+
+			index = (this._capacity + index - 1) % this._capacity;
+			iteration++;
+		} while (true);
+
+		return (score <= 0.5);
+	}
+
+	public acceptStandardWheelEvent(e: StandardWheelEvent): void {
+		if (isChrome) {
+			const targetWindow = dom.getWindow(e.browserEvent);
+			const pageZoomFactor = getZoomFactor(targetWindow);
+			// On Chrome, the incoming delta events are multiplied with the OS zoom factor.
+			// The OS zoom factor can be reverse engineered by using the device pixel ratio and the configured zoom factor into account.
+			this.accept(Date.now(), e.deltaX * pageZoomFactor, e.deltaY * pageZoomFactor);
+		} else {
+			this.accept(Date.now(), e.deltaX, e.deltaY);
+		}
+	}
+
+	public accept(timestamp: number, deltaX: number, deltaY: number): void {
+		let previousItem = null;
+		const item = new MouseWheelClassifierItem(timestamp, deltaX, deltaY);
+
+		if (this._front === -1 && this._rear === -1) {
+			this._memory[0] = item;
+			this._front = 0;
+			this._rear = 0;
+		} else {
+			previousItem = this._memory[this._rear];
+
+			this._rear = (this._rear + 1) % this._capacity;
+			if (this._rear === this._front) {
+				// Drop oldest
+				this._front = (this._front + 1) % this._capacity;
+			}
+			this._memory[this._rear] = item;
+		}
+
+		item.score = this._computeScore(item, previousItem);
+	}
+
+	/**
+	 * A score between 0 and 1 for `item`.
+	 *  - a score towards 0 indicates that the source appears to be a physical mouse wheel
+	 *  - a score towards 1 indicates that the source appears to be a touchpad or magic mouse, etc.
+	 */
+	private _computeScore(item: MouseWheelClassifierItem, previousItem: MouseWheelClassifierItem | null): number {
+
+		if (Math.abs(item.deltaX) > 0 && Math.abs(item.deltaY) > 0) {
+			// both axes exercised => definitely not a physical mouse wheel
+			return 1;
+		}
+
+		let score: number = 0.5;
+
+		if (!this._isAlmostInt(item.deltaX) || !this._isAlmostInt(item.deltaY)) {
+			// non-integer deltas => indicator that this is not a physical mouse wheel
+			score += 0.25;
+		}
+
+		// Non-accelerating scroll => indicator that this is a physical mouse wheel
+		// These can be identified by seeing whether they are the module of one another.
+		if (previousItem) {
+			const absDeltaX = Math.abs(item.deltaX);
+			const absDeltaY = Math.abs(item.deltaY);
+
+			const absPreviousDeltaX = Math.abs(previousItem.deltaX);
+			const absPreviousDeltaY = Math.abs(previousItem.deltaY);
+
+			// Min 1 to avoid division by zero, module 1 will still be 0.
+			const minDeltaX = Math.max(Math.min(absDeltaX, absPreviousDeltaX), 1);
+			const minDeltaY = Math.max(Math.min(absDeltaY, absPreviousDeltaY), 1);
+
+			const maxDeltaX = Math.max(absDeltaX, absPreviousDeltaX);
+			const maxDeltaY = Math.max(absDeltaY, absPreviousDeltaY);
+
+			const isSameModulo = (maxDeltaX % minDeltaX === 0 && maxDeltaY % minDeltaY === 0);
+			if (isSameModulo) {
+				score -= 0.5;
+			}
+		}
+
+		return Math.min(Math.max(score, 0), 1);
+	}
+
+	private _isAlmostInt(value: number): boolean {
+		const delta = Math.abs(Math.round(value) - value);
+		return (delta < 0.01);
+	}
+}
+
+export abstract class AbstractScrollableElement extends Widget {
+
+	private readonly _options: ScrollableElementResolvedOptions;
+	protected readonly _scrollable: Scrollable;
+	private readonly _verticalScrollbar: VerticalScrollbar;
+	private readonly _horizontalScrollbar: HorizontalScrollbar;
+	private readonly _domNode: HTMLElement;
+
+	private readonly _leftShadowDomNode: FastDomNode<HTMLElement> | null;
+	private readonly _topShadowDomNode: FastDomNode<HTMLElement> | null;
+	private readonly _topLeftShadowDomNode: FastDomNode<HTMLElement> | null;
+
+	private readonly _listenOnDomNode: HTMLElement;
+
+	private _mouseWheelToDispose: IDisposable[];
+
+	private _isDragging: boolean;
+	private _mouseIsOver: boolean;
+
+	private readonly _hideTimeout: TimeoutTimer;
+	private _shouldRender: boolean;
+
+	private _revealOnScroll: boolean;
+
+	private readonly _onScroll = this._register(new Emitter<ScrollEvent>());
+	public readonly onScroll: Event<ScrollEvent> = this._onScroll.event;
+
+	private readonly _onWillScroll = this._register(new Emitter<ScrollEvent>());
+	public readonly onWillScroll: Event<ScrollEvent> = this._onWillScroll.event;
+
+	public get options(): Readonly<ScrollableElementResolvedOptions> {
+		return this._options;
+	}
+
+	protected constructor(element: HTMLElement, options: ScrollableElementCreationOptions, scrollable: Scrollable) {
+		super();
+		element.style.overflow = 'hidden';
+		this._options = resolveOptions(options);
+		this._scrollable = scrollable;
+
+		this._register(this._scrollable.onScroll((e) => {
+			this._onWillScroll.fire(e);
+			this._onDidScroll(e);
+			this._onScroll.fire(e);
+		}));
+
+		const scrollbarHost: ScrollbarHost = {
+			onMouseWheel: (mouseWheelEvent: StandardWheelEvent) => this._onMouseWheel(mouseWheelEvent),
+			onDragStart: () => this._onDragStart(),
+			onDragEnd: () => this._onDragEnd(),
+		};
+		this._verticalScrollbar = this._register(new VerticalScrollbar(this._scrollable, this._options, scrollbarHost));
+		this._horizontalScrollbar = this._register(new HorizontalScrollbar(this._scrollable, this._options, scrollbarHost));
+
+		this._domNode = document.createElement('div');
+		this._domNode.className = 'xterm-scrollable-element ' + this._options.className;
+		this._domNode.setAttribute('role', 'presentation');
+		this._domNode.style.position = 'relative';
+		this._domNode.style.overflow = 'hidden';
+		this._domNode.appendChild(element);
+		this._domNode.appendChild(this._horizontalScrollbar.domNode.domNode);
+		this._domNode.appendChild(this._verticalScrollbar.domNode.domNode);
+
+		if (this._options.useShadows) {
+			this._leftShadowDomNode = createFastDomNode(document.createElement('div'));
+			this._leftShadowDomNode.setClassName('shadow');
+			this._domNode.appendChild(this._leftShadowDomNode.domNode);
+
+			this._topShadowDomNode = createFastDomNode(document.createElement('div'));
+			this._topShadowDomNode.setClassName('shadow');
+			this._domNode.appendChild(this._topShadowDomNode.domNode);
+
+			this._topLeftShadowDomNode = createFastDomNode(document.createElement('div'));
+			this._topLeftShadowDomNode.setClassName('shadow');
+			this._domNode.appendChild(this._topLeftShadowDomNode.domNode);
+		} else {
+			this._leftShadowDomNode = null;
+			this._topShadowDomNode = null;
+			this._topLeftShadowDomNode = null;
+		}
+
+		this._listenOnDomNode = this._options.listenOnDomNode || this._domNode;
+
+		this._mouseWheelToDispose = [];
+		this._setListeningToMouseWheel(this._options.handleMouseWheel);
+
+		this.onmouseover(this._listenOnDomNode, (e) => this._onMouseOver(e));
+		this.onmouseleave(this._listenOnDomNode, (e) => this._onMouseLeave(e));
+
+		this._hideTimeout = this._register(new TimeoutTimer());
+		this._isDragging = false;
+		this._mouseIsOver = false;
+
+		this._shouldRender = true;
+
+		this._revealOnScroll = true;
+	}
+
+	public override dispose(): void {
+		this._mouseWheelToDispose = dispose(this._mouseWheelToDispose);
+		super.dispose();
+	}
+
+	/**
+	 * Get the generated 'scrollable' dom node
+	 */
+	public getDomNode(): HTMLElement {
+		return this._domNode;
+	}
+
+	public getOverviewRulerLayoutInfo(): IOverviewRulerLayoutInfo {
+		return {
+			parent: this._domNode,
+			insertBefore: this._verticalScrollbar.domNode.domNode,
+		};
+	}
+
+	/**
+	 * Delegate a pointer down event to the vertical scrollbar.
+	 * This is to help with clicking somewhere else and having the scrollbar react.
+	 */
+	public delegateVerticalScrollbarPointerDown(browserEvent: PointerEvent): void {
+		this._verticalScrollbar.delegatePointerDown(browserEvent);
+	}
+
+	public getScrollDimensions(): IScrollDimensions {
+		return this._scrollable.getScrollDimensions();
+	}
+
+	public setScrollDimensions(dimensions: INewScrollDimensions): void {
+		this._scrollable.setScrollDimensions(dimensions, false);
+	}
+
+	/**
+	 * Update the class name of the scrollable element.
+	 */
+	public updateClassName(newClassName: string): void {
+		this._options.className = newClassName;
+		// Defaults are different on Macs
+		if (platform.isMacintosh) {
+			this._options.className += ' mac';
+		}
+		this._domNode.className = 'xterm-scrollable-element ' + this._options.className;
+	}
+
+	/**
+	 * Update configuration options for the scrollbar.
+	 */
+	public updateOptions(newOptions: ScrollableElementChangeOptions): void {
+		if (typeof newOptions.handleMouseWheel !== 'undefined') {
+			this._options.handleMouseWheel = newOptions.handleMouseWheel;
+			this._setListeningToMouseWheel(this._options.handleMouseWheel);
+		}
+		if (typeof newOptions.mouseWheelScrollSensitivity !== 'undefined') {
+			this._options.mouseWheelScrollSensitivity = newOptions.mouseWheelScrollSensitivity;
+		}
+		if (typeof newOptions.fastScrollSensitivity !== 'undefined') {
+			this._options.fastScrollSensitivity = newOptions.fastScrollSensitivity;
+		}
+		if (typeof newOptions.scrollPredominantAxis !== 'undefined') {
+			this._options.scrollPredominantAxis = newOptions.scrollPredominantAxis;
+		}
+		if (typeof newOptions.horizontal !== 'undefined') {
+			this._options.horizontal = newOptions.horizontal;
+		}
+		if (typeof newOptions.vertical !== 'undefined') {
+			this._options.vertical = newOptions.vertical;
+		}
+		if (typeof newOptions.horizontalScrollbarSize !== 'undefined') {
+			this._options.horizontalScrollbarSize = newOptions.horizontalScrollbarSize;
+		}
+		if (typeof newOptions.verticalScrollbarSize !== 'undefined') {
+			this._options.verticalScrollbarSize = newOptions.verticalScrollbarSize;
+		}
+		if (typeof newOptions.scrollByPage !== 'undefined') {
+			this._options.scrollByPage = newOptions.scrollByPage;
+		}
+		this._horizontalScrollbar.updateOptions(this._options);
+		this._verticalScrollbar.updateOptions(this._options);
+
+		if (!this._options.lazyRender) {
+			this._render();
+		}
+	}
+
+	public setRevealOnScroll(value: boolean) {
+		this._revealOnScroll = value;
+	}
+
+	public delegateScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent) {
+		this._onMouseWheel(new StandardWheelEvent(browserEvent));
+	}
+
+	// -------------------- mouse wheel scrolling --------------------
+
+	private _setListeningToMouseWheel(shouldListen: boolean): void {
+		const isListening = (this._mouseWheelToDispose.length > 0);
+
+		if (isListening === shouldListen) {
+			// No change
+			return;
+		}
+
+		// Stop listening (if necessary)
+		this._mouseWheelToDispose = dispose(this._mouseWheelToDispose);
+
+		// Start listening (if necessary)
+		if (shouldListen) {
+			const onMouseWheel = (browserEvent: IMouseWheelEvent) => {
+				this._onMouseWheel(new StandardWheelEvent(browserEvent));
+			};
+
+			this._mouseWheelToDispose.push(dom.addDisposableListener(this._listenOnDomNode, dom.EventType.MOUSE_WHEEL, onMouseWheel, { passive: false }));
+		}
+	}
+
+	private _onMouseWheel(e: StandardWheelEvent): void {
+		if (e.browserEvent?.defaultPrevented) {
+			return;
+		}
+
+		const classifier = MouseWheelClassifier.INSTANCE;
+		if (SCROLL_WHEEL_SMOOTH_SCROLL_ENABLED) {
+			classifier.acceptStandardWheelEvent(e);
+		}
+
+		// useful for creating unit tests:
+		// console.log(`${Date.now()}, ${e.deltaY}, ${e.deltaX}`);
+
+		let didScroll = false;
+
+		if (e.deltaY || e.deltaX) {
+			let deltaY = e.deltaY * this._options.mouseWheelScrollSensitivity;
+			let deltaX = e.deltaX * this._options.mouseWheelScrollSensitivity;
+
+			if (this._options.scrollPredominantAxis) {
+				if (this._options.scrollYToX && deltaX + deltaY === 0) {
+					// when configured to map Y to X and we both see
+					// no dominant axis and X and Y are competing with
+					// identical values into opposite directions, we
+					// ignore the delta as we cannot make a decision then
+					deltaX = deltaY = 0;
+				} else if (Math.abs(deltaY) >= Math.abs(deltaX)) {
+					deltaX = 0;
+				} else {
+					deltaY = 0;
+				}
+			}
+
+			if (this._options.flipAxes) {
+				[deltaY, deltaX] = [deltaX, deltaY];
+			}
+
+			// Convert vertical scrolling to horizontal if shift is held, this
+			// is handled at a higher level on Mac
+			const shiftConvert = !platform.isMacintosh && e.browserEvent && e.browserEvent.shiftKey;
+			if ((this._options.scrollYToX || shiftConvert) && !deltaX) {
+				deltaX = deltaY;
+				deltaY = 0;
+			}
+
+			if (e.browserEvent && e.browserEvent.altKey) {
+				// fastScrolling
+				deltaX = deltaX * this._options.fastScrollSensitivity;
+				deltaY = deltaY * this._options.fastScrollSensitivity;
+			}
+
+			const futureScrollPosition = this._scrollable.getFutureScrollPosition();
+
+			let desiredScrollPosition: INewScrollPosition = {};
+			if (deltaY) {
+				const deltaScrollTop = SCROLL_WHEEL_SENSITIVITY * deltaY;
+				// Here we convert values such as -0.3 to -1 or 0.3 to 1, otherwise low speed scrolling will never scroll
+				const desiredScrollTop = futureScrollPosition.scrollTop - (deltaScrollTop < 0 ? Math.floor(deltaScrollTop) : Math.ceil(deltaScrollTop));
+				this._verticalScrollbar.writeScrollPosition(desiredScrollPosition, desiredScrollTop);
+			}
+			if (deltaX) {
+				const deltaScrollLeft = SCROLL_WHEEL_SENSITIVITY * deltaX;
+				// Here we convert values such as -0.3 to -1 or 0.3 to 1, otherwise low speed scrolling will never scroll
+				const desiredScrollLeft = futureScrollPosition.scrollLeft - (deltaScrollLeft < 0 ? Math.floor(deltaScrollLeft) : Math.ceil(deltaScrollLeft));
+				this._horizontalScrollbar.writeScrollPosition(desiredScrollPosition, desiredScrollLeft);
+			}
+
+			// Check that we are scrolling towards a location which is valid
+			desiredScrollPosition = this._scrollable.validateScrollPosition(desiredScrollPosition);
+
+			if (futureScrollPosition.scrollLeft !== desiredScrollPosition.scrollLeft || futureScrollPosition.scrollTop !== desiredScrollPosition.scrollTop) {
+
+				const canPerformSmoothScroll = (
+					SCROLL_WHEEL_SMOOTH_SCROLL_ENABLED
+					&& this._options.mouseWheelSmoothScroll
+					&& classifier.isPhysicalMouseWheel()
+				);
+
+				if (canPerformSmoothScroll) {
+					this._scrollable.setScrollPositionSmooth(desiredScrollPosition);
+				} else {
+					this._scrollable.setScrollPositionNow(desiredScrollPosition);
+				}
+
+				didScroll = true;
+			}
+		}
+
+		let consumeMouseWheel = didScroll;
+		if (!consumeMouseWheel && this._options.alwaysConsumeMouseWheel) {
+			consumeMouseWheel = true;
+		}
+		if (!consumeMouseWheel && this._options.consumeMouseWheelIfScrollbarIsNeeded && (this._verticalScrollbar.isNeeded() || this._horizontalScrollbar.isNeeded())) {
+			consumeMouseWheel = true;
+		}
+
+		if (consumeMouseWheel) {
+			e.preventDefault();
+			e.stopPropagation();
+		}
+	}
+
+	private _onDidScroll(e: ScrollEvent): void {
+		this._shouldRender = this._horizontalScrollbar.onDidScroll(e) || this._shouldRender;
+		this._shouldRender = this._verticalScrollbar.onDidScroll(e) || this._shouldRender;
+
+		if (this._options.useShadows) {
+			this._shouldRender = true;
+		}
+
+		if (this._revealOnScroll) {
+			this._reveal();
+		}
+
+		if (!this._options.lazyRender) {
+			this._render();
+		}
+	}
+
+	/**
+	 * Render / mutate the DOM now.
+	 * Should be used together with the ctor option `lazyRender`.
+	 */
+	public renderNow(): void {
+		if (!this._options.lazyRender) {
+			throw new Error('Please use `lazyRender` together with `renderNow`!');
+		}
+
+		this._render();
+	}
+
+	private _render(): void {
+		if (!this._shouldRender) {
+			return;
+		}
+
+		this._shouldRender = false;
+
+		this._horizontalScrollbar.render();
+		this._verticalScrollbar.render();
+
+		if (this._options.useShadows) {
+			const scrollState = this._scrollable.getCurrentScrollPosition();
+			const enableTop = scrollState.scrollTop > 0;
+			const enableLeft = scrollState.scrollLeft > 0;
+
+			const leftClassName = (enableLeft ? ' left' : '');
+			const topClassName = (enableTop ? ' top' : '');
+			const topLeftClassName = (enableLeft || enableTop ? ' top-left-corner' : '');
+			this._leftShadowDomNode!.setClassName(`shadow${leftClassName}`);
+			this._topShadowDomNode!.setClassName(`shadow${topClassName}`);
+			this._topLeftShadowDomNode!.setClassName(`shadow${topLeftClassName}${topClassName}${leftClassName}`);
+		}
+	}
+
+	// -------------------- fade in / fade out --------------------
+
+	private _onDragStart(): void {
+		this._isDragging = true;
+		this._reveal();
+	}
+
+	private _onDragEnd(): void {
+		this._isDragging = false;
+		this._hide();
+	}
+
+	private _onMouseLeave(e: IMouseEvent): void {
+		this._mouseIsOver = false;
+		this._hide();
+	}
+
+	private _onMouseOver(e: IMouseEvent): void {
+		this._mouseIsOver = true;
+		this._reveal();
+	}
+
+	private _reveal(): void {
+		this._verticalScrollbar.beginReveal();
+		this._horizontalScrollbar.beginReveal();
+		this._scheduleHide();
+	}
+
+	private _hide(): void {
+		if (!this._mouseIsOver && !this._isDragging) {
+			this._verticalScrollbar.beginHide();
+			this._horizontalScrollbar.beginHide();
+		}
+	}
+
+	private _scheduleHide(): void {
+		if (!this._mouseIsOver && !this._isDragging) {
+			this._hideTimeout.cancelAndSet(() => this._hide(), HIDE_TIMEOUT);
+		}
+	}
+}
+
+export class ScrollableElement extends AbstractScrollableElement {
+
+	constructor(element: HTMLElement, options: ScrollableElementCreationOptions) {
+		options = options || {};
+		options.mouseWheelSmoothScroll = false;
+		const scrollable = new Scrollable({
+			forceIntegerValues: true,
+			smoothScrollDuration: 0,
+			scheduleAtNextAnimationFrame: (callback) => dom.scheduleAtNextAnimationFrame(dom.getWindow(element), callback)
+		});
+		super(element, options, scrollable);
+		this._register(scrollable);
+	}
+
+	public setScrollPosition(update: INewScrollPosition): void {
+		this._scrollable.setScrollPositionNow(update);
+	}
+
+	public getScrollPosition(): IScrollPosition {
+		return this._scrollable.getCurrentScrollPosition();
+	}
+}
+
+export class SmoothScrollableElement extends AbstractScrollableElement {
+
+	constructor(element: HTMLElement, options: ScrollableElementCreationOptions, scrollable: Scrollable) {
+		super(element, options, scrollable);
+	}
+
+	public setScrollPosition(update: INewScrollPosition & { reuseAnimation?: boolean }): void {
+		if (update.reuseAnimation) {
+			this._scrollable.setScrollPositionSmooth(update, update.reuseAnimation);
+		} else {
+			this._scrollable.setScrollPositionNow(update);
+		}
+	}
+
+	public getScrollPosition(): IScrollPosition {
+		return this._scrollable.getCurrentScrollPosition();
+	}
+
+}
+
+export class DomScrollableElement extends AbstractScrollableElement {
+
+	private _element: HTMLElement;
+
+	constructor(element: HTMLElement, options: ScrollableElementCreationOptions) {
+		options = options || {};
+		options.mouseWheelSmoothScroll = false;
+		const scrollable = new Scrollable({
+			forceIntegerValues: false, // See https://github.com/microsoft/vscode/issues/139877
+			smoothScrollDuration: 0,
+			scheduleAtNextAnimationFrame: (callback) => dom.scheduleAtNextAnimationFrame(dom.getWindow(element), callback)
+		});
+		super(element, options, scrollable);
+		this._register(scrollable);
+		this._element = element;
+		this._register(this.onScroll((e) => {
+			if (e.scrollTopChanged) {
+				this._element.scrollTop = e.scrollTop;
+			}
+			if (e.scrollLeftChanged) {
+				this._element.scrollLeft = e.scrollLeft;
+			}
+		}));
+		this.scanDomNode();
+	}
+
+	public setScrollPosition(update: INewScrollPosition): void {
+		this._scrollable.setScrollPositionNow(update);
+	}
+
+	public getScrollPosition(): IScrollPosition {
+		return this._scrollable.getCurrentScrollPosition();
+	}
+
+	public scanDomNode(): void {
+		// width, scrollLeft, scrollWidth, height, scrollTop, scrollHeight
+		this.setScrollDimensions({
+			width: this._element.clientWidth,
+			scrollWidth: this._element.scrollWidth,
+			height: this._element.clientHeight,
+			scrollHeight: this._element.scrollHeight
+		});
+		this.setScrollPosition({
+			scrollLeft: this._element.scrollLeft,
+			scrollTop: this._element.scrollTop,
+		});
+	}
+}
+
+function resolveOptions(opts: ScrollableElementCreationOptions): ScrollableElementResolvedOptions {
+	const result: ScrollableElementResolvedOptions = {
+		lazyRender: (typeof opts.lazyRender !== 'undefined' ? opts.lazyRender : false),
+		className: (typeof opts.className !== 'undefined' ? opts.className : ''),
+		useShadows: (typeof opts.useShadows !== 'undefined' ? opts.useShadows : true),
+		handleMouseWheel: (typeof opts.handleMouseWheel !== 'undefined' ? opts.handleMouseWheel : true),
+		flipAxes: (typeof opts.flipAxes !== 'undefined' ? opts.flipAxes : false),
+		consumeMouseWheelIfScrollbarIsNeeded: (typeof opts.consumeMouseWheelIfScrollbarIsNeeded !== 'undefined' ? opts.consumeMouseWheelIfScrollbarIsNeeded : false),
+		alwaysConsumeMouseWheel: (typeof opts.alwaysConsumeMouseWheel !== 'undefined' ? opts.alwaysConsumeMouseWheel : false),
+		scrollYToX: (typeof opts.scrollYToX !== 'undefined' ? opts.scrollYToX : false),
+		mouseWheelScrollSensitivity: (typeof opts.mouseWheelScrollSensitivity !== 'undefined' ? opts.mouseWheelScrollSensitivity : 1),
+		fastScrollSensitivity: (typeof opts.fastScrollSensitivity !== 'undefined' ? opts.fastScrollSensitivity : 5),
+		scrollPredominantAxis: (typeof opts.scrollPredominantAxis !== 'undefined' ? opts.scrollPredominantAxis : true),
+		mouseWheelSmoothScroll: (typeof opts.mouseWheelSmoothScroll !== 'undefined' ? opts.mouseWheelSmoothScroll : true),
+		arrowSize: (typeof opts.arrowSize !== 'undefined' ? opts.arrowSize : 11),
+
+		listenOnDomNode: (typeof opts.listenOnDomNode !== 'undefined' ? opts.listenOnDomNode : null),
+
+		horizontal: (typeof opts.horizontal !== 'undefined' ? opts.horizontal : ScrollbarVisibility.Auto),
+		horizontalScrollbarSize: (typeof opts.horizontalScrollbarSize !== 'undefined' ? opts.horizontalScrollbarSize : 10),
+		horizontalSliderSize: (typeof opts.horizontalSliderSize !== 'undefined' ? opts.horizontalSliderSize : 0),
+		horizontalHasArrows: (typeof opts.horizontalHasArrows !== 'undefined' ? opts.horizontalHasArrows : false),
+
+		vertical: (typeof opts.vertical !== 'undefined' ? opts.vertical : ScrollbarVisibility.Auto),
+		verticalScrollbarSize: (typeof opts.verticalScrollbarSize !== 'undefined' ? opts.verticalScrollbarSize : 10),
+		verticalHasArrows: (typeof opts.verticalHasArrows !== 'undefined' ? opts.verticalHasArrows : false),
+		verticalSliderSize: (typeof opts.verticalSliderSize !== 'undefined' ? opts.verticalSliderSize : 0),
+
+		scrollByPage: (typeof opts.scrollByPage !== 'undefined' ? opts.scrollByPage : false)
+	};
+
+	result.horizontalSliderSize = (typeof opts.horizontalSliderSize !== 'undefined' ? opts.horizontalSliderSize : result.horizontalScrollbarSize);
+	result.verticalSliderSize = (typeof opts.verticalSliderSize !== 'undefined' ? opts.verticalSliderSize : result.verticalScrollbarSize);
+
+	// Defaults are different on Macs
+	if (platform.isMacintosh) {
+		result.className += ' mac';
+	}
+
+	return result;
+}
diff --git a/src/vs/base/browser/ui/scrollbar/scrollableElementOptions.ts b/src/vs/base/browser/ui/scrollbar/scrollableElementOptions.ts
new file mode 100644
index 0000000000..8e75751fff
--- /dev/null
+++ b/src/vs/base/browser/ui/scrollbar/scrollableElementOptions.ts
@@ -0,0 +1,165 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { ScrollbarVisibility } from 'vs/base/common/scrollable';
+
+export interface ScrollableElementCreationOptions {
+	/**
+	 * The scrollable element should not do any DOM mutations until renderNow() is called.
+	 * Defaults to false.
+	 */
+	lazyRender?: boolean;
+	/**
+	 * CSS Class name for the scrollable element.
+	 */
+	className?: string;
+	/**
+	 * Drop subtle horizontal and vertical shadows.
+	 * Defaults to false.
+	 */
+	useShadows?: boolean;
+	/**
+	 * Handle mouse wheel (listen to mouse wheel scrolling).
+	 * Defaults to true
+	 */
+	handleMouseWheel?: boolean;
+	/**
+	 * If mouse wheel is handled, make mouse wheel scrolling smooth.
+	 * Defaults to true.
+	 */
+	mouseWheelSmoothScroll?: boolean;
+	/**
+	 * Flip axes. Treat vertical scrolling like horizontal and vice-versa.
+	 * Defaults to false.
+	 */
+	flipAxes?: boolean;
+	/**
+	 * If enabled, will scroll horizontally when scrolling vertical.
+	 * Defaults to false.
+	 */
+	scrollYToX?: boolean;
+	/**
+	 * Consume all mouse wheel events if a scrollbar is needed (i.e. scrollSize > size).
+	 * Defaults to false.
+	 */
+	consumeMouseWheelIfScrollbarIsNeeded?: boolean;
+	/**
+	 * Always consume mouse wheel events, even when scrolling is no longer possible.
+	 * Defaults to false.
+	 */
+	alwaysConsumeMouseWheel?: boolean;
+	/**
+	 * A multiplier to be used on the `deltaX` and `deltaY` of mouse wheel scroll events.
+	 * Defaults to 1.
+	 */
+	mouseWheelScrollSensitivity?: number;
+	/**
+	 * FastScrolling mulitplier speed when pressing `Alt`
+	 * Defaults to 5.
+	 */
+	fastScrollSensitivity?: number;
+	/**
+	 * Whether the scrollable will only scroll along the predominant axis when scrolling both
+	 * vertically and horizontally at the same time.
+	 * Prevents horizontal drift when scrolling vertically on a trackpad.
+	 * Defaults to true.
+	 */
+	scrollPredominantAxis?: boolean;
+	/**
+	 * Height for vertical arrows (top/bottom) and width for horizontal arrows (left/right).
+	 * Defaults to 11.
+	 */
+	arrowSize?: number;
+	/**
+	 * The dom node events should be bound to.
+	 * If no listenOnDomNode is provided, the dom node passed to the constructor will be used for event listening.
+	 */
+	listenOnDomNode?: HTMLElement;
+	/**
+	 * Control the visibility of the horizontal scrollbar.
+	 * Accepted values: 'auto' (on mouse over), 'visible' (always visible), 'hidden' (never visible)
+	 * Defaults to 'auto'.
+	 */
+	horizontal?: ScrollbarVisibility;
+	/**
+	 * Height (in px) of the horizontal scrollbar.
+	 * Defaults to 10.
+	 */
+	horizontalScrollbarSize?: number;
+	/**
+	 * Height (in px) of the horizontal scrollbar slider.
+	 * Defaults to `horizontalScrollbarSize`
+	 */
+	horizontalSliderSize?: number;
+	/**
+	 * Render arrows (left/right) for the horizontal scrollbar.
+	 * Defaults to false.
+	 */
+	horizontalHasArrows?: boolean;
+	/**
+	 * Control the visibility of the vertical scrollbar.
+	 * Accepted values: 'auto' (on mouse over), 'visible' (always visible), 'hidden' (never visible)
+	 * Defaults to 'auto'.
+	 */
+	vertical?: ScrollbarVisibility;
+	/**
+	 * Width (in px) of the vertical scrollbar.
+	 * Defaults to 10.
+	 */
+	verticalScrollbarSize?: number;
+	/**
+	 * Width (in px) of the vertical scrollbar slider.
+	 * Defaults to `verticalScrollbarSize`
+	 */
+	verticalSliderSize?: number;
+	/**
+	 * Render arrows (top/bottom) for the vertical scrollbar.
+	 * Defaults to false.
+	 */
+	verticalHasArrows?: boolean;
+	/**
+	 * Scroll gutter clicks move by page vs. jump to position.
+	 * Defaults to false.
+	 */
+	scrollByPage?: boolean;
+}
+
+export interface ScrollableElementChangeOptions {
+	handleMouseWheel?: boolean;
+	mouseWheelScrollSensitivity?: number;
+	fastScrollSensitivity?: number;
+	scrollPredominantAxis?: boolean;
+	horizontal?: ScrollbarVisibility;
+	horizontalScrollbarSize?: number;
+	vertical?: ScrollbarVisibility;
+	verticalScrollbarSize?: number;
+	scrollByPage?: boolean;
+}
+
+export interface ScrollableElementResolvedOptions {
+	lazyRender: boolean;
+	className: string;
+	useShadows: boolean;
+	handleMouseWheel: boolean;
+	flipAxes: boolean;
+	scrollYToX: boolean;
+	consumeMouseWheelIfScrollbarIsNeeded: boolean;
+	alwaysConsumeMouseWheel: boolean;
+	mouseWheelScrollSensitivity: number;
+	fastScrollSensitivity: number;
+	scrollPredominantAxis: boolean;
+	mouseWheelSmoothScroll: boolean;
+	arrowSize: number;
+	listenOnDomNode: HTMLElement | null;
+	horizontal: ScrollbarVisibility;
+	horizontalScrollbarSize: number;
+	horizontalSliderSize: number;
+	horizontalHasArrows: boolean;
+	vertical: ScrollbarVisibility;
+	verticalScrollbarSize: number;
+	verticalSliderSize: number;
+	verticalHasArrows: boolean;
+	scrollByPage: boolean;
+}
diff --git a/src/vs/base/browser/ui/scrollbar/scrollbarArrow.ts b/src/vs/base/browser/ui/scrollbar/scrollbarArrow.ts
new file mode 100644
index 0000000000..7bc7e9bb3b
--- /dev/null
+++ b/src/vs/base/browser/ui/scrollbar/scrollbarArrow.ts
@@ -0,0 +1,114 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { GlobalPointerMoveMonitor } from 'vs/base/browser/globalPointerMoveMonitor';
+import { Widget } from 'vs/base/browser/ui/widget';
+import { TimeoutTimer } from 'vs/base/common/async';
+import * as dom from 'vs/base/browser/dom';
+
+/**
+ * The arrow image size.
+ */
+export const ARROW_IMG_SIZE = 11;
+
+export interface ScrollbarArrowOptions {
+	onActivate: () => void;
+	className: string;
+	// icon: ThemeIcon;
+
+	bgWidth: number;
+	bgHeight: number;
+
+	top?: number;
+	left?: number;
+	bottom?: number;
+	right?: number;
+}
+
+export class ScrollbarArrow extends Widget {
+
+	private _onActivate: () => void;
+	public bgDomNode: HTMLElement;
+	public domNode: HTMLElement;
+	private _pointerdownRepeatTimer: dom.WindowIntervalTimer;
+	private _pointerdownScheduleRepeatTimer: TimeoutTimer;
+	private _pointerMoveMonitor: GlobalPointerMoveMonitor;
+
+	constructor(opts: ScrollbarArrowOptions) {
+		super();
+		this._onActivate = opts.onActivate;
+
+		this.bgDomNode = document.createElement('div');
+		this.bgDomNode.className = 'arrow-background';
+		this.bgDomNode.style.position = 'absolute';
+		this.bgDomNode.style.width = opts.bgWidth + 'px';
+		this.bgDomNode.style.height = opts.bgHeight + 'px';
+		if (typeof opts.top !== 'undefined') {
+			this.bgDomNode.style.top = '0px';
+		}
+		if (typeof opts.left !== 'undefined') {
+			this.bgDomNode.style.left = '0px';
+		}
+		if (typeof opts.bottom !== 'undefined') {
+			this.bgDomNode.style.bottom = '0px';
+		}
+		if (typeof opts.right !== 'undefined') {
+			this.bgDomNode.style.right = '0px';
+		}
+
+		this.domNode = document.createElement('div');
+		this.domNode.className = opts.className;
+		// this.domNode.classList.add(...ThemeIcon.asClassNameArray(opts.icon));
+
+		this.domNode.style.position = 'absolute';
+		this.domNode.style.width = ARROW_IMG_SIZE + 'px';
+		this.domNode.style.height = ARROW_IMG_SIZE + 'px';
+		if (typeof opts.top !== 'undefined') {
+			this.domNode.style.top = opts.top + 'px';
+		}
+		if (typeof opts.left !== 'undefined') {
+			this.domNode.style.left = opts.left + 'px';
+		}
+		if (typeof opts.bottom !== 'undefined') {
+			this.domNode.style.bottom = opts.bottom + 'px';
+		}
+		if (typeof opts.right !== 'undefined') {
+			this.domNode.style.right = opts.right + 'px';
+		}
+
+		this._pointerMoveMonitor = this._register(new GlobalPointerMoveMonitor());
+		this._register(dom.addStandardDisposableListener(this.bgDomNode, dom.EventType.POINTER_DOWN, (e) => this._arrowPointerDown(e)));
+		this._register(dom.addStandardDisposableListener(this.domNode, dom.EventType.POINTER_DOWN, (e) => this._arrowPointerDown(e)));
+
+		this._pointerdownRepeatTimer = this._register(new dom.WindowIntervalTimer());
+		this._pointerdownScheduleRepeatTimer = this._register(new TimeoutTimer());
+	}
+
+	private _arrowPointerDown(e: PointerEvent): void {
+		if (!e.target || !(e.target instanceof Element)) {
+			return;
+		}
+		const scheduleRepeater = () => {
+			this._pointerdownRepeatTimer.cancelAndSet(() => this._onActivate(), 1000 / 24, dom.getWindow(e));
+		};
+
+		this._onActivate();
+		this._pointerdownRepeatTimer.cancel();
+		this._pointerdownScheduleRepeatTimer.cancelAndSet(scheduleRepeater, 200);
+
+		this._pointerMoveMonitor.startMonitoring(
+			e.target,
+			e.pointerId,
+			e.buttons,
+			(pointerMoveData) => { /* Intentional empty */ },
+			() => {
+				this._pointerdownRepeatTimer.cancel();
+				this._pointerdownScheduleRepeatTimer.cancel();
+			}
+		);
+
+		e.preventDefault();
+	}
+}
diff --git a/src/vs/base/browser/ui/scrollbar/scrollbarState.ts b/src/vs/base/browser/ui/scrollbar/scrollbarState.ts
new file mode 100644
index 0000000000..9a4f4e2cbb
--- /dev/null
+++ b/src/vs/base/browser/ui/scrollbar/scrollbarState.ts
@@ -0,0 +1,243 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+/**
+ * The minimal size of the slider (such that it can still be clickable) -- it is artificially enlarged.
+ */
+const MINIMUM_SLIDER_SIZE = 20;
+
+export class ScrollbarState {
+
+	/**
+	 * For the vertical scrollbar: the width.
+	 * For the horizontal scrollbar: the height.
+	 */
+	private _scrollbarSize: number;
+
+	/**
+	 * For the vertical scrollbar: the height of the pair horizontal scrollbar.
+	 * For the horizontal scrollbar: the width of the pair vertical scrollbar.
+	 */
+	private _oppositeScrollbarSize: number;
+
+	/**
+	 * For the vertical scrollbar: the height of the scrollbar's arrows.
+	 * For the horizontal scrollbar: the width of the scrollbar's arrows.
+	 */
+	private readonly _arrowSize: number;
+
+	// --- variables
+	/**
+	 * For the vertical scrollbar: the viewport height.
+	 * For the horizontal scrollbar: the viewport width.
+	 */
+	private _visibleSize: number;
+
+	/**
+	 * For the vertical scrollbar: the scroll height.
+	 * For the horizontal scrollbar: the scroll width.
+	 */
+	private _scrollSize: number;
+
+	/**
+	 * For the vertical scrollbar: the scroll top.
+	 * For the horizontal scrollbar: the scroll left.
+	 */
+	private _scrollPosition: number;
+
+	// --- computed variables
+
+	/**
+	 * `visibleSize` - `oppositeScrollbarSize`
+	 */
+	private _computedAvailableSize: number;
+	/**
+	 * (`scrollSize` > 0 && `scrollSize` > `visibleSize`)
+	 */
+	private _computedIsNeeded: boolean;
+
+	private _computedSliderSize: number;
+	private _computedSliderRatio: number;
+	private _computedSliderPosition: number;
+
+	constructor(arrowSize: number, scrollbarSize: number, oppositeScrollbarSize: number, visibleSize: number, scrollSize: number, scrollPosition: number) {
+		this._scrollbarSize = Math.round(scrollbarSize);
+		this._oppositeScrollbarSize = Math.round(oppositeScrollbarSize);
+		this._arrowSize = Math.round(arrowSize);
+
+		this._visibleSize = visibleSize;
+		this._scrollSize = scrollSize;
+		this._scrollPosition = scrollPosition;
+
+		this._computedAvailableSize = 0;
+		this._computedIsNeeded = false;
+		this._computedSliderSize = 0;
+		this._computedSliderRatio = 0;
+		this._computedSliderPosition = 0;
+
+		this._refreshComputedValues();
+	}
+
+	public clone(): ScrollbarState {
+		return new ScrollbarState(this._arrowSize, this._scrollbarSize, this._oppositeScrollbarSize, this._visibleSize, this._scrollSize, this._scrollPosition);
+	}
+
+	public setVisibleSize(visibleSize: number): boolean {
+		const iVisibleSize = Math.round(visibleSize);
+		if (this._visibleSize !== iVisibleSize) {
+			this._visibleSize = iVisibleSize;
+			this._refreshComputedValues();
+			return true;
+		}
+		return false;
+	}
+
+	public setScrollSize(scrollSize: number): boolean {
+		const iScrollSize = Math.round(scrollSize);
+		if (this._scrollSize !== iScrollSize) {
+			this._scrollSize = iScrollSize;
+			this._refreshComputedValues();
+			return true;
+		}
+		return false;
+	}
+
+	public setScrollPosition(scrollPosition: number): boolean {
+		const iScrollPosition = Math.round(scrollPosition);
+		if (this._scrollPosition !== iScrollPosition) {
+			this._scrollPosition = iScrollPosition;
+			this._refreshComputedValues();
+			return true;
+		}
+		return false;
+	}
+
+	public setScrollbarSize(scrollbarSize: number): void {
+		this._scrollbarSize = Math.round(scrollbarSize);
+	}
+
+	public setOppositeScrollbarSize(oppositeScrollbarSize: number): void {
+		this._oppositeScrollbarSize = Math.round(oppositeScrollbarSize);
+	}
+
+	private static _computeValues(oppositeScrollbarSize: number, arrowSize: number, visibleSize: number, scrollSize: number, scrollPosition: number) {
+		const computedAvailableSize = Math.max(0, visibleSize - oppositeScrollbarSize);
+		const computedRepresentableSize = Math.max(0, computedAvailableSize - 2 * arrowSize);
+		const computedIsNeeded = (scrollSize > 0 && scrollSize > visibleSize);
+
+		if (!computedIsNeeded) {
+			// There is no need for a slider
+			return {
+				computedAvailableSize: Math.round(computedAvailableSize),
+				computedIsNeeded: computedIsNeeded,
+				computedSliderSize: Math.round(computedRepresentableSize),
+				computedSliderRatio: 0,
+				computedSliderPosition: 0,
+			};
+		}
+
+		// We must artificially increase the size of the slider if needed, since the slider would be too small to grab with the mouse otherwise
+		const computedSliderSize = Math.round(Math.max(MINIMUM_SLIDER_SIZE, Math.floor(visibleSize * computedRepresentableSize / scrollSize)));
+
+		// The slider can move from 0 to `computedRepresentableSize` - `computedSliderSize`
+		// in the same way `scrollPosition` can move from 0 to `scrollSize` - `visibleSize`.
+		const computedSliderRatio = (computedRepresentableSize - computedSliderSize) / (scrollSize - visibleSize);
+		const computedSliderPosition = (scrollPosition * computedSliderRatio);
+
+		return {
+			computedAvailableSize: Math.round(computedAvailableSize),
+			computedIsNeeded: computedIsNeeded,
+			computedSliderSize: Math.round(computedSliderSize),
+			computedSliderRatio: computedSliderRatio,
+			computedSliderPosition: Math.round(computedSliderPosition),
+		};
+	}
+
+	private _refreshComputedValues(): void {
+		const r = ScrollbarState._computeValues(this._oppositeScrollbarSize, this._arrowSize, this._visibleSize, this._scrollSize, this._scrollPosition);
+		this._computedAvailableSize = r.computedAvailableSize;
+		this._computedIsNeeded = r.computedIsNeeded;
+		this._computedSliderSize = r.computedSliderSize;
+		this._computedSliderRatio = r.computedSliderRatio;
+		this._computedSliderPosition = r.computedSliderPosition;
+	}
+
+	public getArrowSize(): number {
+		return this._arrowSize;
+	}
+
+	public getScrollPosition(): number {
+		return this._scrollPosition;
+	}
+
+	public getRectangleLargeSize(): number {
+		return this._computedAvailableSize;
+	}
+
+	public getRectangleSmallSize(): number {
+		return this._scrollbarSize;
+	}
+
+	public isNeeded(): boolean {
+		return this._computedIsNeeded;
+	}
+
+	public getSliderSize(): number {
+		return this._computedSliderSize;
+	}
+
+	public getSliderPosition(): number {
+		return this._computedSliderPosition;
+	}
+
+	/**
+	 * Compute a desired `scrollPosition` such that `offset` ends up in the center of the slider.
+	 * `offset` is based on the same coordinate system as the `sliderPosition`.
+	 */
+	public getDesiredScrollPositionFromOffset(offset: number): number {
+		if (!this._computedIsNeeded) {
+			// no need for a slider
+			return 0;
+		}
+
+		const desiredSliderPosition = offset - this._arrowSize - this._computedSliderSize / 2;
+		return Math.round(desiredSliderPosition / this._computedSliderRatio);
+	}
+
+	/**
+	 * Compute a desired `scrollPosition` from if offset is before or after the slider position.
+	 * If offset is before slider, treat as a page up (or left).  If after, page down (or right).
+	 * `offset` and `_computedSliderPosition` are based on the same coordinate system.
+	 * `_visibleSize` corresponds to a "page" of lines in the returned coordinate system.
+	 */
+	public getDesiredScrollPositionFromOffsetPaged(offset: number): number {
+		if (!this._computedIsNeeded) {
+			// no need for a slider
+			return 0;
+		}
+
+		const correctedOffset = offset - this._arrowSize;  // compensate if has arrows
+		let desiredScrollPosition = this._scrollPosition;
+		if (correctedOffset < this._computedSliderPosition) {
+			desiredScrollPosition -= this._visibleSize;  // page up/left
+		} else {
+			desiredScrollPosition += this._visibleSize;  // page down/right
+		}
+		return desiredScrollPosition;
+	}
+
+	/**
+	 * Compute a desired `scrollPosition` such that the slider moves by `delta`.
+	 */
+	public getDesiredScrollPositionFromDelta(delta: number): number {
+		if (!this._computedIsNeeded) {
+			// no need for a slider
+			return 0;
+		}
+
+		const desiredSliderPosition = this._computedSliderPosition + delta;
+		return Math.round(desiredSliderPosition / this._computedSliderRatio);
+	}
+}
diff --git a/src/vs/base/browser/ui/scrollbar/scrollbarVisibilityController.ts b/src/vs/base/browser/ui/scrollbar/scrollbarVisibilityController.ts
new file mode 100644
index 0000000000..791c7332ff
--- /dev/null
+++ b/src/vs/base/browser/ui/scrollbar/scrollbarVisibilityController.ts
@@ -0,0 +1,118 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { FastDomNode } from 'vs/base/browser/fastDomNode';
+import { TimeoutTimer } from 'vs/base/common/async';
+import { Disposable } from 'vs/base/common/lifecycle';
+import { ScrollbarVisibility } from 'vs/base/common/scrollable';
+
+export class ScrollbarVisibilityController extends Disposable {
+	private _visibility: ScrollbarVisibility;
+	private _visibleClassName: string;
+	private _invisibleClassName: string;
+	private _domNode: FastDomNode<HTMLElement> | null;
+	private _rawShouldBeVisible: boolean;
+	private _shouldBeVisible: boolean;
+	private _isNeeded: boolean;
+	private _isVisible: boolean;
+	private _revealTimer: TimeoutTimer;
+
+	constructor(visibility: ScrollbarVisibility, visibleClassName: string, invisibleClassName: string) {
+		super();
+		this._visibility = visibility;
+		this._visibleClassName = visibleClassName;
+		this._invisibleClassName = invisibleClassName;
+		this._domNode = null;
+		this._isVisible = false;
+		this._isNeeded = false;
+		this._rawShouldBeVisible = false;
+		this._shouldBeVisible = false;
+		this._revealTimer = this._register(new TimeoutTimer());
+	}
+
+	public setVisibility(visibility: ScrollbarVisibility): void {
+		if (this._visibility !== visibility) {
+			this._visibility = visibility;
+			this._updateShouldBeVisible();
+		}
+	}
+
+	// ----------------- Hide / Reveal
+
+	public setShouldBeVisible(rawShouldBeVisible: boolean): void {
+		this._rawShouldBeVisible = rawShouldBeVisible;
+		this._updateShouldBeVisible();
+	}
+
+	private _applyVisibilitySetting(): boolean {
+		if (this._visibility === ScrollbarVisibility.Hidden) {
+			return false;
+		}
+		if (this._visibility === ScrollbarVisibility.Visible) {
+			return true;
+		}
+		return this._rawShouldBeVisible;
+	}
+
+	private _updateShouldBeVisible(): void {
+		const shouldBeVisible = this._applyVisibilitySetting();
+
+		if (this._shouldBeVisible !== shouldBeVisible) {
+			this._shouldBeVisible = shouldBeVisible;
+			this.ensureVisibility();
+		}
+	}
+
+	public setIsNeeded(isNeeded: boolean): void {
+		if (this._isNeeded !== isNeeded) {
+			this._isNeeded = isNeeded;
+			this.ensureVisibility();
+		}
+	}
+
+	public setDomNode(domNode: FastDomNode<HTMLElement>): void {
+		this._domNode = domNode;
+		this._domNode.setClassName(this._invisibleClassName);
+
+		// Now that the flags & the dom node are in a consistent state, ensure the Hidden/Visible configuration
+		this.setShouldBeVisible(false);
+	}
+
+	public ensureVisibility(): void {
+
+		if (!this._isNeeded) {
+			// Nothing to be rendered
+			this._hide(false);
+			return;
+		}
+
+		if (this._shouldBeVisible) {
+			this._reveal();
+		} else {
+			this._hide(true);
+		}
+	}
+
+	private _reveal(): void {
+		if (this._isVisible) {
+			return;
+		}
+		this._isVisible = true;
+
+		// The CSS animation doesn't play otherwise
+		this._revealTimer.setIfNotSet(() => {
+			this._domNode?.setClassName(this._visibleClassName);
+		}, 0);
+	}
+
+	private _hide(withFadeAway: boolean): void {
+		this._revealTimer.cancel();
+		if (!this._isVisible) {
+			return;
+		}
+		this._isVisible = false;
+		this._domNode?.setClassName(this._invisibleClassName + (withFadeAway ? ' fade' : ''));
+	}
+}
diff --git a/src/vs/base/browser/ui/scrollbar/verticalScrollbar.ts b/src/vs/base/browser/ui/scrollbar/verticalScrollbar.ts
new file mode 100644
index 0000000000..331654af3a
--- /dev/null
+++ b/src/vs/base/browser/ui/scrollbar/verticalScrollbar.ts
@@ -0,0 +1,116 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { AbstractScrollbar, ISimplifiedPointerEvent, ScrollbarHost } from 'vs/base/browser/ui/scrollbar/abstractScrollbar';
+import { ScrollableElementResolvedOptions } from 'vs/base/browser/ui/scrollbar/scrollableElementOptions';
+import { ScrollbarState } from 'vs/base/browser/ui/scrollbar/scrollbarState';
+import { INewScrollPosition, Scrollable, ScrollbarVisibility, ScrollEvent } from 'vs/base/common/scrollable';
+
+
+
+export class VerticalScrollbar extends AbstractScrollbar {
+
+	constructor(scrollable: Scrollable, options: ScrollableElementResolvedOptions, host: ScrollbarHost) {
+		const scrollDimensions = scrollable.getScrollDimensions();
+		const scrollPosition = scrollable.getCurrentScrollPosition();
+		super({
+			lazyRender: options.lazyRender,
+			host: host,
+			scrollbarState: new ScrollbarState(
+				(options.verticalHasArrows ? options.arrowSize : 0),
+				(options.vertical === ScrollbarVisibility.Hidden ? 0 : options.verticalScrollbarSize),
+				// give priority to vertical scroll bar over horizontal and let it scroll all the way to the bottom
+				0,
+				scrollDimensions.height,
+				scrollDimensions.scrollHeight,
+				scrollPosition.scrollTop
+			),
+			visibility: options.vertical,
+			extraScrollbarClassName: 'vertical',
+			scrollable: scrollable,
+			scrollByPage: options.scrollByPage
+		});
+
+		if (options.verticalHasArrows) {
+      throw new Error('horizontalHasArrows is not supported in xterm.js');
+			// const arrowDelta = (options.arrowSize - ARROW_IMG_SIZE) / 2;
+			// const scrollbarDelta = (options.verticalScrollbarSize - ARROW_IMG_SIZE) / 2;
+
+			// this._createArrow({
+			// 	className: 'scra',
+			// 	icon: Codicon.scrollbarButtonUp,
+			// 	top: arrowDelta,
+			// 	left: scrollbarDelta,
+			// 	bottom: undefined,
+			// 	right: undefined,
+			// 	bgWidth: options.verticalScrollbarSize,
+			// 	bgHeight: options.arrowSize,
+			// 	onActivate: () => this._host.onMouseWheel(new StandardWheelEvent(null, 0, 1)),
+			// });
+
+			// this._createArrow({
+			// 	className: 'scra',
+			// 	icon: Codicon.scrollbarButtonDown,
+			// 	top: undefined,
+			// 	left: scrollbarDelta,
+			// 	bottom: arrowDelta,
+			// 	right: undefined,
+			// 	bgWidth: options.verticalScrollbarSize,
+			// 	bgHeight: options.arrowSize,
+			// 	onActivate: () => this._host.onMouseWheel(new StandardWheelEvent(null, 0, -1)),
+			// });
+		}
+
+		this._createSlider(0, Math.floor((options.verticalScrollbarSize - options.verticalSliderSize) / 2), options.verticalSliderSize, undefined);
+	}
+
+	protected _updateSlider(sliderSize: number, sliderPosition: number): void {
+		this.slider.setHeight(sliderSize);
+		this.slider.setTop(sliderPosition);
+	}
+
+	protected _renderDomNode(largeSize: number, smallSize: number): void {
+		this.domNode.setWidth(smallSize);
+		this.domNode.setHeight(largeSize);
+		this.domNode.setRight(0);
+		this.domNode.setTop(0);
+	}
+
+	public onDidScroll(e: ScrollEvent): boolean {
+		this._shouldRender = this._onElementScrollSize(e.scrollHeight) || this._shouldRender;
+		this._shouldRender = this._onElementScrollPosition(e.scrollTop) || this._shouldRender;
+		this._shouldRender = this._onElementSize(e.height) || this._shouldRender;
+		return this._shouldRender;
+	}
+
+	protected _pointerDownRelativePosition(offsetX: number, offsetY: number): number {
+		return offsetY;
+	}
+
+	protected _sliderPointerPosition(e: ISimplifiedPointerEvent): number {
+		return e.pageY;
+	}
+
+	protected _sliderOrthogonalPointerPosition(e: ISimplifiedPointerEvent): number {
+		return e.pageX;
+	}
+
+	protected _updateScrollbarSize(size: number): void {
+		this.slider.setWidth(size);
+	}
+
+	public writeScrollPosition(target: INewScrollPosition, scrollPosition: number): void {
+		target.scrollTop = scrollPosition;
+	}
+
+	public updateOptions(options: ScrollableElementResolvedOptions): void {
+		this.updateScrollbarSize(options.vertical === ScrollbarVisibility.Hidden ? 0 : options.verticalScrollbarSize);
+		// give priority to vertical scroll bar over horizontal and let it scroll all the way to the bottom
+		this._scrollbarState.setOppositeScrollbarSize(0);
+		this._visibilityController.setVisibility(options.vertical);
+		this._scrollByPage = options.scrollByPage;
+	}
+
+}
diff --git a/src/vs/base/browser/ui/widget.ts b/src/vs/base/browser/ui/widget.ts
new file mode 100644
index 0000000000..440a9ace25
--- /dev/null
+++ b/src/vs/base/browser/ui/widget.ts
@@ -0,0 +1,57 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import * as dom from 'vs/base/browser/dom';
+import { IKeyboardEvent, StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
+import { IMouseEvent, StandardMouseEvent } from 'vs/base/browser/mouseEvent';
+import { Gesture } from 'vs/base/browser/touch';
+import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
+
+export abstract class Widget extends Disposable {
+
+	protected onclick(domNode: HTMLElement, listener: (e: IMouseEvent) => void): void {
+		this._register(dom.addDisposableListener(domNode, dom.EventType.CLICK, (e: MouseEvent) => listener(new StandardMouseEvent(dom.getWindow(domNode), e))));
+	}
+
+	protected onmousedown(domNode: HTMLElement, listener: (e: IMouseEvent) => void): void {
+		this._register(dom.addDisposableListener(domNode, dom.EventType.MOUSE_DOWN, (e: MouseEvent) => listener(new StandardMouseEvent(dom.getWindow(domNode), e))));
+	}
+
+	protected onmouseover(domNode: HTMLElement, listener: (e: IMouseEvent) => void): void {
+		this._register(dom.addDisposableListener(domNode, dom.EventType.MOUSE_OVER, (e: MouseEvent) => listener(new StandardMouseEvent(dom.getWindow(domNode), e))));
+	}
+
+	protected onmouseleave(domNode: HTMLElement, listener: (e: IMouseEvent) => void): void {
+		this._register(dom.addDisposableListener(domNode, dom.EventType.MOUSE_LEAVE, (e: MouseEvent) => listener(new StandardMouseEvent(dom.getWindow(domNode), e))));
+	}
+
+	protected onkeydown(domNode: HTMLElement, listener: (e: IKeyboardEvent) => void): void {
+		this._register(dom.addDisposableListener(domNode, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => listener(new StandardKeyboardEvent(e))));
+	}
+
+	protected onkeyup(domNode: HTMLElement, listener: (e: IKeyboardEvent) => void): void {
+		this._register(dom.addDisposableListener(domNode, dom.EventType.KEY_UP, (e: KeyboardEvent) => listener(new StandardKeyboardEvent(e))));
+	}
+
+	protected oninput(domNode: HTMLElement, listener: (e: Event) => void): void {
+		this._register(dom.addDisposableListener(domNode, dom.EventType.INPUT, listener));
+	}
+
+	protected onblur(domNode: HTMLElement, listener: (e: Event) => void): void {
+		this._register(dom.addDisposableListener(domNode, dom.EventType.BLUR, listener));
+	}
+
+	protected onfocus(domNode: HTMLElement, listener: (e: Event) => void): void {
+		this._register(dom.addDisposableListener(domNode, dom.EventType.FOCUS, listener));
+	}
+
+	protected onchange(domNode: HTMLElement, listener: (e: Event) => void): void {
+		this._register(dom.addDisposableListener(domNode, dom.EventType.CHANGE, listener));
+	}
+
+	protected ignoreGesture(domNode: HTMLElement): IDisposable {
+		return Gesture.ignoreTarget(domNode);
+	}
+}
diff --git a/src/vs/base/browser/window.ts b/src/vs/base/browser/window.ts
new file mode 100644
index 0000000000..3a377a85b8
--- /dev/null
+++ b/src/vs/base/browser/window.ts
@@ -0,0 +1,14 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+export type CodeWindow = Window & typeof globalThis & {
+	readonly vscodeWindowId: number;
+};
+
+export function ensureCodeWindow(targetWindow: Window, fallbackWindowId: number): asserts targetWindow is CodeWindow {
+}
+
+// eslint-disable-next-line no-restricted-globals
+export const mainWindow = (typeof window === 'object' ? window : globalThis) as CodeWindow;
diff --git a/src/vs/base/common/arrays.ts b/src/vs/base/common/arrays.ts
new file mode 100644
index 0000000000..52e542c0eb
--- /dev/null
+++ b/src/vs/base/common/arrays.ts
@@ -0,0 +1,887 @@
+/*---------------------------------------------------------------------------------------------
+ *  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 { CancellationError } from 'vs/base/common/errors';
+import { ISplice } from 'vs/base/common/sequence';
+import { findFirstIdxMonotonousOrArrLen } from './arraysFind';
+
+/**
+ * Returns the last element of an array.
+ * @param array The array.
+ * @param n Which element from the end (default is zero).
+ */
+export function tail<T>(array: ArrayLike<T>, n: number = 0): T | undefined {
+	return array[array.length - (1 + n)];
+}
+
+export function tail2<T>(arr: T[]): [T[], T] {
+	if (arr.length === 0) {
+		throw new Error('Invalid tail call');
+	}
+
+	return [arr.slice(0, arr.length - 1), arr[arr.length - 1]];
+}
+
+export function equals<T>(one: ReadonlyArray<T> | undefined, other: ReadonlyArray<T> | undefined, itemEquals: (a: T, b: T) => boolean = (a, b) => a === b): boolean {
+	if (one === other) {
+		return true;
+	}
+
+	if (!one || !other) {
+		return false;
+	}
+
+	if (one.length !== other.length) {
+		return false;
+	}
+
+	for (let i = 0, len = one.length; i < len; i++) {
+		if (!itemEquals(one[i], other[i])) {
+			return false;
+		}
+	}
+
+	return true;
+}
+
+/**
+ * Remove the element at `index` by replacing it with the last element. This is faster than `splice`
+ * but changes the order of the array
+ */
+export function removeFastWithoutKeepingOrder<T>(array: T[], index: number) {
+	const last = array.length - 1;
+	if (index < last) {
+		array[index] = array[last];
+	}
+	array.pop();
+}
+
+/**
+ * Performs a binary search algorithm over a sorted array.
+ *
+ * @param array The array being searched.
+ * @param key The value we search for.
+ * @param comparator A function that takes two array elements and returns zero
+ *   if they are equal, a negative number if the first element precedes the
+ *   second one in the sorting order, or a positive number if the second element
+ *   precedes the first one.
+ * @return See {@link binarySearch2}
+ */
+export function binarySearch<T>(array: ReadonlyArray<T>, key: T, comparator: (op1: T, op2: T) => number): number {
+	return binarySearch2(array.length, i => comparator(array[i], key));
+}
+
+/**
+ * Performs a binary search algorithm over a sorted collection. Useful for cases
+ * when we need to perform a binary search over something that isn't actually an
+ * array, and converting data to an array would defeat the use of binary search
+ * in the first place.
+ *
+ * @param length The collection length.
+ * @param compareToKey A function that takes an index of an element in the
+ *   collection and returns zero if the value at this index is equal to the
+ *   search key, a negative number if the value precedes the search key in the
+ *   sorting order, or a positive number if the search key precedes the value.
+ * @return A non-negative index of an element, if found. If not found, the
+ *   result is -(n+1) (or ~n, using bitwise notation), where n is the index
+ *   where the key should be inserted to maintain the sorting order.
+ */
+export function binarySearch2(length: number, compareToKey: (index: number) => number): number {
+	let low = 0,
+		high = length - 1;
+
+	while (low <= high) {
+		const mid = ((low + high) / 2) | 0;
+		const comp = compareToKey(mid);
+		if (comp < 0) {
+			low = mid + 1;
+		} else if (comp > 0) {
+			high = mid - 1;
+		} else {
+			return mid;
+		}
+	}
+	return -(low + 1);
+}
+
+type Compare<T> = (a: T, b: T) => number;
+
+
+export function quickSelect<T>(nth: number, data: T[], compare: Compare<T>): T {
+
+	nth = nth | 0;
+
+	if (nth >= data.length) {
+		throw new TypeError('invalid index');
+	}
+
+	const pivotValue = data[Math.floor(data.length * Math.random())];
+	const lower: T[] = [];
+	const higher: T[] = [];
+	const pivots: T[] = [];
+
+	for (const value of data) {
+		const val = compare(value, pivotValue);
+		if (val < 0) {
+			lower.push(value);
+		} else if (val > 0) {
+			higher.push(value);
+		} else {
+			pivots.push(value);
+		}
+	}
+
+	if (nth < lower.length) {
+		return quickSelect(nth, lower, compare);
+	} else if (nth < lower.length + pivots.length) {
+		return pivots[0];
+	} else {
+		return quickSelect(nth - (lower.length + pivots.length), higher, compare);
+	}
+}
+
+export function groupBy<T>(data: ReadonlyArray<T>, compare: (a: T, b: T) => number): T[][] {
+	const result: T[][] = [];
+	let currentGroup: T[] | undefined = undefined;
+	for (const element of data.slice(0).sort(compare)) {
+		if (!currentGroup || compare(currentGroup[0], element) !== 0) {
+			currentGroup = [element];
+			result.push(currentGroup);
+		} else {
+			currentGroup.push(element);
+		}
+	}
+	return result;
+}
+
+/**
+ * Splits the given items into a list of (non-empty) groups.
+ * `shouldBeGrouped` is used to decide if two consecutive items should be in the same group.
+ * The order of the items is preserved.
+ */
+export function* groupAdjacentBy<T>(items: Iterable<T>, shouldBeGrouped: (item1: T, item2: T) => boolean): Iterable<T[]> {
+	let currentGroup: T[] | undefined;
+	let last: T | undefined;
+	for (const item of items) {
+		if (last !== undefined && shouldBeGrouped(last, item)) {
+			currentGroup!.push(item);
+		} else {
+			if (currentGroup) {
+				yield currentGroup;
+			}
+			currentGroup = [item];
+		}
+		last = item;
+	}
+	if (currentGroup) {
+		yield currentGroup;
+	}
+}
+
+export function forEachAdjacent<T>(arr: T[], f: (item1: T | undefined, item2: T | undefined) => void): void {
+	for (let i = 0; i <= arr.length; i++) {
+		f(i === 0 ? undefined : arr[i - 1], i === arr.length ? undefined : arr[i]);
+	}
+}
+
+export function forEachWithNeighbors<T>(arr: T[], f: (before: T | undefined, element: T, after: T | undefined) => void): void {
+	for (let i = 0; i < arr.length; i++) {
+		f(i === 0 ? undefined : arr[i - 1], arr[i], i + 1 === arr.length ? undefined : arr[i + 1]);
+	}
+}
+
+interface IMutableSplice<T> extends ISplice<T> {
+	readonly toInsert: T[];
+	deleteCount: number;
+}
+
+/**
+ * Diffs two *sorted* arrays and computes the splices which apply the diff.
+ */
+export function sortedDiff<T>(before: ReadonlyArray<T>, after: ReadonlyArray<T>, compare: (a: T, b: T) => number): ISplice<T>[] {
+	const result: IMutableSplice<T>[] = [];
+
+	function pushSplice(start: number, deleteCount: number, toInsert: T[]): void {
+		if (deleteCount === 0 && toInsert.length === 0) {
+			return;
+		}
+
+		const latest = result[result.length - 1];
+
+		if (latest && latest.start + latest.deleteCount === start) {
+			latest.deleteCount += deleteCount;
+			latest.toInsert.push(...toInsert);
+		} else {
+			result.push({ start, deleteCount, toInsert });
+		}
+	}
+
+	let beforeIdx = 0;
+	let afterIdx = 0;
+
+	while (true) {
+		if (beforeIdx === before.length) {
+			pushSplice(beforeIdx, 0, after.slice(afterIdx));
+			break;
+		}
+		if (afterIdx === after.length) {
+			pushSplice(beforeIdx, before.length - beforeIdx, []);
+			break;
+		}
+
+		const beforeElement = before[beforeIdx];
+		const afterElement = after[afterIdx];
+		const n = compare(beforeElement, afterElement);
+		if (n === 0) {
+			// equal
+			beforeIdx += 1;
+			afterIdx += 1;
+		} else if (n < 0) {
+			// beforeElement is smaller -> before element removed
+			pushSplice(beforeIdx, 1, []);
+			beforeIdx += 1;
+		} else if (n > 0) {
+			// beforeElement is greater -> after element added
+			pushSplice(beforeIdx, 0, [afterElement]);
+			afterIdx += 1;
+		}
+	}
+
+	return result;
+}
+
+/**
+ * Takes two *sorted* arrays and computes their delta (removed, added elements).
+ * Finishes in `Math.min(before.length, after.length)` steps.
+ */
+export function delta<T>(before: ReadonlyArray<T>, after: ReadonlyArray<T>, compare: (a: T, b: T) => number): { removed: T[]; added: T[] } {
+	const splices = sortedDiff(before, after, compare);
+	const removed: T[] = [];
+	const added: T[] = [];
+
+	for (const splice of splices) {
+		removed.push(...before.slice(splice.start, splice.start + splice.deleteCount));
+		added.push(...splice.toInsert);
+	}
+
+	return { removed, added };
+}
+
+/**
+ * Returns the top N elements from the array.
+ *
+ * Faster than sorting the entire array when the array is a lot larger than N.
+ *
+ * @param array The unsorted array.
+ * @param compare A sort function for the elements.
+ * @param n The number of elements to return.
+ * @return The first n elements from array when sorted with compare.
+ */
+export function top<T>(array: ReadonlyArray<T>, compare: (a: T, b: T) => number, n: number): T[] {
+	if (n === 0) {
+		return [];
+	}
+	const result = array.slice(0, n).sort(compare);
+	topStep(array, compare, result, n, array.length);
+	return result;
+}
+
+/**
+ * Asynchronous variant of `top()` allowing for splitting up work in batches between which the event loop can run.
+ *
+ * Returns the top N elements from the array.
+ *
+ * Faster than sorting the entire array when the array is a lot larger than N.
+ *
+ * @param array The unsorted array.
+ * @param compare A sort function for the elements.
+ * @param n The number of elements to return.
+ * @param batch The number of elements to examine before yielding to the event loop.
+ * @return The first n elements from array when sorted with compare.
+ */
+export function topAsync<T>(array: T[], compare: (a: T, b: T) => number, n: number, batch: number, token?: CancellationToken): Promise<T[]> {
+	if (n === 0) {
+		return Promise.resolve([]);
+	}
+
+	return new Promise((resolve, reject) => {
+		(async () => {
+			const o = array.length;
+			const result = array.slice(0, n).sort(compare);
+			for (let i = n, m = Math.min(n + batch, o); i < o; i = m, m = Math.min(m + batch, o)) {
+				if (i > n) {
+					await new Promise(resolve => setTimeout(resolve)); // any other delay function would starve I/O
+				}
+				if (token && token.isCancellationRequested) {
+					throw new CancellationError();
+				}
+				topStep(array, compare, result, i, m);
+			}
+			return result;
+		})()
+			.then(resolve, reject);
+	});
+}
+
+function topStep<T>(array: ReadonlyArray<T>, compare: (a: T, b: T) => number, result: T[], i: number, m: number): void {
+	for (const n = result.length; i < m; i++) {
+		const element = array[i];
+		if (compare(element, result[n - 1]) < 0) {
+			result.pop();
+			const j = findFirstIdxMonotonousOrArrLen(result, e => compare(element, e) < 0);
+			result.splice(j, 0, element);
+		}
+	}
+}
+
+/**
+ * @returns New array with all falsy values removed. The original array IS NOT modified.
+ */
+export function coalesce<T>(array: ReadonlyArray<T | undefined | null>): T[] {
+	return array.filter((e): e is T => !!e);
+}
+
+/**
+ * Remove all falsy values from `array`. The original array IS modified.
+ */
+export function coalesceInPlace<T>(array: Array<T | undefined | null>): asserts array is Array<T> {
+	let to = 0;
+	for (let i = 0; i < array.length; i++) {
+		if (!!array[i]) {
+			array[to] = array[i];
+			to += 1;
+		}
+	}
+	array.length = to;
+}
+
+/**
+ * @deprecated Use `Array.copyWithin` instead
+ */
+export function move(array: any[], from: number, to: number): void {
+	array.splice(to, 0, array.splice(from, 1)[0]);
+}
+
+/**
+ * @returns false if the provided object is an array and not empty.
+ */
+export function isFalsyOrEmpty(obj: any): boolean {
+	return !Array.isArray(obj) || obj.length === 0;
+}
+
+/**
+ * @returns True if the provided object is an array and has at least one element.
+ */
+export function isNonEmptyArray<T>(obj: T[] | undefined | null): obj is T[];
+export function isNonEmptyArray<T>(obj: readonly T[] | undefined | null): obj is readonly T[];
+export function isNonEmptyArray<T>(obj: T[] | readonly T[] | undefined | null): obj is T[] | readonly T[] {
+	return Array.isArray(obj) && obj.length > 0;
+}
+
+/**
+ * Removes duplicates from the given array. The optional keyFn allows to specify
+ * how elements are checked for equality by returning an alternate value for each.
+ */
+export function distinct<T>(array: ReadonlyArray<T>, keyFn: (value: T) => any = value => value): T[] {
+	const seen = new Set<any>();
+
+	return array.filter(element => {
+		const key = keyFn!(element);
+		if (seen.has(key)) {
+			return false;
+		}
+		seen.add(key);
+		return true;
+	});
+}
+
+export function uniqueFilter<T, R>(keyFn: (t: T) => R): (t: T) => boolean {
+	const seen = new Set<R>();
+
+	return element => {
+		const key = keyFn(element);
+
+		if (seen.has(key)) {
+			return false;
+		}
+
+		seen.add(key);
+		return true;
+	};
+}
+
+export function firstOrDefault<T, NotFound = T>(array: ReadonlyArray<T>, notFoundValue: NotFound): T | NotFound;
+export function firstOrDefault<T>(array: ReadonlyArray<T>): T | undefined;
+export function firstOrDefault<T, NotFound = T>(array: ReadonlyArray<T>, notFoundValue?: NotFound): T | NotFound | undefined {
+	return array.length > 0 ? array[0] : notFoundValue;
+}
+
+export function lastOrDefault<T, NotFound = T>(array: ReadonlyArray<T>, notFoundValue: NotFound): T | NotFound;
+export function lastOrDefault<T>(array: ReadonlyArray<T>): T | undefined;
+export function lastOrDefault<T, NotFound = T>(array: ReadonlyArray<T>, notFoundValue?: NotFound): T | NotFound | undefined {
+	return array.length > 0 ? array[array.length - 1] : notFoundValue;
+}
+
+export function commonPrefixLength<T>(one: ReadonlyArray<T>, other: ReadonlyArray<T>, equals: (a: T, b: T) => boolean = (a, b) => a === b): number {
+	let result = 0;
+
+	for (let i = 0, len = Math.min(one.length, other.length); i < len && equals(one[i], other[i]); i++) {
+		result++;
+	}
+
+	return result;
+}
+
+export function range(to: number): number[];
+export function range(from: number, to: number): number[];
+export function range(arg: number, to?: number): number[] {
+	let from = typeof to === 'number' ? arg : 0;
+
+	if (typeof to === 'number') {
+		from = arg;
+	} else {
+		from = 0;
+		to = arg;
+	}
+
+	const result: number[] = [];
+
+	if (from <= to) {
+		for (let i = from; i < to; i++) {
+			result.push(i);
+		}
+	} else {
+		for (let i = from; i > to; i--) {
+			result.push(i);
+		}
+	}
+
+	return result;
+}
+
+export function index<T>(array: ReadonlyArray<T>, indexer: (t: T) => string): { [key: string]: T };
+export function index<T, R>(array: ReadonlyArray<T>, indexer: (t: T) => string, mapper: (t: T) => R): { [key: string]: R };
+export function index<T, R>(array: ReadonlyArray<T>, indexer: (t: T) => string, mapper?: (t: T) => R): { [key: string]: R } {
+	return array.reduce((r, t) => {
+		r[indexer(t)] = mapper ? mapper(t) : t;
+		return r;
+	}, Object.create(null));
+}
+
+/**
+ * Inserts an element into an array. Returns a function which, when
+ * called, will remove that element from the array.
+ *
+ * @deprecated In almost all cases, use a `Set<T>` instead.
+ */
+export function insert<T>(array: T[], element: T): () => void {
+	array.push(element);
+
+	return () => remove(array, element);
+}
+
+/**
+ * Removes an element from an array if it can be found.
+ *
+ * @deprecated In almost all cases, use a `Set<T>` instead.
+ */
+export function remove<T>(array: T[], element: T): T | undefined {
+	const index = array.indexOf(element);
+	if (index > -1) {
+		array.splice(index, 1);
+
+		return element;
+	}
+
+	return undefined;
+}
+
+/**
+ * Insert `insertArr` inside `target` at `insertIndex`.
+ * Please don't touch unless you understand https://jsperf.com/inserting-an-array-within-an-array
+ */
+export function arrayInsert<T>(target: T[], insertIndex: number, insertArr: T[]): T[] {
+	const before = target.slice(0, insertIndex);
+	const after = target.slice(insertIndex);
+	return before.concat(insertArr, after);
+}
+
+/**
+ * Uses Fisher-Yates shuffle to shuffle the given array
+ */
+export function shuffle<T>(array: T[], _seed?: number): void {
+	let rand: () => number;
+
+	if (typeof _seed === 'number') {
+		let seed = _seed;
+		// Seeded random number generator in JS. Modified from:
+		// https://stackoverflow.com/questions/521295/seeding-the-random-number-generator-in-javascript
+		rand = () => {
+			const x = Math.sin(seed++) * 179426549; // throw away most significant digits and reduce any potential bias
+			return x - Math.floor(x);
+		};
+	} else {
+		rand = Math.random;
+	}
+
+	for (let i = array.length - 1; i > 0; i -= 1) {
+		const j = Math.floor(rand() * (i + 1));
+		const temp = array[i];
+		array[i] = array[j];
+		array[j] = temp;
+	}
+}
+
+/**
+ * Pushes an element to the start of the array, if found.
+ */
+export function pushToStart<T>(arr: T[], value: T): void {
+	const index = arr.indexOf(value);
+
+	if (index > -1) {
+		arr.splice(index, 1);
+		arr.unshift(value);
+	}
+}
+
+/**
+ * Pushes an element to the end of the array, if found.
+ */
+export function pushToEnd<T>(arr: T[], value: T): void {
+	const index = arr.indexOf(value);
+
+	if (index > -1) {
+		arr.splice(index, 1);
+		arr.push(value);
+	}
+}
+
+export function pushMany<T>(arr: T[], items: ReadonlyArray<T>): void {
+	for (const item of items) {
+		arr.push(item);
+	}
+}
+
+export function mapArrayOrNot<T, U>(items: T | T[], fn: (_: T) => U): U | U[] {
+	return Array.isArray(items) ?
+		items.map(fn) :
+		fn(items);
+}
+
+export function asArray<T>(x: T | T[]): T[];
+export function asArray<T>(x: T | readonly T[]): readonly T[];
+export function asArray<T>(x: T | T[]): T[] {
+	return Array.isArray(x) ? x : [x];
+}
+
+export function getRandomElement<T>(arr: T[]): T | undefined {
+	return arr[Math.floor(Math.random() * arr.length)];
+}
+
+/**
+ * Insert the new items in the array.
+ * @param array The original array.
+ * @param start The zero-based location in the array from which to start inserting elements.
+ * @param newItems The items to be inserted
+ */
+export function insertInto<T>(array: T[], start: number, newItems: T[]): void {
+	const startIdx = getActualStartIndex(array, start);
+	const originalLength = array.length;
+	const newItemsLength = newItems.length;
+	array.length = originalLength + newItemsLength;
+	// Move the items after the start index, start from the end so that we don't overwrite any value.
+	for (let i = originalLength - 1; i >= startIdx; i--) {
+		array[i + newItemsLength] = array[i];
+	}
+
+	for (let i = 0; i < newItemsLength; i++) {
+		array[i + startIdx] = newItems[i];
+	}
+}
+
+/**
+ * Removes elements from an array and inserts new elements in their place, returning the deleted elements. Alternative to the native Array.splice method, it
+ * can only support limited number of items due to the maximum call stack size limit.
+ * @param array The original array.
+ * @param start The zero-based location in the array from which to start removing elements.
+ * @param deleteCount The number of elements to remove.
+ * @returns An array containing the elements that were deleted.
+ */
+export function splice<T>(array: T[], start: number, deleteCount: number, newItems: T[]): T[] {
+	const index = getActualStartIndex(array, start);
+	let result = array.splice(index, deleteCount);
+	if (result === undefined) {
+		// see https://bugs.webkit.org/show_bug.cgi?id=261140
+		result = [];
+	}
+	insertInto(array, index, newItems);
+	return result;
+}
+
+/**
+ * Determine the actual start index (same logic as the native splice() or slice())
+ * If greater than the length of the array, start will be set to the length of the array. In this case, no element will be deleted but the method will behave as an adding function, adding as many element as item[n*] provided.
+ * If negative, it will begin that many elements from the end of the array. (In this case, the origin -1, meaning -n is the index of the nth last element, and is therefore equivalent to the index of array.length - n.) If array.length + start is less than 0, it will begin from index 0.
+ * @param array The target array.
+ * @param start The operation index.
+ */
+function getActualStartIndex<T>(array: T[], start: number): number {
+	return start < 0 ? Math.max(start + array.length, 0) : Math.min(start, array.length);
+}
+
+/**
+ * When comparing two values,
+ * a negative number indicates that the first value is less than the second,
+ * a positive number indicates that the first value is greater than the second,
+ * and zero indicates that neither is the case.
+*/
+export type CompareResult = number;
+
+export namespace CompareResult {
+	export function isLessThan(result: CompareResult): boolean {
+		return result < 0;
+	}
+
+	export function isLessThanOrEqual(result: CompareResult): boolean {
+		return result <= 0;
+	}
+
+	export function isGreaterThan(result: CompareResult): boolean {
+		return result > 0;
+	}
+
+	export function isNeitherLessOrGreaterThan(result: CompareResult): boolean {
+		return result === 0;
+	}
+
+	export const greaterThan = 1;
+	export const lessThan = -1;
+	export const neitherLessOrGreaterThan = 0;
+}
+
+/**
+ * A comparator `c` defines a total order `<=` on `T` as following:
+ * `c(a, b) <= 0` iff `a` <= `b`.
+ * We also have `c(a, b) == 0` iff `c(b, a) == 0`.
+*/
+export type Comparator<T> = (a: T, b: T) => CompareResult;
+
+export function compareBy<TItem, TCompareBy>(selector: (item: TItem) => TCompareBy, comparator: Comparator<TCompareBy>): Comparator<TItem> {
+	return (a, b) => comparator(selector(a), selector(b));
+}
+
+export function tieBreakComparators<TItem>(...comparators: Comparator<TItem>[]): Comparator<TItem> {
+	return (item1, item2) => {
+		for (const comparator of comparators) {
+			const result = comparator(item1, item2);
+			if (!CompareResult.isNeitherLessOrGreaterThan(result)) {
+				return result;
+			}
+		}
+		return CompareResult.neitherLessOrGreaterThan;
+	};
+}
+
+/**
+ * The natural order on numbers.
+*/
+export const numberComparator: Comparator<number> = (a, b) => a - b;
+
+export const booleanComparator: Comparator<boolean> = (a, b) => numberComparator(a ? 1 : 0, b ? 1 : 0);
+
+export function reverseOrder<TItem>(comparator: Comparator<TItem>): Comparator<TItem> {
+	return (a, b) => -comparator(a, b);
+}
+
+export class ArrayQueue<T> {
+	private firstIdx = 0;
+	private lastIdx = this.items.length - 1;
+
+	/**
+	 * Constructs a queue that is backed by the given array. Runtime is O(1).
+	*/
+	constructor(private readonly items: readonly T[]) { }
+
+	get length(): number {
+		return this.lastIdx - this.firstIdx + 1;
+	}
+
+	/**
+	 * Consumes elements from the beginning of the queue as long as the predicate returns true.
+	 * If no elements were consumed, `null` is returned. Has a runtime of O(result.length).
+	*/
+	takeWhile(predicate: (value: T) => boolean): T[] | null {
+		// P(k) := k <= this.lastIdx && predicate(this.items[k])
+		// Find s := min { k | k >= this.firstIdx && !P(k) } and return this.data[this.firstIdx...s)
+
+		let startIdx = this.firstIdx;
+		while (startIdx < this.items.length && predicate(this.items[startIdx])) {
+			startIdx++;
+		}
+		const result = startIdx === this.firstIdx ? null : this.items.slice(this.firstIdx, startIdx);
+		this.firstIdx = startIdx;
+		return result;
+	}
+
+	/**
+	 * Consumes elements from the end of the queue as long as the predicate returns true.
+	 * If no elements were consumed, `null` is returned.
+	 * The result has the same order as the underlying array!
+	*/
+	takeFromEndWhile(predicate: (value: T) => boolean): T[] | null {
+		// P(k) := this.firstIdx >= k && predicate(this.items[k])
+		// Find s := max { k | k <= this.lastIdx && !P(k) } and return this.data(s...this.lastIdx]
+
+		let endIdx = this.lastIdx;
+		while (endIdx >= 0 && predicate(this.items[endIdx])) {
+			endIdx--;
+		}
+		const result = endIdx === this.lastIdx ? null : this.items.slice(endIdx + 1, this.lastIdx + 1);
+		this.lastIdx = endIdx;
+		return result;
+	}
+
+	peek(): T | undefined {
+		if (this.length === 0) {
+			return undefined;
+		}
+		return this.items[this.firstIdx];
+	}
+
+	peekLast(): T | undefined {
+		if (this.length === 0) {
+			return undefined;
+		}
+		return this.items[this.lastIdx];
+	}
+
+	dequeue(): T | undefined {
+		const result = this.items[this.firstIdx];
+		this.firstIdx++;
+		return result;
+	}
+
+	removeLast(): T | undefined {
+		const result = this.items[this.lastIdx];
+		this.lastIdx--;
+		return result;
+	}
+
+	takeCount(count: number): T[] {
+		const result = this.items.slice(this.firstIdx, this.firstIdx + count);
+		this.firstIdx += count;
+		return result;
+	}
+}
+
+/**
+ * This class is faster than an iterator and array for lazy computed data.
+*/
+export class CallbackIterable<T> {
+	public static readonly empty = new CallbackIterable<never>(_callback => { });
+
+	constructor(
+		/**
+		 * Calls the callback for every item.
+		 * Stops when the callback returns false.
+		*/
+		public readonly iterate: (callback: (item: T) => boolean) => void
+	) {
+	}
+
+	forEach(handler: (item: T) => void) {
+		this.iterate(item => { handler(item); return true; });
+	}
+
+	toArray(): T[] {
+		const result: T[] = [];
+		this.iterate(item => { result.push(item); return true; });
+		return result;
+	}
+
+	filter(predicate: (item: T) => boolean): CallbackIterable<T> {
+		return new CallbackIterable(cb => this.iterate(item => predicate(item) ? cb(item) : true));
+	}
+
+	map<TResult>(mapFn: (item: T) => TResult): CallbackIterable<TResult> {
+		return new CallbackIterable<TResult>(cb => this.iterate(item => cb(mapFn(item))));
+	}
+
+	some(predicate: (item: T) => boolean): boolean {
+		let result = false;
+		this.iterate(item => { result = predicate(item); return !result; });
+		return result;
+	}
+
+	findFirst(predicate: (item: T) => boolean): T | undefined {
+		let result: T | undefined;
+		this.iterate(item => {
+			if (predicate(item)) {
+				result = item;
+				return false;
+			}
+			return true;
+		});
+		return result;
+	}
+
+	findLast(predicate: (item: T) => boolean): T | undefined {
+		let result: T | undefined;
+		this.iterate(item => {
+			if (predicate(item)) {
+				result = item;
+			}
+			return true;
+		});
+		return result;
+	}
+
+	findLastMaxBy(comparator: Comparator<T>): T | undefined {
+		let result: T | undefined;
+		let first = true;
+		this.iterate(item => {
+			if (first || CompareResult.isGreaterThan(comparator(item, result!))) {
+				first = false;
+				result = item;
+			}
+			return true;
+		});
+		return result;
+	}
+}
+
+/**
+ * Represents a re-arrangement of items in an array.
+ */
+export class Permutation {
+	constructor(private readonly _indexMap: readonly number[]) { }
+
+	/**
+	 * Returns a permutation that sorts the given array according to the given compare function.
+	 */
+	public static createSortPermutation<T>(arr: readonly T[], compareFn: (a: T, b: T) => number): Permutation {
+		const sortIndices = Array.from(arr.keys()).sort((index1, index2) => compareFn(arr[index1], arr[index2]));
+		return new Permutation(sortIndices);
+	}
+
+	/**
+	 * Returns a new array with the elements of the given array re-arranged according to this permutation.
+	 */
+	apply<T>(arr: readonly T[]): T[] {
+		return arr.map((_, index) => arr[this._indexMap[index]]);
+	}
+
+	/**
+	 * Returns a new permutation that undoes the re-arrangement of this permutation.
+	*/
+	inverse(): Permutation {
+		const inverseIndexMap = this._indexMap.slice();
+		for (let i = 0; i < this._indexMap.length; i++) {
+			inverseIndexMap[this._indexMap[i]] = i;
+		}
+		return new Permutation(inverseIndexMap);
+	}
+}
diff --git a/src/vs/base/common/arraysFind.ts b/src/vs/base/common/arraysFind.ts
new file mode 100644
index 0000000000..1dd102e9e7
--- /dev/null
+++ b/src/vs/base/common/arraysFind.ts
@@ -0,0 +1,202 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { Comparator } from './arrays';
+
+export function findLast<T>(array: readonly T[], predicate: (item: T) => boolean): T | undefined {
+	const idx = findLastIdx(array, predicate);
+	if (idx === -1) {
+		return undefined;
+	}
+	return array[idx];
+}
+
+export function findLastIdx<T>(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)) {
+			return i;
+		}
+	}
+
+	return -1;
+}
+
+/**
+ * Finds the last item where predicate is true using binary search.
+ * `predicate` must be monotonous, i.e. `arr.map(predicate)` must be like `[true, ..., true, false, ..., false]`!
+ *
+ * @returns `undefined` if no item matches, otherwise the last item that matches the predicate.
+ */
+export function findLastMonotonous<T>(array: readonly T[], predicate: (item: T) => boolean): T | undefined {
+	const idx = findLastIdxMonotonous(array, predicate);
+	return idx === -1 ? undefined : array[idx];
+}
+
+/**
+ * Finds the last item where predicate is true using binary search.
+ * `predicate` must be monotonous, i.e. `arr.map(predicate)` must be like `[true, ..., true, false, ..., false]`!
+ *
+ * @returns `startIdx - 1` if predicate is false for all items, otherwise the index of the last item that matches the predicate.
+ */
+export function findLastIdxMonotonous<T>(array: readonly T[], predicate: (item: T) => boolean, startIdx = 0, endIdxEx = array.length): number {
+	let i = startIdx;
+	let j = endIdxEx;
+	while (i < j) {
+		const k = Math.floor((i + j) / 2);
+		if (predicate(array[k])) {
+			i = k + 1;
+		} else {
+			j = k;
+		}
+	}
+	return i - 1;
+}
+
+/**
+ * Finds the first item where predicate is true using binary search.
+ * `predicate` must be monotonous, i.e. `arr.map(predicate)` must be like `[false, ..., false, true, ..., true]`!
+ *
+ * @returns `undefined` if no item matches, otherwise the first item that matches the predicate.
+ */
+export function findFirstMonotonous<T>(array: readonly T[], predicate: (item: T) => boolean): T | undefined {
+	const idx = findFirstIdxMonotonousOrArrLen(array, predicate);
+	return idx === array.length ? undefined : array[idx];
+}
+
+/**
+ * Finds the first item where predicate is true using binary search.
+ * `predicate` must be monotonous, i.e. `arr.map(predicate)` must be like `[false, ..., false, true, ..., true]`!
+ *
+ * @returns `endIdxEx` if predicate is false for all items, otherwise the index of the first item that matches the predicate.
+ */
+export function findFirstIdxMonotonousOrArrLen<T>(array: readonly T[], predicate: (item: T) => boolean, startIdx = 0, endIdxEx = array.length): number {
+	let i = startIdx;
+	let j = endIdxEx;
+	while (i < j) {
+		const k = Math.floor((i + j) / 2);
+		if (predicate(array[k])) {
+			j = k;
+		} else {
+			i = k + 1;
+		}
+	}
+	return i;
+}
+
+export function findFirstIdxMonotonous<T>(array: readonly T[], predicate: (item: T) => boolean, startIdx = 0, endIdxEx = array.length): number {
+	const idx = findFirstIdxMonotonousOrArrLen(array, predicate, startIdx, endIdxEx);
+	return idx === array.length ? -1 : idx;
+}
+
+/**
+ * Use this when
+ * * You have a sorted array
+ * * You query this array with a monotonous predicate to find the last item that has a certain property.
+ * * You query this array multiple times with monotonous predicates that get weaker and weaker.
+ */
+export class MonotonousArray<T> {
+	public static assertInvariants = false;
+
+	private _findLastMonotonousLastIdx = 0;
+	private _prevFindLastPredicate: ((item: T) => boolean) | undefined;
+
+	constructor(private readonly _array: readonly T[]) {
+	}
+
+	/**
+	 * The predicate must be monotonous, i.e. `arr.map(predicate)` must be like `[true, ..., true, false, ..., false]`!
+	 * For subsequent calls, current predicate must be weaker than (or equal to) the previous predicate, i.e. more entries must be `true`.
+	 */
+	findLastMonotonous(predicate: (item: T) => boolean): T | undefined {
+		if (MonotonousArray.assertInvariants) {
+			if (this._prevFindLastPredicate) {
+				for (const item of this._array) {
+					if (this._prevFindLastPredicate(item) && !predicate(item)) {
+						throw new Error('MonotonousArray: current predicate must be weaker than (or equal to) the previous predicate.');
+					}
+				}
+			}
+			this._prevFindLastPredicate = predicate;
+		}
+
+		const idx = findLastIdxMonotonous(this._array, predicate, this._findLastMonotonousLastIdx);
+		this._findLastMonotonousLastIdx = idx + 1;
+		return idx === -1 ? undefined : this._array[idx];
+	}
+}
+
+/**
+ * Returns the first item that is equal to or greater than every other item.
+*/
+export function findFirstMax<T>(array: readonly T[], comparator: Comparator<T>): T | undefined {
+	if (array.length === 0) {
+		return undefined;
+	}
+
+	let max = array[0];
+	for (let i = 1; i < array.length; i++) {
+		const item = array[i];
+		if (comparator(item, max) > 0) {
+			max = item;
+		}
+	}
+	return max;
+}
+
+/**
+ * Returns the last item that is equal to or greater than every other item.
+*/
+export function findLastMax<T>(array: readonly T[], comparator: Comparator<T>): T | undefined {
+	if (array.length === 0) {
+		return undefined;
+	}
+
+	let max = array[0];
+	for (let i = 1; i < array.length; i++) {
+		const item = array[i];
+		if (comparator(item, max) >= 0) {
+			max = item;
+		}
+	}
+	return max;
+}
+
+/**
+ * Returns the first item that is equal to or less than every other item.
+*/
+export function findFirstMin<T>(array: readonly T[], comparator: Comparator<T>): T | undefined {
+	return findFirstMax(array, (a, b) => -comparator(a, b));
+}
+
+export function findMaxIdx<T>(array: readonly T[], comparator: Comparator<T>): number {
+	if (array.length === 0) {
+		return -1;
+	}
+
+	let maxIdx = 0;
+	for (let i = 1; i < array.length; i++) {
+		const item = array[i];
+		if (comparator(item, array[maxIdx]) > 0) {
+			maxIdx = i;
+		}
+	}
+	return maxIdx;
+}
+
+/**
+ * Returns the first mapped value of the array which is not undefined.
+ */
+export function mapFindFirst<T, R>(items: Iterable<T>, mapFn: (value: T) => R | undefined): R | undefined {
+	for (const value of items) {
+		const mapped = mapFn(value);
+		if (mapped !== undefined) {
+			return mapped;
+		}
+	}
+
+	return undefined;
+}
diff --git a/src/vs/base/common/assert.ts b/src/vs/base/common/assert.ts
new file mode 100644
index 0000000000..bbd344d55c
--- /dev/null
+++ b/src/vs/base/common/assert.ts
@@ -0,0 +1,71 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { BugIndicatingError, onUnexpectedError } from 'vs/base/common/errors';
+
+/**
+ * Throws an error with the provided message if the provided value does not evaluate to a true Javascript value.
+ *
+ * @deprecated Use `assert(...)` instead.
+ * This method is usually used like this:
+ * ```ts
+ * import * as assert from 'vs/base/common/assert';
+ * assert.ok(...);
+ * ```
+ *
+ * However, `assert` in that example is a user chosen name.
+ * There is no tooling for generating such an import statement.
+ * Thus, the `assert(...)` function should be used instead.
+ */
+export function ok(value?: unknown, message?: string) {
+	if (!value) {
+		throw new Error(message ? `Assertion failed (${message})` : 'Assertion Failed');
+	}
+}
+
+export function assertNever(value: never, message = 'Unreachable'): never {
+	throw new Error(message);
+}
+
+export function assert(condition: boolean, message = 'unexpected state'): asserts condition {
+	if (!condition) {
+		throw new BugIndicatingError(`Assertion Failed: ${message}`);
+	}
+}
+
+/**
+ * Like assert, but doesn't throw.
+ */
+export function softAssert(condition: boolean): void {
+	if (!condition) {
+		onUnexpectedError(new BugIndicatingError('Soft Assertion Failed'));
+	}
+}
+
+/**
+ * condition must be side-effect free!
+ */
+export function assertFn(condition: () => boolean): void {
+	if (!condition()) {
+		// eslint-disable-next-line no-debugger
+		debugger;
+		// Reevaluate `condition` again to make debugging easier
+		condition();
+		onUnexpectedError(new BugIndicatingError('Assertion Failed'));
+	}
+}
+
+export function checkAdjacentItems<T>(items: readonly T[], predicate: (item1: T, item2: T) => boolean): boolean {
+	let i = 0;
+	while (i < items.length - 1) {
+		const a = items[i];
+		const b = items[i + 1];
+		if (!predicate(a, b)) {
+			return false;
+		}
+		i++;
+	}
+	return true;
+}
diff --git a/src/vs/base/common/async.ts b/src/vs/base/common/async.ts
new file mode 100644
index 0000000000..20f15f3254
--- /dev/null
+++ b/src/vs/base/common/async.ts
@@ -0,0 +1,1992 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
+import { BugIndicatingError, CancellationError } from 'vs/base/common/errors';
+import { Emitter, Event } from 'vs/base/common/event';
+import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
+import { setTimeout0 } from 'vs/base/common/platform';
+import { MicrotaskDelay } from './symbols';
+import { Lazy } from 'vs/base/common/lazy';
+
+export function isThenable<T>(obj: unknown): obj is Promise<T> {
+	return !!obj && typeof (obj as unknown as Promise<T>).then === 'function';
+}
+
+export interface CancelablePromise<T> extends Promise<T> {
+	cancel(): void;
+}
+
+export function createCancelablePromise<T>(callback: (token: CancellationToken) => Promise<T>): CancelablePromise<T> {
+	const source = new CancellationTokenSource();
+
+	const thenable = callback(source.token);
+	const promise = new Promise<T>((resolve, reject) => {
+		const subscription = source.token.onCancellationRequested(() => {
+			subscription.dispose();
+			reject(new CancellationError());
+		});
+		Promise.resolve(thenable).then(value => {
+			subscription.dispose();
+			source.dispose();
+			resolve(value);
+		}, err => {
+			subscription.dispose();
+			source.dispose();
+			reject(err);
+		});
+	});
+
+	return <CancelablePromise<T>>new class {
+		cancel() {
+			source.cancel();
+			source.dispose();
+		}
+		then<TResult1 = T, TResult2 = never>(resolve?: ((value: T) => TResult1 | Promise<TResult1>) | undefined | null, reject?: ((reason: any) => TResult2 | Promise<TResult2>) | undefined | null): Promise<TResult1 | TResult2> {
+			return promise.then(resolve, reject);
+		}
+		catch<TResult = never>(reject?: ((reason: any) => TResult | Promise<TResult>) | undefined | null): Promise<T | TResult> {
+			return this.then(undefined, reject);
+		}
+		finally(onfinally?: (() => void) | undefined | null): Promise<T> {
+			return promise.finally(onfinally);
+		}
+	};
+}
+
+/**
+ * Returns a promise that resolves with `undefined` as soon as the passed token is cancelled.
+ * @see {@link raceCancellationError}
+ */
+export function raceCancellation<T>(promise: Promise<T>, token: CancellationToken): Promise<T | undefined>;
+
+/**
+ * Returns a promise that resolves with `defaultValue` as soon as the passed token is cancelled.
+ * @see {@link raceCancellationError}
+ */
+export function raceCancellation<T>(promise: Promise<T>, token: CancellationToken, defaultValue: T): Promise<T>;
+
+export function raceCancellation<T>(promise: Promise<T>, token: CancellationToken, defaultValue?: T): Promise<T | undefined> {
+	return new Promise((resolve, reject) => {
+		const ref = token.onCancellationRequested(() => {
+			ref.dispose();
+			resolve(defaultValue);
+		});
+		promise.then(resolve, reject).finally(() => ref.dispose());
+	});
+}
+
+/**
+ * Returns a promise that rejects with an {@CancellationError} as soon as the passed token is cancelled.
+ * @see {@link raceCancellation}
+ */
+export function raceCancellationError<T>(promise: Promise<T>, token: CancellationToken): Promise<T> {
+	return new Promise((resolve, reject) => {
+		const ref = token.onCancellationRequested(() => {
+			ref.dispose();
+			reject(new CancellationError());
+		});
+		promise.then(resolve, reject).finally(() => ref.dispose());
+	});
+}
+
+/**
+ * Returns as soon as one of the promises resolves or rejects and cancels remaining promises
+ */
+export async function raceCancellablePromises<T>(cancellablePromises: CancelablePromise<T>[]): Promise<T> {
+	let resolvedPromiseIndex = -1;
+	const promises = cancellablePromises.map((promise, index) => promise.then(result => { resolvedPromiseIndex = index; return result; }));
+	try {
+		const result = await Promise.race(promises);
+		return result;
+	} finally {
+		cancellablePromises.forEach((cancellablePromise, index) => {
+			if (index !== resolvedPromiseIndex) {
+				cancellablePromise.cancel();
+			}
+		});
+	}
+}
+
+export function raceTimeout<T>(promise: Promise<T>, timeout: number, onTimeout?: () => void): Promise<T | undefined> {
+	let promiseResolve: ((value: T | undefined) => void) | undefined = undefined;
+
+	const timer = setTimeout(() => {
+		promiseResolve?.(undefined);
+		onTimeout?.();
+	}, timeout);
+
+	return Promise.race([
+		promise.finally(() => clearTimeout(timer)),
+		new Promise<T | undefined>(resolve => promiseResolve = resolve)
+	]);
+}
+
+export function asPromise<T>(callback: () => T | Thenable<T>): Promise<T> {
+	return new Promise<T>((resolve, reject) => {
+		const item = callback();
+		if (isThenable<T>(item)) {
+			item.then(resolve, reject);
+		} else {
+			resolve(item);
+		}
+	});
+}
+
+/**
+ * Creates and returns a new promise, plus its `resolve` and `reject` callbacks.
+ *
+ * Replace with standardized [`Promise.withResolvers`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers) once it is supported
+ */
+export function promiseWithResolvers<T>(): { promise: Promise<T>; resolve: (value: T | PromiseLike<T>) => void; reject: (err?: any) => void } {
+	let resolve: (value: T | PromiseLike<T>) => void;
+	let reject: (reason?: any) => void;
+	const promise = new Promise<T>((res, rej) => {
+		resolve = res;
+		reject = rej;
+	});
+	return { promise, resolve: resolve!, reject: reject! };
+}
+
+export interface ITask<T> {
+	(): T;
+}
+
+/**
+ * A helper to prevent accumulation of sequential async tasks.
+ *
+ * Imagine a mail man with the sole task of delivering letters. As soon as
+ * a letter submitted for delivery, he drives to the destination, delivers it
+ * and returns to his base. Imagine that during the trip, N more letters were submitted.
+ * When the mail man returns, he picks those N letters and delivers them all in a
+ * single trip. Even though N+1 submissions occurred, only 2 deliveries were made.
+ *
+ * The throttler implements this via the queue() method, by providing it a task
+ * factory. Following the example:
+ *
+ * 		const throttler = new Throttler();
+ * 		const letters = [];
+ *
+ * 		function deliver() {
+ * 			const lettersToDeliver = letters;
+ * 			letters = [];
+ * 			return makeTheTrip(lettersToDeliver);
+ * 		}
+ *
+ * 		function onLetterReceived(l) {
+ * 			letters.push(l);
+ * 			throttler.queue(deliver);
+ * 		}
+ */
+export class Throttler implements IDisposable {
+
+	private activePromise: Promise<any> | null;
+	private queuedPromise: Promise<any> | null;
+	private queuedPromiseFactory: ITask<Promise<any>> | null;
+
+	private isDisposed = false;
+
+	constructor() {
+		this.activePromise = null;
+		this.queuedPromise = null;
+		this.queuedPromiseFactory = null;
+	}
+
+	queue<T>(promiseFactory: ITask<Promise<T>>): Promise<T> {
+		if (this.isDisposed) {
+			return Promise.reject(new Error('Throttler is disposed'));
+		}
+
+		if (this.activePromise) {
+			this.queuedPromiseFactory = promiseFactory;
+
+			if (!this.queuedPromise) {
+				const onComplete = () => {
+					this.queuedPromise = null;
+
+					if (this.isDisposed) {
+						return;
+					}
+
+					const result = this.queue(this.queuedPromiseFactory!);
+					this.queuedPromiseFactory = null;
+
+					return result;
+				};
+
+				this.queuedPromise = new Promise(resolve => {
+					this.activePromise!.then(onComplete, onComplete).then(resolve);
+				});
+			}
+
+			return new Promise((resolve, reject) => {
+				this.queuedPromise!.then(resolve, reject);
+			});
+		}
+
+		this.activePromise = promiseFactory();
+
+		return new Promise((resolve, reject) => {
+			this.activePromise!.then((result: T) => {
+				this.activePromise = null;
+				resolve(result);
+			}, (err: unknown) => {
+				this.activePromise = null;
+				reject(err);
+			});
+		});
+	}
+
+	dispose(): void {
+		this.isDisposed = true;
+	}
+}
+
+export class Sequencer {
+
+	private current: Promise<unknown> = Promise.resolve(null);
+
+	queue<T>(promiseTask: ITask<Promise<T>>): Promise<T> {
+		return this.current = this.current.then(() => promiseTask(), () => promiseTask());
+	}
+}
+
+export class SequencerByKey<TKey> {
+
+	private promiseMap = new Map<TKey, Promise<unknown>>();
+
+	queue<T>(key: TKey, promiseTask: ITask<Promise<T>>): Promise<T> {
+		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;
+	}
+}
+
+interface IScheduledLater extends IDisposable {
+	isTriggered(): boolean;
+}
+
+const timeoutDeferred = (timeout: number, fn: () => void): IScheduledLater => {
+	let scheduled = true;
+	const handle = setTimeout(() => {
+		scheduled = false;
+		fn();
+	}, timeout);
+	return {
+		isTriggered: () => scheduled,
+		dispose: () => {
+			clearTimeout(handle);
+			scheduled = false;
+		},
+	};
+};
+
+const microtaskDeferred = (fn: () => void): IScheduledLater => {
+	let scheduled = true;
+	queueMicrotask(() => {
+		if (scheduled) {
+			scheduled = false;
+			fn();
+		}
+	});
+
+	return {
+		isTriggered: () => scheduled,
+		dispose: () => { scheduled = false; },
+	};
+};
+
+/**
+ * A helper to delay (debounce) execution of a task that is being requested often.
+ *
+ * Following the throttler, now imagine the mail man wants to optimize the number of
+ * trips proactively. The trip itself can be long, so he decides not to make the trip
+ * as soon as a letter is submitted. Instead he waits a while, in case more
+ * letters are submitted. After said waiting period, if no letters were submitted, he
+ * decides to make the trip. Imagine that N more letters were submitted after the first
+ * one, all within a short period of time between each other. Even though N+1
+ * submissions occurred, only 1 delivery was made.
+ *
+ * The delayer offers this behavior via the trigger() method, into which both the task
+ * to be executed and the waiting period (delay) must be passed in as arguments. Following
+ * the example:
+ *
+ * 		const delayer = new Delayer(WAITING_PERIOD);
+ * 		const letters = [];
+ *
+ * 		function letterReceived(l) {
+ * 			letters.push(l);
+ * 			delayer.trigger(() => { return makeTheTrip(); });
+ * 		}
+ */
+export class Delayer<T> implements IDisposable {
+
+	private deferred: IScheduledLater | null;
+	private completionPromise: Promise<any> | null;
+	private doResolve: ((value?: any | Promise<any>) => void) | null;
+	private doReject: ((err: any) => void) | null;
+	private task: ITask<T | Promise<T>> | null;
+
+	constructor(public defaultDelay: number | typeof MicrotaskDelay) {
+		this.deferred = null;
+		this.completionPromise = null;
+		this.doResolve = null;
+		this.doReject = null;
+		this.task = null;
+	}
+
+	trigger(task: ITask<T | Promise<T>>, delay = this.defaultDelay): Promise<T> {
+		this.task = task;
+		this.cancelTimeout();
+
+		if (!this.completionPromise) {
+			this.completionPromise = new Promise((resolve, reject) => {
+				this.doResolve = resolve;
+				this.doReject = reject;
+			}).then(() => {
+				this.completionPromise = null;
+				this.doResolve = null;
+				if (this.task) {
+					const task = this.task;
+					this.task = null;
+					return task();
+				}
+				return undefined;
+			});
+		}
+
+		const fn = () => {
+			this.deferred = null;
+			this.doResolve?.(null);
+		};
+
+		this.deferred = delay === MicrotaskDelay ? microtaskDeferred(fn) : timeoutDeferred(delay, fn);
+
+		return this.completionPromise;
+	}
+
+	isTriggered(): boolean {
+		return !!this.deferred?.isTriggered();
+	}
+
+	cancel(): void {
+		this.cancelTimeout();
+
+		if (this.completionPromise) {
+			this.doReject?.(new CancellationError());
+			this.completionPromise = null;
+		}
+	}
+
+	private cancelTimeout(): void {
+		this.deferred?.dispose();
+		this.deferred = null;
+	}
+
+	dispose(): void {
+		this.cancel();
+	}
+}
+
+/**
+ * A helper to delay execution of a task that is being requested often, while
+ * preventing accumulation of consecutive executions, while the task runs.
+ *
+ * The mail man is clever and waits for a certain amount of time, before going
+ * out to deliver letters. While the mail man is going out, more letters arrive
+ * and can only be delivered once he is back. Once he is back the mail man will
+ * do one more trip to deliver the letters that have accumulated while he was out.
+ */
+export class ThrottledDelayer<T> {
+
+	private delayer: Delayer<Promise<T>>;
+	private throttler: Throttler;
+
+	constructor(defaultDelay: number) {
+		this.delayer = new Delayer(defaultDelay);
+		this.throttler = new Throttler();
+	}
+
+	trigger(promiseFactory: ITask<Promise<T>>, delay?: number): Promise<T> {
+		return this.delayer.trigger(() => this.throttler.queue(promiseFactory), delay) as unknown as Promise<T>;
+	}
+
+	isTriggered(): boolean {
+		return this.delayer.isTriggered();
+	}
+
+	cancel(): void {
+		this.delayer.cancel();
+	}
+
+	dispose(): void {
+		this.delayer.dispose();
+		this.throttler.dispose();
+	}
+}
+
+/**
+ * A barrier that is initially closed and then becomes opened permanently.
+ */
+export class Barrier {
+	private _isOpen: boolean;
+	private _promise: Promise<boolean>;
+	private _completePromise!: (v: boolean) => void;
+
+	constructor() {
+		this._isOpen = false;
+		this._promise = new Promise<boolean>((c, e) => {
+			this._completePromise = c;
+		});
+	}
+
+	isOpen(): boolean {
+		return this._isOpen;
+	}
+
+	open(): void {
+		this._isOpen = true;
+		this._completePromise(true);
+	}
+
+	wait(): Promise<boolean> {
+		return this._promise;
+	}
+}
+
+/**
+ * A barrier that is initially closed and then becomes opened permanently after a certain period of
+ * time or when open is called explicitly
+ */
+export class AutoOpenBarrier extends Barrier {
+
+	private readonly _timeout: any;
+
+	constructor(autoOpenTimeMs: number) {
+		super();
+		this._timeout = setTimeout(() => this.open(), autoOpenTimeMs);
+	}
+
+	override open(): void {
+		clearTimeout(this._timeout);
+		super.open();
+	}
+}
+
+export function timeout(millis: number): CancelablePromise<void>;
+export function timeout(millis: number, token: CancellationToken): Promise<void>;
+export function timeout(millis: number, token?: CancellationToken): CancelablePromise<void> | Promise<void> {
+	if (!token) {
+		return createCancelablePromise(token => timeout(millis, token));
+	}
+
+	return new Promise((resolve, reject) => {
+		const handle = setTimeout(() => {
+			disposable.dispose();
+			resolve();
+		}, millis);
+		const disposable = token.onCancellationRequested(() => {
+			clearTimeout(handle);
+			disposable.dispose();
+			reject(new CancellationError());
+		});
+	});
+}
+
+/**
+ * Creates a timeout that can be disposed using its returned value.
+ * @param handler The timeout handler.
+ * @param timeout An optional timeout in milliseconds.
+ * @param store An optional {@link DisposableStore} that will have the timeout disposable managed automatically.
+ *
+ * @example
+ * const store = new DisposableStore;
+ * // Call the timeout after 1000ms at which point it will be automatically
+ * // evicted from the store.
+ * const timeoutDisposable = disposableTimeout(() => {}, 1000, store);
+ *
+ * if (foo) {
+ *   // Cancel the timeout and evict it from store.
+ *   timeoutDisposable.dispose();
+ * }
+ */
+export function disposableTimeout(handler: () => void, timeout = 0, store?: DisposableStore): IDisposable {
+	const timer = setTimeout(() => {
+		handler();
+		if (store) {
+			disposable.dispose();
+		}
+	}, timeout);
+	const disposable = toDisposable(() => {
+		clearTimeout(timer);
+		store?.deleteAndLeak(disposable);
+	});
+	store?.add(disposable);
+	return disposable;
+}
+
+/**
+ * Runs the provided list of promise factories in sequential order. The returned
+ * promise will complete to an array of results from each promise.
+ */
+
+export function sequence<T>(promiseFactories: ITask<Promise<T>>[]): Promise<T[]> {
+	const results: T[] = [];
+	let index = 0;
+	const len = promiseFactories.length;
+
+	function next(): Promise<T> | null {
+		return index < len ? promiseFactories[index++]() : null;
+	}
+
+	function thenHandler(result: any): Promise<any> {
+		if (result !== undefined && result !== null) {
+			results.push(result);
+		}
+
+		const n = next();
+		if (n) {
+			return n.then(thenHandler);
+		}
+
+		return Promise.resolve(results);
+	}
+
+	return Promise.resolve(null).then(thenHandler);
+}
+
+export function first<T>(promiseFactories: ITask<Promise<T>>[], shouldStop: (t: T) => boolean = t => !!t, defaultValue: T | null = null): Promise<T | null> {
+	let index = 0;
+	const len = promiseFactories.length;
+
+	const loop: () => Promise<T | null> = () => {
+		if (index >= len) {
+			return Promise.resolve(defaultValue);
+		}
+
+		const factory = promiseFactories[index++];
+		const promise = Promise.resolve(factory());
+
+		return promise.then(result => {
+			if (shouldStop(result)) {
+				return Promise.resolve(result);
+			}
+
+			return loop();
+		});
+	};
+
+	return loop();
+}
+
+/**
+ * Returns the result of the first promise that matches the "shouldStop",
+ * running all promises in parallel. Supports cancelable promises.
+ */
+export function firstParallel<T>(promiseList: Promise<T>[], shouldStop?: (t: T) => boolean, defaultValue?: T | null): Promise<T | null>;
+export function firstParallel<T, R extends T>(promiseList: Promise<T>[], shouldStop: (t: T) => t is R, defaultValue?: R | null): Promise<R | null>;
+export function firstParallel<T>(promiseList: Promise<T>[], shouldStop: (t: T) => boolean = t => !!t, defaultValue: T | null = null) {
+	if (promiseList.length === 0) {
+		return Promise.resolve(defaultValue);
+	}
+
+	let todo = promiseList.length;
+	const finish = () => {
+		todo = -1;
+		for (const promise of promiseList) {
+			(promise as Partial<CancelablePromise<T>>).cancel?.();
+		}
+	};
+
+	return new Promise<T | null>((resolve, reject) => {
+		for (const promise of promiseList) {
+			promise.then(result => {
+				if (--todo >= 0 && shouldStop(result)) {
+					finish();
+					resolve(result);
+				} else if (todo === 0) {
+					resolve(defaultValue);
+				}
+			})
+				.catch(err => {
+					if (--todo >= 0) {
+						finish();
+						reject(err);
+					}
+				});
+		}
+	});
+}
+
+interface ILimitedTaskFactory<T> {
+	factory: ITask<Promise<T>>;
+	c: (value: T | Promise<T>) => void;
+	e: (error?: unknown) => void;
+}
+
+export interface ILimiter<T> {
+
+	readonly size: number;
+
+	queue(factory: ITask<Promise<T>>): Promise<T>;
+
+	clear(): void;
+}
+
+/**
+ * A helper to queue N promises and run them all with a max degree of parallelism. The helper
+ * ensures that at any time no more than M promises are running at the same time.
+ */
+export class Limiter<T> implements ILimiter<T> {
+
+	private _size = 0;
+	private _isDisposed = false;
+	private runningPromises: number;
+	private readonly maxDegreeOfParalellism: number;
+	private readonly outstandingPromises: ILimitedTaskFactory<T>[];
+	private readonly _onDrained: Emitter<void>;
+
+	constructor(maxDegreeOfParalellism: number) {
+		this.maxDegreeOfParalellism = maxDegreeOfParalellism;
+		this.outstandingPromises = [];
+		this.runningPromises = 0;
+		this._onDrained = new Emitter<void>();
+	}
+
+	/**
+	 *
+	 * @returns A promise that resolved when all work is done (onDrained) or when
+	 * there is nothing to do
+	 */
+	whenIdle(): Promise<void> {
+		return this.size > 0
+			? Event.toPromise(this.onDrained)
+			: Promise.resolve();
+	}
+
+	get onDrained(): Event<void> {
+		return this._onDrained.event;
+	}
+
+	get size(): number {
+		return this._size;
+	}
+
+	queue(factory: ITask<Promise<T>>): Promise<T> {
+		if (this._isDisposed) {
+			throw new Error('Object has been disposed');
+		}
+		this._size++;
+
+		return new Promise<T>((c, e) => {
+			this.outstandingPromises.push({ factory, c, e });
+			this.consume();
+		});
+	}
+
+	private consume(): void {
+		while (this.outstandingPromises.length && this.runningPromises < this.maxDegreeOfParalellism) {
+			const iLimitedTask = this.outstandingPromises.shift()!;
+			this.runningPromises++;
+
+			const promise = iLimitedTask.factory();
+			promise.then(iLimitedTask.c, iLimitedTask.e);
+			promise.then(() => this.consumed(), () => this.consumed());
+		}
+	}
+
+	private consumed(): void {
+		if (this._isDisposed) {
+			return;
+		}
+		this.runningPromises--;
+		if (--this._size === 0) {
+			this._onDrained.fire();
+		}
+
+		if (this.outstandingPromises.length > 0) {
+			this.consume();
+		}
+	}
+
+	clear(): void {
+		if (this._isDisposed) {
+			throw new Error('Object has been disposed');
+		}
+		this.outstandingPromises.length = 0;
+		this._size = this.runningPromises;
+	}
+
+	dispose(): void {
+		this._isDisposed = true;
+		this.outstandingPromises.length = 0; // stop further processing
+		this._size = 0;
+		this._onDrained.dispose();
+	}
+}
+
+/**
+ * A queue is handles one promise at a time and guarantees that at any time only one promise is executing.
+ */
+export class Queue<T> extends Limiter<T> {
+
+	constructor() {
+		super(1);
+	}
+}
+
+/**
+ * Same as `Queue`, ensures that only 1 task is executed at the same time. The difference to `Queue` is that
+ * there is only 1 task about to be scheduled next. As such, calling `queue` while a task is executing will
+ * replace the currently queued task until it executes.
+ *
+ * As such, the returned promise may not be from the factory that is passed in but from the next factory that
+ * is running after having called `queue`.
+ */
+export class LimitedQueue {
+
+	private readonly sequentializer = new TaskSequentializer();
+
+	private tasks = 0;
+
+	queue(factory: ITask<Promise<void>>): Promise<void> {
+		if (!this.sequentializer.isRunning()) {
+			return this.sequentializer.run(this.tasks++, factory());
+		}
+
+		return this.sequentializer.queue(() => {
+			return this.sequentializer.run(this.tasks++, factory());
+		});
+	}
+}
+
+export class TimeoutTimer implements IDisposable {
+	private _token: any;
+	private _isDisposed = false;
+
+	constructor();
+	constructor(runner: () => void, timeout: number);
+	constructor(runner?: () => void, timeout?: number) {
+		this._token = -1;
+
+		if (typeof runner === 'function' && typeof timeout === 'number') {
+			this.setIfNotSet(runner, timeout);
+		}
+	}
+
+	dispose(): void {
+		this.cancel();
+		this._isDisposed = true;
+	}
+
+	cancel(): void {
+		if (this._token !== -1) {
+			clearTimeout(this._token);
+			this._token = -1;
+		}
+	}
+
+	cancelAndSet(runner: () => void, timeout: number): void {
+		if (this._isDisposed) {
+			throw new BugIndicatingError(`Calling 'cancelAndSet' on a disposed TimeoutTimer`);
+		}
+
+		this.cancel();
+		this._token = setTimeout(() => {
+			this._token = -1;
+			runner();
+		}, timeout);
+	}
+
+	setIfNotSet(runner: () => void, timeout: number): void {
+		if (this._isDisposed) {
+			throw new BugIndicatingError(`Calling 'setIfNotSet' on a disposed TimeoutTimer`);
+		}
+
+		if (this._token !== -1) {
+			// timer is already set
+			return;
+		}
+		this._token = setTimeout(() => {
+			this._token = -1;
+			runner();
+		}, timeout);
+	}
+}
+
+export class IntervalTimer implements IDisposable {
+
+	private disposable: IDisposable | undefined = undefined;
+	private isDisposed = false;
+
+	cancel(): void {
+		this.disposable?.dispose();
+		this.disposable = undefined;
+	}
+
+	cancelAndSet(runner: () => void, interval: number, context = globalThis): void {
+		if (this.isDisposed) {
+			throw new BugIndicatingError(`Calling 'cancelAndSet' on a disposed IntervalTimer`);
+		}
+
+		this.cancel();
+		const handle = context.setInterval(() => {
+			runner();
+		}, interval);
+
+		this.disposable = toDisposable(() => {
+			context.clearInterval(handle);
+			this.disposable = undefined;
+		});
+	}
+
+	dispose(): void {
+		this.cancel();
+		this.isDisposed = true;
+	}
+}
+
+export class RunOnceScheduler implements IDisposable {
+
+	protected runner: ((...args: unknown[]) => void) | null;
+
+	private timeoutToken: any;
+	private timeout: number;
+	private timeoutHandler: () => void;
+
+	constructor(runner: (...args: any[]) => void, delay: number) {
+		this.timeoutToken = -1;
+		this.runner = runner;
+		this.timeout = delay;
+		this.timeoutHandler = this.onTimeout.bind(this);
+	}
+
+	/**
+	 * Dispose RunOnceScheduler
+	 */
+	dispose(): void {
+		this.cancel();
+		this.runner = null;
+	}
+
+	/**
+	 * Cancel current scheduled runner (if any).
+	 */
+	cancel(): void {
+		if (this.isScheduled()) {
+			clearTimeout(this.timeoutToken);
+			this.timeoutToken = -1;
+		}
+	}
+
+	/**
+	 * Cancel previous runner (if any) & schedule a new runner.
+	 */
+	schedule(delay = this.timeout): void {
+		this.cancel();
+		this.timeoutToken = setTimeout(this.timeoutHandler, delay);
+	}
+
+	get delay(): number {
+		return this.timeout;
+	}
+
+	set delay(value: number) {
+		this.timeout = value;
+	}
+
+	/**
+	 * Returns true if scheduled.
+	 */
+	isScheduled(): boolean {
+		return this.timeoutToken !== -1;
+	}
+
+	flush(): void {
+		if (this.isScheduled()) {
+			this.cancel();
+			this.doRun();
+		}
+	}
+
+	private onTimeout() {
+		this.timeoutToken = -1;
+		if (this.runner) {
+			this.doRun();
+		}
+	}
+
+	protected doRun(): void {
+		this.runner?.();
+	}
+}
+
+/**
+ * Same as `RunOnceScheduler`, but doesn't count the time spent in sleep mode.
+ * > **NOTE**: Only offers 1s resolution.
+ *
+ * When calling `setTimeout` with 3hrs, and putting the computer immediately to sleep
+ * for 8hrs, `setTimeout` will fire **as soon as the computer wakes from sleep**. But
+ * this scheduler will execute 3hrs **after waking the computer from sleep**.
+ */
+export class ProcessTimeRunOnceScheduler {
+
+	private runner: (() => void) | null;
+	private timeout: number;
+
+	private counter: number;
+	private intervalToken: any;
+	private intervalHandler: () => void;
+
+	constructor(runner: () => void, delay: number) {
+		if (delay % 1000 !== 0) {
+			console.warn(`ProcessTimeRunOnceScheduler resolution is 1s, ${delay}ms is not a multiple of 1000ms.`);
+		}
+		this.runner = runner;
+		this.timeout = delay;
+		this.counter = 0;
+		this.intervalToken = -1;
+		this.intervalHandler = this.onInterval.bind(this);
+	}
+
+	dispose(): void {
+		this.cancel();
+		this.runner = null;
+	}
+
+	cancel(): void {
+		if (this.isScheduled()) {
+			clearInterval(this.intervalToken);
+			this.intervalToken = -1;
+		}
+	}
+
+	/**
+	 * Cancel previous runner (if any) & schedule a new runner.
+	 */
+	schedule(delay = this.timeout): void {
+		if (delay % 1000 !== 0) {
+			console.warn(`ProcessTimeRunOnceScheduler resolution is 1s, ${delay}ms is not a multiple of 1000ms.`);
+		}
+		this.cancel();
+		this.counter = Math.ceil(delay / 1000);
+		this.intervalToken = setInterval(this.intervalHandler, 1000);
+	}
+
+	/**
+	 * Returns true if scheduled.
+	 */
+	isScheduled(): boolean {
+		return this.intervalToken !== -1;
+	}
+
+	private onInterval() {
+		this.counter--;
+		if (this.counter > 0) {
+			// still need to wait
+			return;
+		}
+
+		// time elapsed
+		clearInterval(this.intervalToken);
+		this.intervalToken = -1;
+		this.runner?.();
+	}
+}
+
+export class RunOnceWorker<T> extends RunOnceScheduler {
+
+	private units: T[] = [];
+
+	constructor(runner: (units: T[]) => void, timeout: number) {
+		super(runner, timeout);
+	}
+
+	work(unit: T): void {
+		this.units.push(unit);
+
+		if (!this.isScheduled()) {
+			this.schedule();
+		}
+	}
+
+	protected override doRun(): void {
+		const units = this.units;
+		this.units = [];
+
+		this.runner?.(units);
+	}
+
+	override dispose(): void {
+		this.units = [];
+
+		super.dispose();
+	}
+}
+
+export interface IThrottledWorkerOptions {
+
+	/**
+	 * maximum of units the worker will pass onto handler at once
+	 */
+	maxWorkChunkSize: number;
+
+	/**
+	 * maximum of units the worker will keep in memory for processing
+	 */
+	maxBufferedWork: number | undefined;
+
+	/**
+	 * delay before processing the next round of chunks when chunk size exceeds limits
+	 */
+	throttleDelay: number;
+}
+
+/**
+ * The `ThrottledWorker` will accept units of work `T`
+ * to handle. The contract is:
+ * * there is a maximum of units the worker can handle at once (via `maxWorkChunkSize`)
+ * * there is a maximum of units the worker will keep in memory for processing (via `maxBufferedWork`)
+ * * after having handled `maxWorkChunkSize` units, the worker needs to rest (via `throttleDelay`)
+ */
+export class ThrottledWorker<T> extends Disposable {
+
+	private readonly pendingWork: T[] = [];
+
+	private readonly throttler = this._register(new MutableDisposable<RunOnceScheduler>());
+	private disposed = false;
+
+	constructor(
+		private options: IThrottledWorkerOptions,
+		private readonly handler: (units: T[]) => void
+	) {
+		super();
+	}
+
+	/**
+	 * The number of work units that are pending to be processed.
+	 */
+	get pending(): number { return this.pendingWork.length; }
+
+	/**
+	 * Add units to be worked on. Use `pending` to figure out
+	 * how many units are not yet processed after this method
+	 * was called.
+	 *
+	 * @returns whether the work was accepted or not. If the
+	 * worker is disposed, it will not accept any more work.
+	 * If the number of pending units would become larger
+	 * than `maxPendingWork`, more work will also not be accepted.
+	 */
+	work(units: readonly T[]): boolean {
+		if (this.disposed) {
+			return false; // work not accepted: disposed
+		}
+
+		// Check for reaching maximum of pending work
+		if (typeof this.options.maxBufferedWork === 'number') {
+
+			// Throttled: simple check if pending + units exceeds max pending
+			if (this.throttler.value) {
+				if (this.pending + units.length > this.options.maxBufferedWork) {
+					return false; // work not accepted: too much pending work
+				}
+			}
+
+			// Unthrottled: same as throttled, but account for max chunk getting
+			// worked on directly without being pending
+			else {
+				if (this.pending + units.length - this.options.maxWorkChunkSize > this.options.maxBufferedWork) {
+					return false; // work not accepted: too much pending work
+				}
+			}
+		}
+
+		// Add to pending units first
+		for (const unit of units) {
+			this.pendingWork.push(unit);
+		}
+
+		// If not throttled, start working directly
+		// Otherwise, when the throttle delay has
+		// past, pending work will be worked again.
+		if (!this.throttler.value) {
+			this.doWork();
+		}
+
+		return true; // work accepted
+	}
+
+	private doWork(): void {
+
+		// Extract chunk to handle and handle it
+		this.handler(this.pendingWork.splice(0, this.options.maxWorkChunkSize));
+
+		// If we have remaining work, schedule it after a delay
+		if (this.pendingWork.length > 0) {
+			this.throttler.value = new RunOnceScheduler(() => {
+				this.throttler.clear();
+
+				this.doWork();
+			}, this.options.throttleDelay);
+			this.throttler.value.schedule();
+		}
+	}
+
+	override dispose(): void {
+		super.dispose();
+
+		this.disposed = true;
+	}
+}
+
+//#region -- run on idle tricks ------------
+
+export interface IdleDeadline {
+	readonly didTimeout: boolean;
+	timeRemaining(): number;
+}
+
+type IdleApi = Pick<typeof globalThis, 'requestIdleCallback' | 'cancelIdleCallback'>;
+
+
+/**
+ * Execute the callback the next time the browser is idle, returning an
+ * {@link IDisposable} that will cancel the callback when disposed. This wraps
+ * [requestIdleCallback] so it will fallback to [setTimeout] if the environment
+ * doesn't support it.
+ *
+ * @param callback The callback to run when idle, this includes an
+ * [IdleDeadline] that provides the time alloted for the idle callback by the
+ * browser. Not respecting this deadline will result in a degraded user
+ * experience.
+ * @param timeout A timeout at which point to queue no longer wait for an idle
+ * callback but queue it on the regular event loop (like setTimeout). Typically
+ * this should not be used.
+ *
+ * [IdleDeadline]: https://developer.mozilla.org/en-US/docs/Web/API/IdleDeadline
+ * [requestIdleCallback]: https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback
+ * [setTimeout]: https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout
+ *
+ * **Note** that there is `dom.ts#runWhenWindowIdle` which is better suited when running inside a browser
+ * context
+ */
+export let runWhenGlobalIdle: (callback: (idle: IdleDeadline) => void, timeout?: number) => IDisposable;
+
+export let _runWhenIdle: (targetWindow: IdleApi, callback: (idle: IdleDeadline) => void, timeout?: number) => IDisposable;
+
+(function () {
+	if (typeof globalThis.requestIdleCallback !== 'function' || typeof globalThis.cancelIdleCallback !== 'function') {
+		_runWhenIdle = (_targetWindow, runner) => {
+			setTimeout0(() => {
+				if (disposed) {
+					return;
+				}
+				const end = Date.now() + 15; // one frame at 64fps
+				const deadline: IdleDeadline = {
+					didTimeout: true,
+					timeRemaining() {
+						return Math.max(0, end - Date.now());
+					}
+				};
+				runner(Object.freeze(deadline));
+			});
+			let disposed = false;
+			return {
+				dispose() {
+					if (disposed) {
+						return;
+					}
+					disposed = true;
+				}
+			};
+		};
+	} else {
+		_runWhenIdle = (targetWindow: IdleApi, runner, timeout?) => {
+			const handle: number = targetWindow.requestIdleCallback(runner, typeof timeout === 'number' ? { timeout } : undefined);
+			let disposed = false;
+			return {
+				dispose() {
+					if (disposed) {
+						return;
+					}
+					disposed = true;
+					targetWindow.cancelIdleCallback(handle);
+				}
+			};
+		};
+	}
+	runWhenGlobalIdle = (runner) => _runWhenIdle(globalThis, runner);
+})();
+
+export abstract class AbstractIdleValue<T> {
+
+	private readonly _executor: () => void;
+	private readonly _handle: IDisposable;
+
+	private _didRun: boolean = false;
+	private _value?: T;
+	private _error: unknown;
+
+	constructor(targetWindow: IdleApi, executor: () => T) {
+		this._executor = () => {
+			try {
+				this._value = executor();
+			} catch (err) {
+				this._error = err;
+			} finally {
+				this._didRun = true;
+			}
+		};
+		this._handle = _runWhenIdle(targetWindow, () => this._executor());
+	}
+
+	dispose(): void {
+		this._handle.dispose();
+	}
+
+	get value(): T {
+		if (!this._didRun) {
+			this._handle.dispose();
+			this._executor();
+		}
+		if (this._error) {
+			throw this._error;
+		}
+		return this._value!;
+	}
+
+	get isInitialized(): boolean {
+		return this._didRun;
+	}
+}
+
+/**
+ * An `IdleValue` that always uses the current window (which might be throttled or inactive)
+ *
+ * **Note** that there is `dom.ts#WindowIdleValue` which is better suited when running inside a browser
+ * context
+ */
+export class GlobalIdleValue<T> extends AbstractIdleValue<T> {
+
+	constructor(executor: () => T) {
+		super(globalThis, executor);
+	}
+}
+
+//#endregion
+
+export async function retry<T>(task: ITask<Promise<T>>, delay: number, retries: number): Promise<T> {
+	let lastError: Error | undefined;
+
+	for (let i = 0; i < retries; i++) {
+		try {
+			return await task();
+		} catch (error) {
+			lastError = error;
+
+			await timeout(delay);
+		}
+	}
+
+	throw lastError;
+}
+
+//#region Task Sequentializer
+
+interface IRunningTask {
+	readonly taskId: number;
+	readonly cancel: () => void;
+	readonly promise: Promise<void>;
+}
+
+interface IQueuedTask {
+	readonly promise: Promise<void>;
+	readonly promiseResolve: () => void;
+	readonly promiseReject: (error: Error) => void;
+	run: ITask<Promise<void>>;
+}
+
+export interface ITaskSequentializerWithRunningTask {
+	readonly running: Promise<void>;
+}
+
+export interface ITaskSequentializerWithQueuedTask {
+	readonly queued: IQueuedTask;
+}
+
+/**
+ * @deprecated use `LimitedQueue` instead for an easier to use API
+ */
+export class TaskSequentializer {
+
+	private _running?: IRunningTask;
+	private _queued?: IQueuedTask;
+
+	isRunning(taskId?: number): this is ITaskSequentializerWithRunningTask {
+		if (typeof taskId === 'number') {
+			return this._running?.taskId === taskId;
+		}
+
+		return !!this._running;
+	}
+
+	get running(): Promise<void> | undefined {
+		return this._running?.promise;
+	}
+
+	cancelRunning(): void {
+		this._running?.cancel();
+	}
+
+	run(taskId: number, promise: Promise<void>, onCancel?: () => void,): Promise<void> {
+		this._running = { taskId, cancel: () => onCancel?.(), promise };
+
+		promise.then(() => this.doneRunning(taskId), () => this.doneRunning(taskId));
+
+		return promise;
+	}
+
+	private doneRunning(taskId: number): void {
+		if (this._running && taskId === this._running.taskId) {
+
+			// only set running to done if the promise finished that is associated with that taskId
+			this._running = undefined;
+
+			// schedule the queued task now that we are free if we have any
+			this.runQueued();
+		}
+	}
+
+	private runQueued(): void {
+		if (this._queued) {
+			const queued = this._queued;
+			this._queued = undefined;
+
+			// Run queued task and complete on the associated promise
+			queued.run().then(queued.promiseResolve, queued.promiseReject);
+		}
+	}
+
+	/**
+	 * Note: the promise to schedule as next run MUST itself call `run`.
+	 *       Otherwise, this sequentializer will report `false` for `isRunning`
+	 *       even when this task is running. Missing this detail means that
+	 *       suddenly multiple tasks will run in parallel.
+	 */
+	queue(run: ITask<Promise<void>>): Promise<void> {
+
+		// this is our first queued task, so we create associated promise with it
+		// so that we can return a promise that completes when the task has
+		// completed.
+		if (!this._queued) {
+			const { promise, resolve: promiseResolve, reject: promiseReject } = promiseWithResolvers<void>();
+			this._queued = {
+				run,
+				promise,
+				promiseResolve: promiseResolve!,
+				promiseReject: promiseReject!
+			};
+		}
+
+		// we have a previous queued task, just overwrite it
+		else {
+			this._queued.run = run;
+		}
+
+		return this._queued.promise;
+	}
+
+	hasQueued(): this is ITaskSequentializerWithQueuedTask {
+		return !!this._queued;
+	}
+
+	async join(): Promise<void> {
+		return this._queued?.promise ?? this._running?.promise;
+	}
+}
+
+//#endregion
+
+//#region
+
+/**
+ * The `IntervalCounter` allows to count the number
+ * of calls to `increment()` over a duration of
+ * `interval`. This utility can be used to conditionally
+ * throttle a frequent task when a certain threshold
+ * is reached.
+ */
+export class IntervalCounter {
+
+	private lastIncrementTime = 0;
+
+	private value = 0;
+
+	constructor(private readonly interval: number, private readonly nowFn = () => Date.now()) { }
+
+	increment(): number {
+		const now = this.nowFn();
+
+		// We are outside of the range of `interval` and as such
+		// start counting from 0 and remember the time
+		if (now - this.lastIncrementTime > this.interval) {
+			this.lastIncrementTime = now;
+			this.value = 0;
+		}
+
+		this.value++;
+
+		return this.value;
+	}
+}
+
+//#endregion
+
+//#region
+
+export type ValueCallback<T = unknown> = (value: T | Promise<T>) => void;
+
+const enum DeferredOutcome {
+	Resolved,
+	Rejected
+}
+
+/**
+ * Creates a promise whose resolution or rejection can be controlled imperatively.
+ */
+export class DeferredPromise<T> {
+
+	private completeCallback!: ValueCallback<T>;
+	private errorCallback!: (err: unknown) => void;
+	private outcome?: { outcome: DeferredOutcome.Rejected; value: any } | { outcome: DeferredOutcome.Resolved; value: T };
+
+	public get isRejected() {
+		return this.outcome?.outcome === DeferredOutcome.Rejected;
+	}
+
+	public get isResolved() {
+		return this.outcome?.outcome === DeferredOutcome.Resolved;
+	}
+
+	public get isSettled() {
+		return !!this.outcome;
+	}
+
+	public get value() {
+		return this.outcome?.outcome === DeferredOutcome.Resolved ? this.outcome?.value : undefined;
+	}
+
+	public readonly p: Promise<T>;
+
+	constructor() {
+		this.p = new Promise<T>((c, e) => {
+			this.completeCallback = c;
+			this.errorCallback = e;
+		});
+	}
+
+	public complete(value: T) {
+		return new Promise<void>(resolve => {
+			this.completeCallback(value);
+			this.outcome = { outcome: DeferredOutcome.Resolved, value };
+			resolve();
+		});
+	}
+
+	public error(err: unknown) {
+		return new Promise<void>(resolve => {
+			this.errorCallback(err);
+			this.outcome = { outcome: DeferredOutcome.Rejected, value: err };
+			resolve();
+		});
+	}
+
+	public cancel() {
+		return this.error(new CancellationError());
+	}
+}
+
+//#endregion
+
+//#region Promises
+
+export namespace Promises {
+
+	/**
+	 * A drop-in replacement for `Promise.all` with the only difference
+	 * that the method awaits every promise to either fulfill or reject.
+	 *
+	 * Similar to `Promise.all`, only the first error will be returned
+	 * if any.
+	 */
+	export async function settled<T>(promises: Promise<T>[]): Promise<T[]> {
+		let firstError: Error | undefined = undefined;
+
+		const result = await Promise.all(promises.map(promise => promise.then(value => value, error => {
+			if (!firstError) {
+				firstError = error;
+			}
+
+			return undefined; // do not rethrow so that other promises can settle
+		})));
+
+		if (typeof firstError !== 'undefined') {
+			throw firstError;
+		}
+
+		return result as unknown as T[]; // cast is needed and protected by the `throw` above
+	}
+
+	/**
+	 * A helper to create a new `Promise<T>` with a body that is a promise
+	 * itself. By default, an error that raises from the async body will
+	 * end up as a unhandled rejection, so this utility properly awaits the
+	 * body and rejects the promise as a normal promise does without async
+	 * body.
+	 *
+	 * This method should only be used in rare cases where otherwise `async`
+	 * cannot be used (e.g. when callbacks are involved that require this).
+	 */
+	export function withAsyncBody<T, E = Error>(bodyFn: (resolve: (value: T) => unknown, reject: (error: E) => unknown) => Promise<unknown>): Promise<T> {
+		// eslint-disable-next-line no-async-promise-executor
+		return new Promise<T>(async (resolve, reject) => {
+			try {
+				await bodyFn(resolve, reject);
+			} catch (error) {
+				reject(error);
+			}
+		});
+	}
+}
+
+export class StatefulPromise<T> {
+	private _value: T | undefined = undefined;
+	get value(): T | undefined { return this._value; }
+
+	private _error: unknown = undefined;
+	get error(): unknown { return this._error; }
+
+	private _isResolved = false;
+	get isResolved() { return this._isResolved; }
+
+	public readonly promise: Promise<T>;
+
+	constructor(promise: Promise<T>) {
+		this.promise = promise.then(
+			value => {
+				this._value = value;
+				this._isResolved = true;
+				return value;
+			},
+			error => {
+				this._error = error;
+				this._isResolved = true;
+				throw error;
+			}
+		);
+	}
+
+	/**
+	 * Returns the resolved value.
+	 * Throws if the promise is not resolved yet.
+	 */
+	public requireValue(): T {
+		if (!this._isResolved) {
+			throw new BugIndicatingError('Promise is not resolved yet');
+		}
+		if (this._error) {
+			throw this._error;
+		}
+		return this._value!;
+	}
+}
+
+export class LazyStatefulPromise<T> {
+	private readonly _promise = new Lazy(() => new StatefulPromise(this._compute()));
+
+	constructor(
+		private readonly _compute: () => Promise<T>,
+	) { }
+
+	/**
+	 * Returns the resolved value.
+	 * Throws if the promise is not resolved yet.
+	 */
+	public requireValue(): T {
+		return this._promise.value.requireValue();
+	}
+
+	/**
+	 * Returns the promise (and triggers a computation of the promise if not yet done so).
+	 */
+	public getPromise(): Promise<T> {
+		return this._promise.value.promise;
+	}
+
+	/**
+	 * Reads the current value without triggering a computation of the promise.
+	 */
+	public get currentValue(): T | undefined {
+		return this._promise.rawValue?.value;
+	}
+}
+
+//#endregion
+
+//#region
+
+const enum AsyncIterableSourceState {
+	Initial,
+	DoneOK,
+	DoneError,
+}
+
+/**
+ * An object that allows to emit async values asynchronously or bring the iterable to an error state using `reject()`.
+ * This emitter is valid only for the duration of the executor (until the promise returned by the executor settles).
+ */
+export interface AsyncIterableEmitter<T> {
+	/**
+	 * The value will be appended at the end.
+	 *
+	 * **NOTE** If `reject()` has already been called, this method has no effect.
+	 */
+	emitOne(value: T): void;
+	/**
+	 * The values will be appended at the end.
+	 *
+	 * **NOTE** If `reject()` has already been called, this method has no effect.
+	 */
+	emitMany(values: T[]): void;
+	/**
+	 * Writing an error will permanently invalidate this iterable.
+	 * The current users will receive an error thrown, as will all future users.
+	 *
+	 * **NOTE** If `reject()` have already been called, this method has no effect.
+	 */
+	reject(error: Error): void;
+}
+
+/**
+ * An executor for the `AsyncIterableObject` that has access to an emitter.
+ */
+export interface AsyncIterableExecutor<T> {
+	/**
+	 * @param emitter An object that allows to emit async values valid only for the duration of the executor.
+	 */
+	(emitter: AsyncIterableEmitter<T>): void | Promise<void>;
+}
+
+/**
+ * A rich implementation for an `AsyncIterable<T>`.
+ */
+export class AsyncIterableObject<T> implements AsyncIterable<T> {
+
+	public static fromArray<T>(items: T[]): AsyncIterableObject<T> {
+		return new AsyncIterableObject<T>((writer) => {
+			writer.emitMany(items);
+		});
+	}
+
+	public static fromPromise<T>(promise: Promise<T[]>): AsyncIterableObject<T> {
+		return new AsyncIterableObject<T>(async (emitter) => {
+			emitter.emitMany(await promise);
+		});
+	}
+
+	public static fromPromises<T>(promises: Promise<T>[]): AsyncIterableObject<T> {
+		return new AsyncIterableObject<T>(async (emitter) => {
+			await Promise.all(promises.map(async (p) => emitter.emitOne(await p)));
+		});
+	}
+
+	public static merge<T>(iterables: AsyncIterable<T>[]): AsyncIterableObject<T> {
+		return new AsyncIterableObject(async (emitter) => {
+			await Promise.all(iterables.map(async (iterable) => {
+				for await (const item of iterable) {
+					emitter.emitOne(item);
+				}
+			}));
+		});
+	}
+
+	public static EMPTY = AsyncIterableObject.fromArray<any>([]);
+
+	private _state: AsyncIterableSourceState;
+	private _results: T[];
+	private _error: Error | null;
+	private readonly _onReturn?: () => void | Promise<void>;
+	private readonly _onStateChanged: Emitter<void>;
+
+	constructor(executor: AsyncIterableExecutor<T>, onReturn?: () => void | Promise<void>) {
+		this._state = AsyncIterableSourceState.Initial;
+		this._results = [];
+		this._error = null;
+		this._onReturn = onReturn;
+		this._onStateChanged = new Emitter<void>();
+
+		queueMicrotask(async () => {
+			const writer: AsyncIterableEmitter<T> = {
+				emitOne: (item) => this.emitOne(item),
+				emitMany: (items) => this.emitMany(items),
+				reject: (error) => this.reject(error)
+			};
+			try {
+				await Promise.resolve(executor(writer));
+				this.resolve();
+			} catch (err) {
+				this.reject(err);
+			} finally {
+				writer.emitOne = undefined!;
+				writer.emitMany = undefined!;
+				writer.reject = undefined!;
+			}
+		});
+	}
+
+	[Symbol.asyncIterator](): AsyncIterator<T, undefined, undefined> {
+		let i = 0;
+		return {
+			next: async () => {
+				do {
+					if (this._state === AsyncIterableSourceState.DoneError) {
+						throw this._error;
+					}
+					if (i < this._results.length) {
+						return { done: false, value: this._results[i++] };
+					}
+					if (this._state === AsyncIterableSourceState.DoneOK) {
+						return { done: true, value: undefined };
+					}
+					await Event.toPromise(this._onStateChanged.event);
+				} while (true);
+			},
+			return: async () => {
+				this._onReturn?.();
+				return { done: true, value: undefined };
+			}
+		};
+	}
+
+	public static map<T, R>(iterable: AsyncIterable<T>, mapFn: (item: T) => R): AsyncIterableObject<R> {
+		return new AsyncIterableObject<R>(async (emitter) => {
+			for await (const item of iterable) {
+				emitter.emitOne(mapFn(item));
+			}
+		});
+	}
+
+	public map<R>(mapFn: (item: T) => R): AsyncIterableObject<R> {
+		return AsyncIterableObject.map(this, mapFn);
+	}
+
+	public static filter<T>(iterable: AsyncIterable<T>, filterFn: (item: T) => boolean): AsyncIterableObject<T> {
+		return new AsyncIterableObject<T>(async (emitter) => {
+			for await (const item of iterable) {
+				if (filterFn(item)) {
+					emitter.emitOne(item);
+				}
+			}
+		});
+	}
+
+	public filter(filterFn: (item: T) => boolean): AsyncIterableObject<T> {
+		return AsyncIterableObject.filter(this, filterFn);
+	}
+
+	public static coalesce<T>(iterable: AsyncIterable<T | undefined | null>): AsyncIterableObject<T> {
+		return <AsyncIterableObject<T>>AsyncIterableObject.filter(iterable, item => !!item);
+	}
+
+	public coalesce(): AsyncIterableObject<NonNullable<T>> {
+		return AsyncIterableObject.coalesce(this) as AsyncIterableObject<NonNullable<T>>;
+	}
+
+	public static async toPromise<T>(iterable: AsyncIterable<T>): Promise<T[]> {
+		const result: T[] = [];
+		for await (const item of iterable) {
+			result.push(item);
+		}
+		return result;
+	}
+
+	public toPromise(): Promise<T[]> {
+		return AsyncIterableObject.toPromise(this);
+	}
+
+	/**
+	 * The value will be appended at the end.
+	 *
+	 * **NOTE** If `resolve()` or `reject()` have already been called, this method has no effect.
+	 */
+	private emitOne(value: T): void {
+		if (this._state !== AsyncIterableSourceState.Initial) {
+			return;
+		}
+		// it is important to add new values at the end,
+		// as we may have iterators already running on the array
+		this._results.push(value);
+		this._onStateChanged.fire();
+	}
+
+	/**
+	 * The values will be appended at the end.
+	 *
+	 * **NOTE** If `resolve()` or `reject()` have already been called, this method has no effect.
+	 */
+	private emitMany(values: T[]): void {
+		if (this._state !== AsyncIterableSourceState.Initial) {
+			return;
+		}
+		// it is important to add new values at the end,
+		// as we may have iterators already running on the array
+		this._results = this._results.concat(values);
+		this._onStateChanged.fire();
+	}
+
+	/**
+	 * Calling `resolve()` will mark the result array as complete.
+	 *
+	 * **NOTE** `resolve()` must be called, otherwise all consumers of this iterable will hang indefinitely, similar to a non-resolved promise.
+	 * **NOTE** If `resolve()` or `reject()` have already been called, this method has no effect.
+	 */
+	private resolve(): void {
+		if (this._state !== AsyncIterableSourceState.Initial) {
+			return;
+		}
+		this._state = AsyncIterableSourceState.DoneOK;
+		this._onStateChanged.fire();
+	}
+
+	/**
+	 * Writing an error will permanently invalidate this iterable.
+	 * The current users will receive an error thrown, as will all future users.
+	 *
+	 * **NOTE** If `resolve()` or `reject()` have already been called, this method has no effect.
+	 */
+	private reject(error: Error) {
+		if (this._state !== AsyncIterableSourceState.Initial) {
+			return;
+		}
+		this._state = AsyncIterableSourceState.DoneError;
+		this._error = error;
+		this._onStateChanged.fire();
+	}
+}
+
+export class CancelableAsyncIterableObject<T> extends AsyncIterableObject<T> {
+	constructor(
+		private readonly _source: CancellationTokenSource,
+		executor: AsyncIterableExecutor<T>
+	) {
+		super(executor);
+	}
+
+	cancel(): void {
+		this._source.cancel();
+	}
+}
+
+export function createCancelableAsyncIterable<T>(callback: (token: CancellationToken) => AsyncIterable<T>): CancelableAsyncIterableObject<T> {
+	const source = new CancellationTokenSource();
+	const innerIterable = callback(source.token);
+
+	return new CancelableAsyncIterableObject<T>(source, async (emitter) => {
+		const subscription = source.token.onCancellationRequested(() => {
+			subscription.dispose();
+			source.dispose();
+			emitter.reject(new CancellationError());
+		});
+		try {
+			for await (const item of innerIterable) {
+				if (source.token.isCancellationRequested) {
+					// canceled in the meantime
+					return;
+				}
+				emitter.emitOne(item);
+			}
+			subscription.dispose();
+			source.dispose();
+		} catch (err) {
+			subscription.dispose();
+			source.dispose();
+			emitter.reject(err);
+		}
+	});
+}
+
+export class AsyncIterableSource<T> {
+
+	private readonly _deferred = new DeferredPromise<void>();
+	private readonly _asyncIterable: AsyncIterableObject<T>;
+
+	private _errorFn: (error: Error) => void;
+	private _emitFn: (item: T) => void;
+
+	/**
+	 *
+	 * @param onReturn A function that will be called when consuming the async iterable
+	 * has finished by the consumer, e.g the for-await-loop has be existed (break, return) early.
+	 * This is NOT called when resolving this source by its owner.
+	 */
+	constructor(onReturn?: () => Promise<void> | void) {
+		this._asyncIterable = new AsyncIterableObject(emitter => {
+
+			if (earlyError) {
+				emitter.reject(earlyError);
+				return;
+			}
+			if (earlyItems) {
+				emitter.emitMany(earlyItems);
+			}
+			this._errorFn = (error: Error) => emitter.reject(error);
+			this._emitFn = (item: T) => emitter.emitOne(item);
+			return this._deferred.p;
+		}, onReturn);
+
+		let earlyError: Error | undefined;
+		let earlyItems: T[] | undefined;
+
+		this._emitFn = (item: T) => {
+			if (!earlyItems) {
+				earlyItems = [];
+			}
+			earlyItems.push(item);
+		};
+		this._errorFn = (error: Error) => {
+			if (!earlyError) {
+				earlyError = error;
+			}
+		};
+	}
+
+	get asyncIterable(): AsyncIterableObject<T> {
+		return this._asyncIterable;
+	}
+
+	resolve(): void {
+		this._deferred.complete();
+	}
+
+	reject(error: Error): void {
+		this._errorFn(error);
+		this._deferred.complete();
+	}
+
+	emitOne(item: T): void {
+		this._emitFn(item);
+	}
+}
+
+//#endregion
diff --git a/src/vs/base/common/cancellation.ts b/src/vs/base/common/cancellation.ts
new file mode 100644
index 0000000000..4b45fd2803
--- /dev/null
+++ b/src/vs/base/common/cancellation.ts
@@ -0,0 +1,148 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { Emitter, Event } from 'vs/base/common/event';
+import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
+
+export interface CancellationToken {
+
+	/**
+	 * A flag signalling is cancellation has been requested.
+	 */
+	readonly isCancellationRequested: boolean;
+
+	/**
+	 * An event which fires when cancellation is requested. This event
+	 * only ever fires `once` as cancellation can only happen once. Listeners
+	 * that are registered after cancellation will be called (next event loop run),
+	 * but also only once.
+	 *
+	 * @event
+	 */
+	readonly onCancellationRequested: (listener: (e: any) => any, thisArgs?: any, disposables?: IDisposable[]) => IDisposable;
+}
+
+const shortcutEvent: Event<any> = Object.freeze(function (callback, context?): IDisposable {
+	const handle = setTimeout(callback.bind(context), 0);
+	return { dispose() { clearTimeout(handle); } };
+});
+
+export namespace CancellationToken {
+
+	export function isCancellationToken(thing: unknown): thing is CancellationToken {
+		if (thing === CancellationToken.None || thing === CancellationToken.Cancelled) {
+			return true;
+		}
+		if (thing instanceof MutableToken) {
+			return true;
+		}
+		if (!thing || typeof thing !== 'object') {
+			return false;
+		}
+		return typeof (thing as CancellationToken).isCancellationRequested === 'boolean'
+			&& typeof (thing as CancellationToken).onCancellationRequested === 'function';
+	}
+
+
+	export const None = Object.freeze<CancellationToken>({
+		isCancellationRequested: false,
+		onCancellationRequested: Event.None
+	});
+
+	export const Cancelled = Object.freeze<CancellationToken>({
+		isCancellationRequested: true,
+		onCancellationRequested: shortcutEvent
+	});
+}
+
+class MutableToken implements CancellationToken {
+
+	private _isCancelled: boolean = false;
+	private _emitter: Emitter<any> | null = null;
+
+	public cancel() {
+		if (!this._isCancelled) {
+			this._isCancelled = true;
+			if (this._emitter) {
+				this._emitter.fire(undefined);
+				this.dispose();
+			}
+		}
+	}
+
+	get isCancellationRequested(): boolean {
+		return this._isCancelled;
+	}
+
+	get onCancellationRequested(): Event<any> {
+		if (this._isCancelled) {
+			return shortcutEvent;
+		}
+		if (!this._emitter) {
+			this._emitter = new Emitter<any>();
+		}
+		return this._emitter.event;
+	}
+
+	public dispose(): void {
+		if (this._emitter) {
+			this._emitter.dispose();
+			this._emitter = null;
+		}
+	}
+}
+
+export class CancellationTokenSource {
+
+	private _token?: CancellationToken = undefined;
+	private _parentListener?: IDisposable = undefined;
+
+	constructor(parent?: CancellationToken) {
+		this._parentListener = parent && parent.onCancellationRequested(this.cancel, this);
+	}
+
+	get token(): CancellationToken {
+		if (!this._token) {
+			// be lazy and create the token only when
+			// actually needed
+			this._token = new MutableToken();
+		}
+		return this._token;
+	}
+
+	cancel(): void {
+		if (!this._token) {
+			// save an object by returning the default
+			// cancelled token when cancellation happens
+			// before someone asks for the token
+			this._token = CancellationToken.Cancelled;
+
+		} else if (this._token instanceof MutableToken) {
+			// actually cancel
+			this._token.cancel();
+		}
+	}
+
+	dispose(cancel: boolean = false): void {
+		if (cancel) {
+			this.cancel();
+		}
+		this._parentListener?.dispose();
+		if (!this._token) {
+			// ensure to initialize with an empty token if we had none
+			this._token = CancellationToken.None;
+
+		} else if (this._token instanceof MutableToken) {
+			// actually dispose
+			this._token.dispose();
+		}
+	}
+}
+
+export function cancelOnDispose(store: DisposableStore): CancellationToken {
+	const source = new CancellationTokenSource();
+	store.add({ dispose() { source.cancel(); } });
+	return source.token;
+}
diff --git a/src/vs/base/common/charCode.ts b/src/vs/base/common/charCode.ts
new file mode 100644
index 0000000000..de0a25bade
--- /dev/null
+++ b/src/vs/base/common/charCode.ts
@@ -0,0 +1,450 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+// Names from https://blog.codinghorror.com/ascii-pronunciation-rules-for-programmers/
+
+/**
+ * An inlined enum containing useful character codes (to be used with String.charCodeAt).
+ * Please leave the const keyword such that it gets inlined when compiled to JavaScript!
+ */
+export const enum CharCode {
+	Null = 0,
+	/**
+	 * The `\b` character.
+	 */
+	Backspace = 8,
+	/**
+	 * The `\t` character.
+	 */
+	Tab = 9,
+	/**
+	 * The `\n` character.
+	 */
+	LineFeed = 10,
+	/**
+	 * The `\r` character.
+	 */
+	CarriageReturn = 13,
+	Space = 32,
+	/**
+	 * The `!` character.
+	 */
+	ExclamationMark = 33,
+	/**
+	 * The `"` character.
+	 */
+	DoubleQuote = 34,
+	/**
+	 * The `#` character.
+	 */
+	Hash = 35,
+	/**
+	 * The `$` character.
+	 */
+	DollarSign = 36,
+	/**
+	 * The `%` character.
+	 */
+	PercentSign = 37,
+	/**
+	 * The `&` character.
+	 */
+	Ampersand = 38,
+	/**
+	 * The `'` character.
+	 */
+	SingleQuote = 39,
+	/**
+	 * The `(` character.
+	 */
+	OpenParen = 40,
+	/**
+	 * The `)` character.
+	 */
+	CloseParen = 41,
+	/**
+	 * The `*` character.
+	 */
+	Asterisk = 42,
+	/**
+	 * The `+` character.
+	 */
+	Plus = 43,
+	/**
+	 * The `,` character.
+	 */
+	Comma = 44,
+	/**
+	 * The `-` character.
+	 */
+	Dash = 45,
+	/**
+	 * The `.` character.
+	 */
+	Period = 46,
+	/**
+	 * The `/` character.
+	 */
+	Slash = 47,
+
+	Digit0 = 48,
+	Digit1 = 49,
+	Digit2 = 50,
+	Digit3 = 51,
+	Digit4 = 52,
+	Digit5 = 53,
+	Digit6 = 54,
+	Digit7 = 55,
+	Digit8 = 56,
+	Digit9 = 57,
+
+	/**
+	 * The `:` character.
+	 */
+	Colon = 58,
+	/**
+	 * The `;` character.
+	 */
+	Semicolon = 59,
+	/**
+	 * The `<` character.
+	 */
+	LessThan = 60,
+	/**
+	 * The `=` character.
+	 */
+	Equals = 61,
+	/**
+	 * The `>` character.
+	 */
+	GreaterThan = 62,
+	/**
+	 * The `?` character.
+	 */
+	QuestionMark = 63,
+	/**
+	 * The `@` character.
+	 */
+	AtSign = 64,
+
+	A = 65,
+	B = 66,
+	C = 67,
+	D = 68,
+	E = 69,
+	F = 70,
+	G = 71,
+	H = 72,
+	I = 73,
+	J = 74,
+	K = 75,
+	L = 76,
+	M = 77,
+	N = 78,
+	O = 79,
+	P = 80,
+	Q = 81,
+	R = 82,
+	S = 83,
+	T = 84,
+	U = 85,
+	V = 86,
+	W = 87,
+	X = 88,
+	Y = 89,
+	Z = 90,
+
+	/**
+	 * The `[` character.
+	 */
+	OpenSquareBracket = 91,
+	/**
+	 * The `\` character.
+	 */
+	Backslash = 92,
+	/**
+	 * The `]` character.
+	 */
+	CloseSquareBracket = 93,
+	/**
+	 * The `^` character.
+	 */
+	Caret = 94,
+	/**
+	 * The `_` character.
+	 */
+	Underline = 95,
+	/**
+	 * The ``(`)`` character.
+	 */
+	BackTick = 96,
+
+	a = 97,
+	b = 98,
+	c = 99,
+	d = 100,
+	e = 101,
+	f = 102,
+	g = 103,
+	h = 104,
+	i = 105,
+	j = 106,
+	k = 107,
+	l = 108,
+	m = 109,
+	n = 110,
+	o = 111,
+	p = 112,
+	q = 113,
+	r = 114,
+	s = 115,
+	t = 116,
+	u = 117,
+	v = 118,
+	w = 119,
+	x = 120,
+	y = 121,
+	z = 122,
+
+	/**
+	 * The `{` character.
+	 */
+	OpenCurlyBrace = 123,
+	/**
+	 * The `|` character.
+	 */
+	Pipe = 124,
+	/**
+	 * The `}` character.
+	 */
+	CloseCurlyBrace = 125,
+	/**
+	 * The `~` character.
+	 */
+	Tilde = 126,
+
+	/**
+	 * The &nbsp; (no-break space) character.
+	 * Unicode Character 'NO-BREAK SPACE' (U+00A0)
+	 */
+	NoBreakSpace = 160,
+
+	U_Combining_Grave_Accent = 0x0300,								//	U+0300	Combining Grave Accent
+	U_Combining_Acute_Accent = 0x0301,								//	U+0301	Combining Acute Accent
+	U_Combining_Circumflex_Accent = 0x0302,							//	U+0302	Combining Circumflex Accent
+	U_Combining_Tilde = 0x0303,										//	U+0303	Combining Tilde
+	U_Combining_Macron = 0x0304,									//	U+0304	Combining Macron
+	U_Combining_Overline = 0x0305,									//	U+0305	Combining Overline
+	U_Combining_Breve = 0x0306,										//	U+0306	Combining Breve
+	U_Combining_Dot_Above = 0x0307,									//	U+0307	Combining Dot Above
+	U_Combining_Diaeresis = 0x0308,									//	U+0308	Combining Diaeresis
+	U_Combining_Hook_Above = 0x0309,								//	U+0309	Combining Hook Above
+	U_Combining_Ring_Above = 0x030A,								//	U+030A	Combining Ring Above
+	U_Combining_Double_Acute_Accent = 0x030B,						//	U+030B	Combining Double Acute Accent
+	U_Combining_Caron = 0x030C,										//	U+030C	Combining Caron
+	U_Combining_Vertical_Line_Above = 0x030D,						//	U+030D	Combining Vertical Line Above
+	U_Combining_Double_Vertical_Line_Above = 0x030E,				//	U+030E	Combining Double Vertical Line Above
+	U_Combining_Double_Grave_Accent = 0x030F,						//	U+030F	Combining Double Grave Accent
+	U_Combining_Candrabindu = 0x0310,								//	U+0310	Combining Candrabindu
+	U_Combining_Inverted_Breve = 0x0311,							//	U+0311	Combining Inverted Breve
+	U_Combining_Turned_Comma_Above = 0x0312,						//	U+0312	Combining Turned Comma Above
+	U_Combining_Comma_Above = 0x0313,								//	U+0313	Combining Comma Above
+	U_Combining_Reversed_Comma_Above = 0x0314,						//	U+0314	Combining Reversed Comma Above
+	U_Combining_Comma_Above_Right = 0x0315,							//	U+0315	Combining Comma Above Right
+	U_Combining_Grave_Accent_Below = 0x0316,						//	U+0316	Combining Grave Accent Below
+	U_Combining_Acute_Accent_Below = 0x0317,						//	U+0317	Combining Acute Accent Below
+	U_Combining_Left_Tack_Below = 0x0318,							//	U+0318	Combining Left Tack Below
+	U_Combining_Right_Tack_Below = 0x0319,							//	U+0319	Combining Right Tack Below
+	U_Combining_Left_Angle_Above = 0x031A,							//	U+031A	Combining Left Angle Above
+	U_Combining_Horn = 0x031B,										//	U+031B	Combining Horn
+	U_Combining_Left_Half_Ring_Below = 0x031C,						//	U+031C	Combining Left Half Ring Below
+	U_Combining_Up_Tack_Below = 0x031D,								//	U+031D	Combining Up Tack Below
+	U_Combining_Down_Tack_Below = 0x031E,							//	U+031E	Combining Down Tack Below
+	U_Combining_Plus_Sign_Below = 0x031F,							//	U+031F	Combining Plus Sign Below
+	U_Combining_Minus_Sign_Below = 0x0320,							//	U+0320	Combining Minus Sign Below
+	U_Combining_Palatalized_Hook_Below = 0x0321,					//	U+0321	Combining Palatalized Hook Below
+	U_Combining_Retroflex_Hook_Below = 0x0322,						//	U+0322	Combining Retroflex Hook Below
+	U_Combining_Dot_Below = 0x0323,									//	U+0323	Combining Dot Below
+	U_Combining_Diaeresis_Below = 0x0324,							//	U+0324	Combining Diaeresis Below
+	U_Combining_Ring_Below = 0x0325,								//	U+0325	Combining Ring Below
+	U_Combining_Comma_Below = 0x0326,								//	U+0326	Combining Comma Below
+	U_Combining_Cedilla = 0x0327,									//	U+0327	Combining Cedilla
+	U_Combining_Ogonek = 0x0328,									//	U+0328	Combining Ogonek
+	U_Combining_Vertical_Line_Below = 0x0329,						//	U+0329	Combining Vertical Line Below
+	U_Combining_Bridge_Below = 0x032A,								//	U+032A	Combining Bridge Below
+	U_Combining_Inverted_Double_Arch_Below = 0x032B,				//	U+032B	Combining Inverted Double Arch Below
+	U_Combining_Caron_Below = 0x032C,								//	U+032C	Combining Caron Below
+	U_Combining_Circumflex_Accent_Below = 0x032D,					//	U+032D	Combining Circumflex Accent Below
+	U_Combining_Breve_Below = 0x032E,								//	U+032E	Combining Breve Below
+	U_Combining_Inverted_Breve_Below = 0x032F,						//	U+032F	Combining Inverted Breve Below
+	U_Combining_Tilde_Below = 0x0330,								//	U+0330	Combining Tilde Below
+	U_Combining_Macron_Below = 0x0331,								//	U+0331	Combining Macron Below
+	U_Combining_Low_Line = 0x0332,									//	U+0332	Combining Low Line
+	U_Combining_Double_Low_Line = 0x0333,							//	U+0333	Combining Double Low Line
+	U_Combining_Tilde_Overlay = 0x0334,								//	U+0334	Combining Tilde Overlay
+	U_Combining_Short_Stroke_Overlay = 0x0335,						//	U+0335	Combining Short Stroke Overlay
+	U_Combining_Long_Stroke_Overlay = 0x0336,						//	U+0336	Combining Long Stroke Overlay
+	U_Combining_Short_Solidus_Overlay = 0x0337,						//	U+0337	Combining Short Solidus Overlay
+	U_Combining_Long_Solidus_Overlay = 0x0338,						//	U+0338	Combining Long Solidus Overlay
+	U_Combining_Right_Half_Ring_Below = 0x0339,						//	U+0339	Combining Right Half Ring Below
+	U_Combining_Inverted_Bridge_Below = 0x033A,						//	U+033A	Combining Inverted Bridge Below
+	U_Combining_Square_Below = 0x033B,								//	U+033B	Combining Square Below
+	U_Combining_Seagull_Below = 0x033C,								//	U+033C	Combining Seagull Below
+	U_Combining_X_Above = 0x033D,									//	U+033D	Combining X Above
+	U_Combining_Vertical_Tilde = 0x033E,							//	U+033E	Combining Vertical Tilde
+	U_Combining_Double_Overline = 0x033F,							//	U+033F	Combining Double Overline
+	U_Combining_Grave_Tone_Mark = 0x0340,							//	U+0340	Combining Grave Tone Mark
+	U_Combining_Acute_Tone_Mark = 0x0341,							//	U+0341	Combining Acute Tone Mark
+	U_Combining_Greek_Perispomeni = 0x0342,							//	U+0342	Combining Greek Perispomeni
+	U_Combining_Greek_Koronis = 0x0343,								//	U+0343	Combining Greek Koronis
+	U_Combining_Greek_Dialytika_Tonos = 0x0344,						//	U+0344	Combining Greek Dialytika Tonos
+	U_Combining_Greek_Ypogegrammeni = 0x0345,						//	U+0345	Combining Greek Ypogegrammeni
+	U_Combining_Bridge_Above = 0x0346,								//	U+0346	Combining Bridge Above
+	U_Combining_Equals_Sign_Below = 0x0347,							//	U+0347	Combining Equals Sign Below
+	U_Combining_Double_Vertical_Line_Below = 0x0348,				//	U+0348	Combining Double Vertical Line Below
+	U_Combining_Left_Angle_Below = 0x0349,							//	U+0349	Combining Left Angle Below
+	U_Combining_Not_Tilde_Above = 0x034A,							//	U+034A	Combining Not Tilde Above
+	U_Combining_Homothetic_Above = 0x034B,							//	U+034B	Combining Homothetic Above
+	U_Combining_Almost_Equal_To_Above = 0x034C,						//	U+034C	Combining Almost Equal To Above
+	U_Combining_Left_Right_Arrow_Below = 0x034D,					//	U+034D	Combining Left Right Arrow Below
+	U_Combining_Upwards_Arrow_Below = 0x034E,						//	U+034E	Combining Upwards Arrow Below
+	U_Combining_Grapheme_Joiner = 0x034F,							//	U+034F	Combining Grapheme Joiner
+	U_Combining_Right_Arrowhead_Above = 0x0350,						//	U+0350	Combining Right Arrowhead Above
+	U_Combining_Left_Half_Ring_Above = 0x0351,						//	U+0351	Combining Left Half Ring Above
+	U_Combining_Fermata = 0x0352,									//	U+0352	Combining Fermata
+	U_Combining_X_Below = 0x0353,									//	U+0353	Combining X Below
+	U_Combining_Left_Arrowhead_Below = 0x0354,						//	U+0354	Combining Left Arrowhead Below
+	U_Combining_Right_Arrowhead_Below = 0x0355,						//	U+0355	Combining Right Arrowhead Below
+	U_Combining_Right_Arrowhead_And_Up_Arrowhead_Below = 0x0356,	//	U+0356	Combining Right Arrowhead And Up Arrowhead Below
+	U_Combining_Right_Half_Ring_Above = 0x0357,						//	U+0357	Combining Right Half Ring Above
+	U_Combining_Dot_Above_Right = 0x0358,							//	U+0358	Combining Dot Above Right
+	U_Combining_Asterisk_Below = 0x0359,							//	U+0359	Combining Asterisk Below
+	U_Combining_Double_Ring_Below = 0x035A,							//	U+035A	Combining Double Ring Below
+	U_Combining_Zigzag_Above = 0x035B,								//	U+035B	Combining Zigzag Above
+	U_Combining_Double_Breve_Below = 0x035C,						//	U+035C	Combining Double Breve Below
+	U_Combining_Double_Breve = 0x035D,								//	U+035D	Combining Double Breve
+	U_Combining_Double_Macron = 0x035E,								//	U+035E	Combining Double Macron
+	U_Combining_Double_Macron_Below = 0x035F,						//	U+035F	Combining Double Macron Below
+	U_Combining_Double_Tilde = 0x0360,								//	U+0360	Combining Double Tilde
+	U_Combining_Double_Inverted_Breve = 0x0361,						//	U+0361	Combining Double Inverted Breve
+	U_Combining_Double_Rightwards_Arrow_Below = 0x0362,				//	U+0362	Combining Double Rightwards Arrow Below
+	U_Combining_Latin_Small_Letter_A = 0x0363, 						//	U+0363	Combining Latin Small Letter A
+	U_Combining_Latin_Small_Letter_E = 0x0364, 						//	U+0364	Combining Latin Small Letter E
+	U_Combining_Latin_Small_Letter_I = 0x0365, 						//	U+0365	Combining Latin Small Letter I
+	U_Combining_Latin_Small_Letter_O = 0x0366, 						//	U+0366	Combining Latin Small Letter O
+	U_Combining_Latin_Small_Letter_U = 0x0367, 						//	U+0367	Combining Latin Small Letter U
+	U_Combining_Latin_Small_Letter_C = 0x0368, 						//	U+0368	Combining Latin Small Letter C
+	U_Combining_Latin_Small_Letter_D = 0x0369, 						//	U+0369	Combining Latin Small Letter D
+	U_Combining_Latin_Small_Letter_H = 0x036A, 						//	U+036A	Combining Latin Small Letter H
+	U_Combining_Latin_Small_Letter_M = 0x036B, 						//	U+036B	Combining Latin Small Letter M
+	U_Combining_Latin_Small_Letter_R = 0x036C, 						//	U+036C	Combining Latin Small Letter R
+	U_Combining_Latin_Small_Letter_T = 0x036D, 						//	U+036D	Combining Latin Small Letter T
+	U_Combining_Latin_Small_Letter_V = 0x036E, 						//	U+036E	Combining Latin Small Letter V
+	U_Combining_Latin_Small_Letter_X = 0x036F, 						//	U+036F	Combining Latin Small Letter X
+
+	/**
+	 * Unicode Character 'LINE SEPARATOR' (U+2028)
+	 * http://www.fileformat.info/info/unicode/char/2028/index.htm
+	 */
+	LINE_SEPARATOR = 0x2028,
+	/**
+	 * Unicode Character 'PARAGRAPH SEPARATOR' (U+2029)
+	 * http://www.fileformat.info/info/unicode/char/2029/index.htm
+	 */
+	PARAGRAPH_SEPARATOR = 0x2029,
+	/**
+	 * Unicode Character 'NEXT LINE' (U+0085)
+	 * http://www.fileformat.info/info/unicode/char/0085/index.htm
+	 */
+	NEXT_LINE = 0x0085,
+
+	// http://www.fileformat.info/info/unicode/category/Sk/list.htm
+	U_CIRCUMFLEX = 0x005E,									// U+005E	CIRCUMFLEX
+	U_GRAVE_ACCENT = 0x0060,								// U+0060	GRAVE ACCENT
+	U_DIAERESIS = 0x00A8,									// U+00A8	DIAERESIS
+	U_MACRON = 0x00AF,										// U+00AF	MACRON
+	U_ACUTE_ACCENT = 0x00B4,								// U+00B4	ACUTE ACCENT
+	U_CEDILLA = 0x00B8,										// U+00B8	CEDILLA
+	U_MODIFIER_LETTER_LEFT_ARROWHEAD = 0x02C2,				// U+02C2	MODIFIER LETTER LEFT ARROWHEAD
+	U_MODIFIER_LETTER_RIGHT_ARROWHEAD = 0x02C3,				// U+02C3	MODIFIER LETTER RIGHT ARROWHEAD
+	U_MODIFIER_LETTER_UP_ARROWHEAD = 0x02C4,				// U+02C4	MODIFIER LETTER UP ARROWHEAD
+	U_MODIFIER_LETTER_DOWN_ARROWHEAD = 0x02C5,				// U+02C5	MODIFIER LETTER DOWN ARROWHEAD
+	U_MODIFIER_LETTER_CENTRED_RIGHT_HALF_RING = 0x02D2,		// U+02D2	MODIFIER LETTER CENTRED RIGHT HALF RING
+	U_MODIFIER_LETTER_CENTRED_LEFT_HALF_RING = 0x02D3,		// U+02D3	MODIFIER LETTER CENTRED LEFT HALF RING
+	U_MODIFIER_LETTER_UP_TACK = 0x02D4,						// U+02D4	MODIFIER LETTER UP TACK
+	U_MODIFIER_LETTER_DOWN_TACK = 0x02D5,					// U+02D5	MODIFIER LETTER DOWN TACK
+	U_MODIFIER_LETTER_PLUS_SIGN = 0x02D6,					// U+02D6	MODIFIER LETTER PLUS SIGN
+	U_MODIFIER_LETTER_MINUS_SIGN = 0x02D7,					// U+02D7	MODIFIER LETTER MINUS SIGN
+	U_BREVE = 0x02D8,										// U+02D8	BREVE
+	U_DOT_ABOVE = 0x02D9,									// U+02D9	DOT ABOVE
+	U_RING_ABOVE = 0x02DA,									// U+02DA	RING ABOVE
+	U_OGONEK = 0x02DB,										// U+02DB	OGONEK
+	U_SMALL_TILDE = 0x02DC,									// U+02DC	SMALL TILDE
+	U_DOUBLE_ACUTE_ACCENT = 0x02DD,							// U+02DD	DOUBLE ACUTE ACCENT
+	U_MODIFIER_LETTER_RHOTIC_HOOK = 0x02DE,					// U+02DE	MODIFIER LETTER RHOTIC HOOK
+	U_MODIFIER_LETTER_CROSS_ACCENT = 0x02DF,				// U+02DF	MODIFIER LETTER CROSS ACCENT
+	U_MODIFIER_LETTER_EXTRA_HIGH_TONE_BAR = 0x02E5,			// U+02E5	MODIFIER LETTER EXTRA-HIGH TONE BAR
+	U_MODIFIER_LETTER_HIGH_TONE_BAR = 0x02E6,				// U+02E6	MODIFIER LETTER HIGH TONE BAR
+	U_MODIFIER_LETTER_MID_TONE_BAR = 0x02E7,				// U+02E7	MODIFIER LETTER MID TONE BAR
+	U_MODIFIER_LETTER_LOW_TONE_BAR = 0x02E8,				// U+02E8	MODIFIER LETTER LOW TONE BAR
+	U_MODIFIER_LETTER_EXTRA_LOW_TONE_BAR = 0x02E9,			// U+02E9	MODIFIER LETTER EXTRA-LOW TONE BAR
+	U_MODIFIER_LETTER_YIN_DEPARTING_TONE_MARK = 0x02EA,		// U+02EA	MODIFIER LETTER YIN DEPARTING TONE MARK
+	U_MODIFIER_LETTER_YANG_DEPARTING_TONE_MARK = 0x02EB,	// U+02EB	MODIFIER LETTER YANG DEPARTING TONE MARK
+	U_MODIFIER_LETTER_UNASPIRATED = 0x02ED,					// U+02ED	MODIFIER LETTER UNASPIRATED
+	U_MODIFIER_LETTER_LOW_DOWN_ARROWHEAD = 0x02EF,			// U+02EF	MODIFIER LETTER LOW DOWN ARROWHEAD
+	U_MODIFIER_LETTER_LOW_UP_ARROWHEAD = 0x02F0,			// U+02F0	MODIFIER LETTER LOW UP ARROWHEAD
+	U_MODIFIER_LETTER_LOW_LEFT_ARROWHEAD = 0x02F1,			// U+02F1	MODIFIER LETTER LOW LEFT ARROWHEAD
+	U_MODIFIER_LETTER_LOW_RIGHT_ARROWHEAD = 0x02F2,			// U+02F2	MODIFIER LETTER LOW RIGHT ARROWHEAD
+	U_MODIFIER_LETTER_LOW_RING = 0x02F3,					// U+02F3	MODIFIER LETTER LOW RING
+	U_MODIFIER_LETTER_MIDDLE_GRAVE_ACCENT = 0x02F4,			// U+02F4	MODIFIER LETTER MIDDLE GRAVE ACCENT
+	U_MODIFIER_LETTER_MIDDLE_DOUBLE_GRAVE_ACCENT = 0x02F5,	// U+02F5	MODIFIER LETTER MIDDLE DOUBLE GRAVE ACCENT
+	U_MODIFIER_LETTER_MIDDLE_DOUBLE_ACUTE_ACCENT = 0x02F6,	// U+02F6	MODIFIER LETTER MIDDLE DOUBLE ACUTE ACCENT
+	U_MODIFIER_LETTER_LOW_TILDE = 0x02F7,					// U+02F7	MODIFIER LETTER LOW TILDE
+	U_MODIFIER_LETTER_RAISED_COLON = 0x02F8,				// U+02F8	MODIFIER LETTER RAISED COLON
+	U_MODIFIER_LETTER_BEGIN_HIGH_TONE = 0x02F9,				// U+02F9	MODIFIER LETTER BEGIN HIGH TONE
+	U_MODIFIER_LETTER_END_HIGH_TONE = 0x02FA,				// U+02FA	MODIFIER LETTER END HIGH TONE
+	U_MODIFIER_LETTER_BEGIN_LOW_TONE = 0x02FB,				// U+02FB	MODIFIER LETTER BEGIN LOW TONE
+	U_MODIFIER_LETTER_END_LOW_TONE = 0x02FC,				// U+02FC	MODIFIER LETTER END LOW TONE
+	U_MODIFIER_LETTER_SHELF = 0x02FD,						// U+02FD	MODIFIER LETTER SHELF
+	U_MODIFIER_LETTER_OPEN_SHELF = 0x02FE,					// U+02FE	MODIFIER LETTER OPEN SHELF
+	U_MODIFIER_LETTER_LOW_LEFT_ARROW = 0x02FF,				// U+02FF	MODIFIER LETTER LOW LEFT ARROW
+	U_GREEK_LOWER_NUMERAL_SIGN = 0x0375,					// U+0375	GREEK LOWER NUMERAL SIGN
+	U_GREEK_TONOS = 0x0384,									// U+0384	GREEK TONOS
+	U_GREEK_DIALYTIKA_TONOS = 0x0385,						// U+0385	GREEK DIALYTIKA TONOS
+	U_GREEK_KORONIS = 0x1FBD,								// U+1FBD	GREEK KORONIS
+	U_GREEK_PSILI = 0x1FBF,									// U+1FBF	GREEK PSILI
+	U_GREEK_PERISPOMENI = 0x1FC0,							// U+1FC0	GREEK PERISPOMENI
+	U_GREEK_DIALYTIKA_AND_PERISPOMENI = 0x1FC1,				// U+1FC1	GREEK DIALYTIKA AND PERISPOMENI
+	U_GREEK_PSILI_AND_VARIA = 0x1FCD,						// U+1FCD	GREEK PSILI AND VARIA
+	U_GREEK_PSILI_AND_OXIA = 0x1FCE,						// U+1FCE	GREEK PSILI AND OXIA
+	U_GREEK_PSILI_AND_PERISPOMENI = 0x1FCF,					// U+1FCF	GREEK PSILI AND PERISPOMENI
+	U_GREEK_DASIA_AND_VARIA = 0x1FDD,						// U+1FDD	GREEK DASIA AND VARIA
+	U_GREEK_DASIA_AND_OXIA = 0x1FDE,						// U+1FDE	GREEK DASIA AND OXIA
+	U_GREEK_DASIA_AND_PERISPOMENI = 0x1FDF,					// U+1FDF	GREEK DASIA AND PERISPOMENI
+	U_GREEK_DIALYTIKA_AND_VARIA = 0x1FED,					// U+1FED	GREEK DIALYTIKA AND VARIA
+	U_GREEK_DIALYTIKA_AND_OXIA = 0x1FEE,					// U+1FEE	GREEK DIALYTIKA AND OXIA
+	U_GREEK_VARIA = 0x1FEF,									// U+1FEF	GREEK VARIA
+	U_GREEK_OXIA = 0x1FFD,									// U+1FFD	GREEK OXIA
+	U_GREEK_DASIA = 0x1FFE,									// U+1FFE	GREEK DASIA
+
+	U_IDEOGRAPHIC_FULL_STOP = 0x3002,						// U+3002	IDEOGRAPHIC FULL STOP
+	U_LEFT_CORNER_BRACKET = 0x300C,							// U+300C	LEFT CORNER BRACKET
+	U_RIGHT_CORNER_BRACKET = 0x300D,						// U+300D	RIGHT CORNER BRACKET
+	U_LEFT_BLACK_LENTICULAR_BRACKET = 0x3010,				// U+3010	LEFT BLACK LENTICULAR BRACKET
+	U_RIGHT_BLACK_LENTICULAR_BRACKET = 0x3011,				// U+3011	RIGHT BLACK LENTICULAR BRACKET
+
+
+	U_OVERLINE = 0x203E, // Unicode Character 'OVERLINE'
+
+	/**
+	 * UTF-8 BOM
+	 * Unicode Character 'ZERO WIDTH NO-BREAK SPACE' (U+FEFF)
+	 * http://www.fileformat.info/info/unicode/char/feff/index.htm
+	 */
+	UTF8_BOM = 65279,
+
+	U_FULLWIDTH_SEMICOLON = 0xFF1B,							// U+FF1B	FULLWIDTH SEMICOLON
+	U_FULLWIDTH_COMMA = 0xFF0C,								// U+FF0C	FULLWIDTH COMMA
+}
diff --git a/src/vs/base/common/collections.ts b/src/vs/base/common/collections.ts
new file mode 100644
index 0000000000..d0df190c75
--- /dev/null
+++ b/src/vs/base/common/collections.ts
@@ -0,0 +1,140 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+/**
+ * An interface for a JavaScript object that
+ * acts a dictionary. The keys are strings.
+ */
+export type IStringDictionary<V> = Record<string, V>;
+
+/**
+ * An interface for a JavaScript object that
+ * acts a dictionary. The keys are numbers.
+ */
+export type INumberDictionary<V> = Record<number, V>;
+
+/**
+ * Groups the collection into a dictionary based on the provided
+ * group function.
+ */
+export function groupBy<K extends string | number | symbol, V>(data: V[], groupFn: (element: V) => K): Record<K, V[]> {
+	const result: Record<K, V[]> = Object.create(null);
+	for (const element of data) {
+		const key = groupFn(element);
+		let target = result[key];
+		if (!target) {
+			target = result[key] = [];
+		}
+		target.push(element);
+	}
+	return result;
+}
+
+export function diffSets<T>(before: Set<T>, after: Set<T>): { removed: T[]; added: T[] } {
+	const removed: T[] = [];
+	const added: T[] = [];
+	for (const element of before) {
+		if (!after.has(element)) {
+			removed.push(element);
+		}
+	}
+	for (const element of after) {
+		if (!before.has(element)) {
+			added.push(element);
+		}
+	}
+	return { removed, added };
+}
+
+export function diffMaps<K, V>(before: Map<K, V>, after: Map<K, V>): { removed: V[]; added: V[] } {
+	const removed: V[] = [];
+	const added: V[] = [];
+	for (const [index, value] of before) {
+		if (!after.has(index)) {
+			removed.push(value);
+		}
+	}
+	for (const [index, value] of after) {
+		if (!before.has(index)) {
+			added.push(value);
+		}
+	}
+	return { removed, added };
+}
+
+/**
+ * Computes the intersection of two sets.
+ *
+ * @param setA - The first set.
+ * @param setB - The second iterable.
+ * @returns A new set containing the elements that are in both `setA` and `setB`.
+ */
+export function intersection<T>(setA: Set<T>, setB: Iterable<T>): Set<T> {
+	const result = new Set<T>();
+	for (const elem of setB) {
+		if (setA.has(elem)) {
+			result.add(elem);
+		}
+	}
+	return result;
+}
+
+export class SetWithKey<T> implements Set<T> {
+	private _map = new Map<any, T>();
+
+	constructor(values: T[], private toKey: (t: T) => any) {
+		for (const value of values) {
+			this.add(value);
+		}
+	}
+
+	get size(): number {
+		return this._map.size;
+	}
+
+	add(value: T): this {
+		const key = this.toKey(value);
+		this._map.set(key, value);
+		return this;
+	}
+
+	delete(value: T): boolean {
+		return this._map.delete(this.toKey(value));
+	}
+
+	has(value: T): boolean {
+		return this._map.has(this.toKey(value));
+	}
+
+	*entries(): IterableIterator<[T, T]> {
+		for (const entry of this._map.values()) {
+			yield [entry, entry];
+		}
+	}
+
+	keys(): IterableIterator<T> {
+		return this.values();
+	}
+
+	*values(): IterableIterator<T> {
+		for (const entry of this._map.values()) {
+			yield entry;
+		}
+	}
+
+	clear(): void {
+		this._map.clear();
+	}
+
+	forEach(callbackfn: (value: T, value2: T, set: Set<T>) => void, thisArg?: any): void {
+		this._map.forEach(entry => callbackfn.call(thisArg, entry, entry, this));
+	}
+
+	[Symbol.iterator](): IterableIterator<T> {
+		return this.values();
+	}
+
+	[Symbol.toStringTag]: string = 'SetWithKey';
+}
diff --git a/src/vs/base/common/decorators.ts b/src/vs/base/common/decorators.ts
new file mode 100644
index 0000000000..34592a7b81
--- /dev/null
+++ b/src/vs/base/common/decorators.ts
@@ -0,0 +1,130 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+function createDecorator(mapFn: (fn: Function, key: string) => Function): Function {
+	return (target: any, key: string, descriptor: any) => {
+		let fnKey: string | null = null;
+		let fn: Function | null = null;
+
+		if (typeof descriptor.value === 'function') {
+			fnKey = 'value';
+			fn = descriptor.value;
+		} else if (typeof descriptor.get === 'function') {
+			fnKey = 'get';
+			fn = descriptor.get;
+		}
+
+		if (!fn) {
+			throw new Error('not supported');
+		}
+
+		descriptor[fnKey!] = mapFn(fn, key);
+	};
+}
+
+export function memoize(_target: any, key: string, descriptor: any) {
+	let fnKey: string | null = null;
+	let fn: Function | null = null;
+
+	if (typeof descriptor.value === 'function') {
+		fnKey = 'value';
+		fn = descriptor.value;
+
+		if (fn!.length !== 0) {
+			console.warn('Memoize should only be used in functions with zero parameters');
+		}
+	} else if (typeof descriptor.get === 'function') {
+		fnKey = 'get';
+		fn = descriptor.get;
+	}
+
+	if (!fn) {
+		throw new Error('not supported');
+	}
+
+	const memoizeKey = `$memoize$${key}`;
+	descriptor[fnKey!] = function (...args: any[]) {
+		if (!this.hasOwnProperty(memoizeKey)) {
+			Object.defineProperty(this, memoizeKey, {
+				configurable: false,
+				enumerable: false,
+				writable: false,
+				value: fn.apply(this, args)
+			});
+		}
+
+		return this[memoizeKey];
+	};
+}
+
+export interface IDebounceReducer<T> {
+	(previousValue: T, ...args: any[]): T;
+}
+
+export function debounce<T>(delay: number, reducer?: IDebounceReducer<T>, initialValueProvider?: () => T): Function {
+	return createDecorator((fn, key) => {
+		const timerKey = `$debounce$${key}`;
+		const resultKey = `$debounce$result$${key}`;
+
+		return function (this: any, ...args: any[]) {
+			if (!this[resultKey]) {
+				this[resultKey] = initialValueProvider ? initialValueProvider() : undefined;
+			}
+
+			clearTimeout(this[timerKey]);
+
+			if (reducer) {
+				this[resultKey] = reducer(this[resultKey], ...args);
+				args = [this[resultKey]];
+			}
+
+			this[timerKey] = setTimeout(() => {
+				fn.apply(this, args);
+				this[resultKey] = initialValueProvider ? initialValueProvider() : undefined;
+			}, delay);
+		};
+	});
+}
+
+export function throttle<T>(delay: number, reducer?: IDebounceReducer<T>, initialValueProvider?: () => T): Function {
+	return createDecorator((fn, key) => {
+		const timerKey = `$throttle$timer$${key}`;
+		const resultKey = `$throttle$result$${key}`;
+		const lastRunKey = `$throttle$lastRun$${key}`;
+		const pendingKey = `$throttle$pending$${key}`;
+
+		return function (this: any, ...args: any[]) {
+			if (!this[resultKey]) {
+				this[resultKey] = initialValueProvider ? initialValueProvider() : undefined;
+			}
+			if (this[lastRunKey] === null || this[lastRunKey] === undefined) {
+				this[lastRunKey] = -Number.MAX_VALUE;
+			}
+
+			if (reducer) {
+				this[resultKey] = reducer(this[resultKey], ...args);
+			}
+
+			if (this[pendingKey]) {
+				return;
+			}
+
+			const nextTime = this[lastRunKey] + delay;
+			if (nextTime <= Date.now()) {
+				this[lastRunKey] = Date.now();
+				fn.apply(this, [this[resultKey]]);
+				this[resultKey] = initialValueProvider ? initialValueProvider() : undefined;
+			} else {
+				this[pendingKey] = true;
+				this[timerKey] = setTimeout(() => {
+					this[pendingKey] = false;
+					this[lastRunKey] = Date.now();
+					fn.apply(this, [this[resultKey]]);
+					this[resultKey] = initialValueProvider ? initialValueProvider() : undefined;
+				}, nextTime - Date.now());
+			}
+		};
+	});
+}
diff --git a/src/vs/base/common/equals.ts b/src/vs/base/common/equals.ts
new file mode 100644
index 0000000000..6e2ae8503a
--- /dev/null
+++ b/src/vs/base/common/equals.ts
@@ -0,0 +1,146 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import * as arrays from 'vs/base/common/arrays';
+
+export type EqualityComparer<T> = (a: T, b: T) => boolean;
+
+/**
+ * Compares two items for equality using strict equality.
+*/
+export const strictEquals: EqualityComparer<any> = (a, b) => a === b;
+
+/**
+ * Checks if the items of two arrays are equal.
+ * By default, strict equality is used to compare elements, but a custom equality comparer can be provided.
+ */
+export function itemsEquals<T>(itemEquals: EqualityComparer<T> = strictEquals): EqualityComparer<readonly T[]> {
+	return (a, b) => arrays.equals(a, b, itemEquals);
+}
+
+/**
+ * Two items are considered equal, if their stringified representations are equal.
+*/
+export function jsonStringifyEquals<T>(): EqualityComparer<T> {
+	return (a, b) => JSON.stringify(a) === JSON.stringify(b);
+}
+
+/**
+ * Uses `item.equals(other)` to determine equality.
+ */
+export function itemEquals<T extends { equals(other: T): boolean }>(): EqualityComparer<T> {
+	return (a, b) => a.equals(b);
+}
+
+/**
+ * Checks if two items are both null or undefined, or are equal according to the provided equality comparer.
+*/
+export function equalsIfDefined<T>(v1: T | undefined | null, v2: T | undefined | null, equals: EqualityComparer<T>): boolean;
+/**
+ * Returns an equality comparer that checks if two items are both null or undefined, or are equal according to the provided equality comparer.
+*/
+export function equalsIfDefined<T>(equals: EqualityComparer<T>): EqualityComparer<T | undefined | null>;
+export function equalsIfDefined<T>(equalsOrV1: EqualityComparer<T> | T, v2?: T | undefined | null, equals?: EqualityComparer<T>): EqualityComparer<T | undefined | null> | boolean {
+	if (equals !== undefined) {
+		const v1 = equalsOrV1 as T | undefined;
+		if (v1 === undefined || v1 === null || v2 === undefined || v2 === null) {
+			return v2 === v1;
+		}
+		return equals(v1, v2);
+	} else {
+		const equals = equalsOrV1 as EqualityComparer<T>;
+		return (v1, v2) => {
+			if (v1 === undefined || v1 === null || v2 === undefined || v2 === null) {
+				return v2 === v1;
+			}
+			return equals(v1, v2);
+		};
+	}
+}
+
+/**
+ * Drills into arrays (items ordered) and objects (keys unordered) and uses strict equality on everything else.
+*/
+export function structuralEquals<T>(a: T, b: T): boolean {
+	if (a === b) {
+		return true;
+	}
+
+	if (Array.isArray(a) && Array.isArray(b)) {
+		if (a.length !== b.length) {
+			return false;
+		}
+		for (let i = 0; i < a.length; i++) {
+			if (!structuralEquals(a[i], b[i])) {
+				return false;
+			}
+		}
+		return true;
+	}
+
+	if (a && typeof a === 'object' && b && typeof b === 'object') {
+		if (Object.getPrototypeOf(a) === Object.prototype && Object.getPrototypeOf(b) === Object.prototype) {
+			const aObj = a as Record<string, unknown>;
+			const bObj = b as Record<string, unknown>;
+			const keysA = Object.keys(aObj);
+			const keysB = Object.keys(bObj);
+			const keysBSet = new Set(keysB);
+
+			if (keysA.length !== keysB.length) {
+				return false;
+			}
+
+			for (const key of keysA) {
+				if (!keysBSet.has(key)) {
+					return false;
+				}
+				if (!structuralEquals(aObj[key], bObj[key])) {
+					return false;
+				}
+			}
+
+			return true;
+		}
+	}
+
+	return false;
+}
+
+/**
+ * `getStructuralKey(a) === getStructuralKey(b) <=> structuralEquals(a, b)`
+ * (assuming that a and b are not cyclic structures and nothing extends globalThis Array).
+*/
+export function getStructuralKey(t: unknown): string {
+	return JSON.stringify(toNormalizedJsonStructure(t));
+}
+
+let objectId = 0;
+const objIds = new WeakMap<object, number>();
+
+function toNormalizedJsonStructure(t: unknown): unknown {
+	if (Array.isArray(t)) {
+		return t.map(toNormalizedJsonStructure);
+	}
+
+	if (t && typeof t === 'object') {
+		if (Object.getPrototypeOf(t) === Object.prototype) {
+			const tObj = t as Record<string, unknown>;
+			const res: Record<string, unknown> = Object.create(null);
+			for (const key of Object.keys(tObj).sort()) {
+				res[key] = toNormalizedJsonStructure(tObj[key]);
+			}
+			return res;
+		} else {
+			let objId = objIds.get(t);
+			if (objId === undefined) {
+				objId = objectId++;
+				objIds.set(t, objId);
+			}
+			// Random string to prevent collisions
+			return objId + '----2b76a038c20c4bcc';
+		}
+	}
+	return t;
+}
diff --git a/src/vs/base/common/errors.ts b/src/vs/base/common/errors.ts
new file mode 100644
index 0000000000..ce5d8b2985
--- /dev/null
+++ b/src/vs/base/common/errors.ts
@@ -0,0 +1,303 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+export interface ErrorListenerCallback {
+	(error: any): void;
+}
+
+export interface ErrorListenerUnbind {
+	(): void;
+}
+
+// Avoid circular dependency on EventEmitter by implementing a subset of the interface.
+export class ErrorHandler {
+	private unexpectedErrorHandler: (e: any) => void;
+	private listeners: ErrorListenerCallback[];
+
+	constructor() {
+
+		this.listeners = [];
+
+		this.unexpectedErrorHandler = function (e: any) {
+			setTimeout(() => {
+				if (e.stack) {
+					if (ErrorNoTelemetry.isErrorNoTelemetry(e)) {
+						throw new ErrorNoTelemetry(e.message + '\n\n' + e.stack);
+					}
+
+					throw new Error(e.message + '\n\n' + e.stack);
+				}
+
+				throw e;
+			}, 0);
+		};
+	}
+
+	addListener(listener: ErrorListenerCallback): ErrorListenerUnbind {
+		this.listeners.push(listener);
+
+		return () => {
+			this._removeListener(listener);
+		};
+	}
+
+	private emit(e: any): void {
+		this.listeners.forEach((listener) => {
+			listener(e);
+		});
+	}
+
+	private _removeListener(listener: ErrorListenerCallback): void {
+		this.listeners.splice(this.listeners.indexOf(listener), 1);
+	}
+
+	setUnexpectedErrorHandler(newUnexpectedErrorHandler: (e: any) => void): void {
+		this.unexpectedErrorHandler = newUnexpectedErrorHandler;
+	}
+
+	getUnexpectedErrorHandler(): (e: any) => void {
+		return this.unexpectedErrorHandler;
+	}
+
+	onUnexpectedError(e: any): void {
+		this.unexpectedErrorHandler(e);
+		this.emit(e);
+	}
+
+	// For external errors, we don't want the listeners to be called
+	onUnexpectedExternalError(e: any): void {
+		this.unexpectedErrorHandler(e);
+	}
+}
+
+export const errorHandler = new ErrorHandler();
+
+/** @skipMangle */
+export function setUnexpectedErrorHandler(newUnexpectedErrorHandler: (e: any) => void): void {
+	errorHandler.setUnexpectedErrorHandler(newUnexpectedErrorHandler);
+}
+
+/**
+ * Returns if the error is a SIGPIPE error. SIGPIPE errors should generally be
+ * logged at most once, to avoid a loop.
+ *
+ * @see https://github.com/microsoft/vscode-remote-release/issues/6481
+ */
+export function isSigPipeError(e: unknown): e is Error {
+	if (!e || typeof e !== 'object') {
+		return false;
+	}
+
+	const cast = e as Record<string, string | undefined>;
+	return cast.code === 'EPIPE' && cast.syscall?.toUpperCase() === 'WRITE';
+}
+
+export function onUnexpectedError(e: any): undefined {
+	// ignore errors from cancelled promises
+	if (!isCancellationError(e)) {
+		errorHandler.onUnexpectedError(e);
+	}
+	return undefined;
+}
+
+export function onUnexpectedExternalError(e: any): undefined {
+	// ignore errors from cancelled promises
+	if (!isCancellationError(e)) {
+		errorHandler.onUnexpectedExternalError(e);
+	}
+	return undefined;
+}
+
+export interface SerializedError {
+	readonly $isError: true;
+	readonly name: string;
+	readonly message: string;
+	readonly stack: string;
+	readonly noTelemetry: boolean;
+}
+
+export function transformErrorForSerialization(error: Error): SerializedError;
+export function transformErrorForSerialization(error: any): any;
+export function transformErrorForSerialization(error: any): any {
+	if (error instanceof Error) {
+		const { name, message } = error;
+		const stack: string = (<any>error).stacktrace || (<any>error).stack;
+		return {
+			$isError: true,
+			name,
+			message,
+			stack,
+			noTelemetry: ErrorNoTelemetry.isErrorNoTelemetry(error)
+		};
+	}
+
+	// return as is
+	return error;
+}
+
+export function transformErrorFromSerialization(data: SerializedError): Error {
+	let error: Error;
+	if (data.noTelemetry) {
+		error = new ErrorNoTelemetry();
+	} else {
+		error = new Error();
+		error.name = data.name;
+	}
+	error.message = data.message;
+	error.stack = data.stack;
+	return error;
+}
+
+// see https://github.com/v8/v8/wiki/Stack%20Trace%20API#basic-stack-traces
+export interface V8CallSite {
+	getThis(): unknown;
+	getTypeName(): string | null;
+	getFunction(): Function | undefined;
+	getFunctionName(): string | null;
+	getMethodName(): string | null;
+	getFileName(): string | null;
+	getLineNumber(): number | null;
+	getColumnNumber(): number | null;
+	getEvalOrigin(): string | undefined;
+	isToplevel(): boolean;
+	isEval(): boolean;
+	isNative(): boolean;
+	isConstructor(): boolean;
+	toString(): string;
+}
+
+const canceledName = 'Canceled';
+
+/**
+ * Checks if the given error is a promise in canceled state
+ */
+export function isCancellationError(error: any): boolean {
+	if (error instanceof CancellationError) {
+		return true;
+	}
+	return error instanceof Error && error.name === canceledName && error.message === canceledName;
+}
+
+// !!!IMPORTANT!!!
+// Do NOT change this class because it is also used as an API-type.
+export class CancellationError extends Error {
+	constructor() {
+		super(canceledName);
+		this.name = this.message;
+	}
+}
+
+/**
+ * @deprecated use {@link CancellationError `new CancellationError()`} instead
+ */
+export function canceled(): Error {
+	const error = new Error(canceledName);
+	error.name = error.message;
+	return error;
+}
+
+export function illegalArgument(name?: string): Error {
+	if (name) {
+		return new Error(`Illegal argument: ${name}`);
+	} else {
+		return new Error('Illegal argument');
+	}
+}
+
+export function illegalState(name?: string): Error {
+	if (name) {
+		return new Error(`Illegal state: ${name}`);
+	} else {
+		return new Error('Illegal state');
+	}
+}
+
+export class ReadonlyError extends TypeError {
+	constructor(name?: string) {
+		super(name ? `${name} is read-only and cannot be changed` : 'Cannot change read-only property');
+	}
+}
+
+export function getErrorMessage(err: any): string {
+	if (!err) {
+		return 'Error';
+	}
+
+	if (err.message) {
+		return err.message;
+	}
+
+	if (err.stack) {
+		return err.stack.split('\n')[0];
+	}
+
+	return String(err);
+}
+
+export class NotImplementedError extends Error {
+	constructor(message?: string) {
+		super('NotImplemented');
+		if (message) {
+			this.message = message;
+		}
+	}
+}
+
+export class NotSupportedError extends Error {
+	constructor(message?: string) {
+		super('NotSupported');
+		if (message) {
+			this.message = message;
+		}
+	}
+}
+
+export class ExpectedError extends Error {
+	readonly isExpected = true;
+}
+
+/**
+ * Error that when thrown won't be logged in telemetry as an unhandled error.
+ */
+export class ErrorNoTelemetry extends Error {
+	override readonly name: string;
+
+	constructor(msg?: string) {
+		super(msg);
+		this.name = 'CodeExpectedError';
+	}
+
+	public static fromError(err: Error): ErrorNoTelemetry {
+		if (err instanceof ErrorNoTelemetry) {
+			return err;
+		}
+
+		const result = new ErrorNoTelemetry();
+		result.message = err.message;
+		result.stack = err.stack;
+		return result;
+	}
+
+	public static isErrorNoTelemetry(err: Error): err is ErrorNoTelemetry {
+		return err.name === 'CodeExpectedError';
+	}
+}
+
+/**
+ * This error indicates a bug.
+ * Do not throw this for invalid user input.
+ * Only catch this error to recover gracefully from bugs.
+ */
+export class BugIndicatingError extends Error {
+	constructor(message?: string) {
+		super(message || 'An unexpected bug occurred.');
+		Object.setPrototypeOf(this, BugIndicatingError.prototype);
+
+		// Because we know for sure only buggy code throws this,
+		// we definitely want to break here and fix the bug.
+		// eslint-disable-next-line no-debugger
+		// debugger;
+	}
+}
diff --git a/src/vs/base/common/event.ts b/src/vs/base/common/event.ts
new file mode 100644
index 0000000000..d563a2c77d
--- /dev/null
+++ b/src/vs/base/common/event.ts
@@ -0,0 +1,1762 @@
+/*---------------------------------------------------------------------------------------------
+ *  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 { onUnexpectedError } from 'vs/base/common/errors';
+import { createSingleCallFunction } from 'vs/base/common/functional';
+import { combinedDisposable, Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
+import { LinkedList } from 'vs/base/common/linkedList';
+import { IObservable, IObserver } from 'vs/base/common/observable';
+import { StopWatch } from 'vs/base/common/stopwatch';
+import { MicrotaskDelay } from 'vs/base/common/symbols';
+
+
+// -----------------------------------------------------------------------------------------------------------------------
+// Uncomment the next line to print warnings whenever a listener is GC'ed without having been disposed. This is a LEAK.
+// -----------------------------------------------------------------------------------------------------------------------
+const _enableListenerGCedWarning = false
+	// || Boolean("TRUE") // causes a linter warning so that it cannot be pushed
+	;
+
+// -----------------------------------------------------------------------------------------------------------------------
+// Uncomment the next line to print warnings whenever an emitter with listeners is disposed. That is a sign of code smell.
+// -----------------------------------------------------------------------------------------------------------------------
+const _enableDisposeWithListenerWarning = false
+	// || Boolean("TRUE") // causes a linter warning so that it cannot be pushed
+	;
+
+
+// -----------------------------------------------------------------------------------------------------------------------
+// Uncomment the next line to print warnings whenever a snapshotted event is used repeatedly without cleanup.
+// See https://github.com/microsoft/vscode/issues/142851
+// -----------------------------------------------------------------------------------------------------------------------
+const _enableSnapshotPotentialLeakWarning = false
+	// || Boolean("TRUE") // causes a linter warning so that it cannot be pushed
+	;
+
+/**
+ * An event with zero or one parameters that can be subscribed to. The event is a function itself.
+ */
+export interface Event<T> {
+	(listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore): IDisposable;
+}
+
+export namespace Event {
+	export const None: Event<any> = () => Disposable.None;
+
+	function _addLeakageTraceLogic(options: EmitterOptions) {
+		if (_enableSnapshotPotentialLeakWarning) {
+			const { onDidAddListener: origListenerDidAdd } = options;
+			const stack = Stacktrace.create();
+			let count = 0;
+			options.onDidAddListener = () => {
+				if (++count === 2) {
+					console.warn('snapshotted emitter LIKELY used public and SHOULD HAVE BEEN created with DisposableStore. snapshotted here');
+					stack.print();
+				}
+				origListenerDidAdd?.();
+			};
+		}
+	}
+
+	/**
+	 * Given an event, returns another event which debounces calls and defers the listeners to a later task via a shared
+	 * `setTimeout`. The event is converted into a signal (`Event<void>`) to avoid additional object creation as a
+	 * result of merging events and to try prevent race conditions that could arise when using related deferred and
+	 * non-deferred events.
+	 *
+	 * This is useful for deferring non-critical work (eg. general UI updates) to ensure it does not block critical work
+	 * (eg. latency of keypress to text rendered).
+	 *
+	 * *NOTE* that this function returns an `Event` and it MUST be called with a `DisposableStore` whenever the returned
+	 * event is accessible to "third parties", e.g the event is a public property. Otherwise a leaked listener on the
+	 * returned event causes this utility to leak a listener on the original event.
+	 *
+	 * @param event The event source for the new event.
+	 * @param disposable A disposable store to add the new EventEmitter to.
+	 */
+	export function defer(event: Event<unknown>, disposable?: DisposableStore): Event<void> {
+		return debounce<unknown, void>(event, () => void 0, 0, undefined, true, undefined, disposable);
+	}
+
+	/**
+	 * Given an event, returns another event which only fires once.
+	 *
+	 * @param event The event source for the new event.
+	 */
+	export function once<T>(event: Event<T>): Event<T> {
+		return (listener, thisArgs = null, disposables?) => {
+			// we need this, in case the event fires during the listener call
+			let didFire = false;
+			let result: IDisposable | undefined = undefined;
+			result = event(e => {
+				if (didFire) {
+					return;
+				} else if (result) {
+					result.dispose();
+				} else {
+					didFire = true;
+				}
+
+				return listener.call(thisArgs, e);
+			}, null, disposables);
+
+			if (didFire) {
+				result.dispose();
+			}
+
+			return result;
+		};
+	}
+
+	/**
+	 * Maps an event of one type into an event of another type using a mapping function, similar to how
+	 * `Array.prototype.map` works.
+	 *
+	 * *NOTE* that this function returns an `Event` and it MUST be called with a `DisposableStore` whenever the returned
+	 * event is accessible to "third parties", e.g the event is a public property. Otherwise a leaked listener on the
+	 * returned event causes this utility to leak a listener on the original event.
+	 *
+	 * @param event The event source for the new event.
+	 * @param map The mapping function.
+	 * @param disposable A disposable store to add the new EventEmitter to.
+	 */
+	export function map<I, O>(event: Event<I>, map: (i: I) => O, disposable?: DisposableStore): Event<O> {
+		return snapshot((listener, thisArgs = null, disposables?) => event(i => listener.call(thisArgs, map(i)), null, disposables), disposable);
+	}
+
+	/**
+	 * Wraps an event in another event that performs some function on the event object before firing.
+	 *
+	 * *NOTE* that this function returns an `Event` and it MUST be called with a `DisposableStore` whenever the returned
+	 * event is accessible to "third parties", e.g the event is a public property. Otherwise a leaked listener on the
+	 * returned event causes this utility to leak a listener on the original event.
+	 *
+	 * @param event The event source for the new event.
+	 * @param each The function to perform on the event object.
+	 * @param disposable A disposable store to add the new EventEmitter to.
+	 */
+	export function forEach<I>(event: Event<I>, each: (i: I) => void, disposable?: DisposableStore): Event<I> {
+		return snapshot((listener, thisArgs = null, disposables?) => event(i => { each(i); listener.call(thisArgs, i); }, null, disposables), disposable);
+	}
+
+	/**
+	 * Wraps an event in another event that fires only when some condition is met.
+	 *
+	 * *NOTE* that this function returns an `Event` and it MUST be called with a `DisposableStore` whenever the returned
+	 * event is accessible to "third parties", e.g the event is a public property. Otherwise a leaked listener on the
+	 * returned event causes this utility to leak a listener on the original event.
+	 *
+	 * @param event The event source for the new event.
+	 * @param filter The filter function that defines the condition. The event will fire for the object if this function
+	 * returns true.
+	 * @param disposable A disposable store to add the new EventEmitter to.
+	 */
+	export function filter<T, U>(event: Event<T | U>, filter: (e: T | U) => e is T, disposable?: DisposableStore): Event<T>;
+	export function filter<T>(event: Event<T>, filter: (e: T) => boolean, disposable?: DisposableStore): Event<T>;
+	export function filter<T, R>(event: Event<T | R>, filter: (e: T | R) => e is R, disposable?: DisposableStore): Event<R>;
+	export function filter<T>(event: Event<T>, filter: (e: T) => boolean, disposable?: DisposableStore): Event<T> {
+		return snapshot((listener, thisArgs = null, disposables?) => event(e => filter(e) && listener.call(thisArgs, e), null, disposables), disposable);
+	}
+
+	/**
+	 * Given an event, returns the same event but typed as `Event<void>`.
+	 */
+	export function signal<T>(event: Event<T>): Event<void> {
+		return event as Event<any> as Event<void>;
+	}
+
+	/**
+	 * Given a collection of events, returns a single event which emits whenever any of the provided events emit.
+	 */
+	export function any<T>(...events: Event<T>[]): Event<T>;
+	export function any(...events: Event<any>[]): Event<void>;
+	export function any<T>(...events: Event<T>[]): Event<T> {
+		return (listener, thisArgs = null, disposables?) => {
+			const disposable = combinedDisposable(...events.map(event => event(e => listener.call(thisArgs, e))));
+			return addAndReturnDisposable(disposable, disposables);
+		};
+	}
+
+	/**
+	 * *NOTE* that this function returns an `Event` and it MUST be called with a `DisposableStore` whenever the returned
+	 * event is accessible to "third parties", e.g the event is a public property. Otherwise a leaked listener on the
+	 * returned event causes this utility to leak a listener on the original event.
+	 */
+	export function reduce<I, O>(event: Event<I>, merge: (last: O | undefined, event: I) => O, initial?: O, disposable?: DisposableStore): Event<O> {
+		let output: O | undefined = initial;
+
+		return map<I, O>(event, e => {
+			output = merge(output, e);
+			return output;
+		}, disposable);
+	}
+
+	function snapshot<T>(event: Event<T>, disposable: DisposableStore | undefined): Event<T> {
+		let listener: IDisposable | undefined;
+
+		const options: EmitterOptions | undefined = {
+			onWillAddFirstListener() {
+				listener = event(emitter.fire, emitter);
+			},
+			onDidRemoveLastListener() {
+				listener?.dispose();
+			}
+		};
+
+		if (!disposable) {
+			_addLeakageTraceLogic(options);
+		}
+
+		const emitter = new Emitter<T>(options);
+
+		disposable?.add(emitter);
+
+		return emitter.event;
+	}
+
+	/**
+	 * Adds the IDisposable to the store if it's set, and returns it. Useful to
+	 * Event function implementation.
+	 */
+	function addAndReturnDisposable<T extends IDisposable>(d: T, store: DisposableStore | IDisposable[] | undefined): T {
+		if (store instanceof Array) {
+			store.push(d);
+		} else if (store) {
+			store.add(d);
+		}
+		return d;
+	}
+
+	/**
+	 * Given an event, creates a new emitter that event that will debounce events based on {@link delay} and give an
+	 * array event object of all events that fired.
+	 *
+	 * *NOTE* that this function returns an `Event` and it MUST be called with a `DisposableStore` whenever the returned
+	 * event is accessible to "third parties", e.g the event is a public property. Otherwise a leaked listener on the
+	 * returned event causes this utility to leak a listener on the original event.
+	 *
+	 * @param event The original event to debounce.
+	 * @param merge A function that reduces all events into a single event.
+	 * @param delay The number of milliseconds to debounce.
+	 * @param leading Whether to fire a leading event without debouncing.
+	 * @param flushOnListenerRemove Whether to fire all debounced events when a listener is removed. If this is not
+	 * specified, some events could go missing. Use this if it's important that all events are processed, even if the
+	 * listener gets disposed before the debounced event fires.
+	 * @param leakWarningThreshold See {@link EmitterOptions.leakWarningThreshold}.
+	 * @param disposable A disposable store to register the debounce emitter to.
+	 */
+	export function debounce<T>(event: Event<T>, merge: (last: T | undefined, event: T) => T, delay?: number | typeof MicrotaskDelay, leading?: boolean, flushOnListenerRemove?: boolean, leakWarningThreshold?: number, disposable?: DisposableStore): Event<T>;
+	export function debounce<I, O>(event: Event<I>, merge: (last: O | undefined, event: I) => O, delay?: number | typeof MicrotaskDelay, leading?: boolean, flushOnListenerRemove?: boolean, leakWarningThreshold?: number, disposable?: DisposableStore): Event<O>;
+	export function debounce<I, O>(event: Event<I>, merge: (last: O | undefined, event: I) => O, delay: number | typeof MicrotaskDelay = 100, leading = false, flushOnListenerRemove = false, leakWarningThreshold?: number, disposable?: DisposableStore): Event<O> {
+		let subscription: IDisposable;
+		let output: O | undefined = undefined;
+		let handle: any = undefined;
+		let numDebouncedCalls = 0;
+		let doFire: (() => void) | undefined;
+
+		const options: EmitterOptions | undefined = {
+			leakWarningThreshold,
+			onWillAddFirstListener() {
+				subscription = event(cur => {
+					numDebouncedCalls++;
+					output = merge(output, cur);
+
+					if (leading && !handle) {
+						emitter.fire(output);
+						output = undefined;
+					}
+
+					doFire = () => {
+						const _output = output;
+						output = undefined;
+						handle = undefined;
+						if (!leading || numDebouncedCalls > 1) {
+							emitter.fire(_output!);
+						}
+						numDebouncedCalls = 0;
+					};
+
+					if (typeof delay === 'number') {
+						clearTimeout(handle);
+						handle = setTimeout(doFire, delay);
+					} else {
+						if (handle === undefined) {
+							handle = 0;
+							queueMicrotask(doFire);
+						}
+					}
+				});
+			},
+			onWillRemoveListener() {
+				if (flushOnListenerRemove && numDebouncedCalls > 0) {
+					doFire?.();
+				}
+			},
+			onDidRemoveLastListener() {
+				doFire = undefined;
+				subscription.dispose();
+			}
+		};
+
+		if (!disposable) {
+			_addLeakageTraceLogic(options);
+		}
+
+		const emitter = new Emitter<O>(options);
+
+		disposable?.add(emitter);
+
+		return emitter.event;
+	}
+
+	/**
+	 * Debounces an event, firing after some delay (default=0) with an array of all event original objects.
+	 *
+	 * *NOTE* that this function returns an `Event` and it MUST be called with a `DisposableStore` whenever the returned
+	 * event is accessible to "third parties", e.g the event is a public property. Otherwise a leaked listener on the
+	 * returned event causes this utility to leak a listener on the original event.
+	 */
+	export function accumulate<T>(event: Event<T>, delay: number = 0, disposable?: DisposableStore): Event<T[]> {
+		return Event.debounce<T, T[]>(event, (last, e) => {
+			if (!last) {
+				return [e];
+			}
+			last.push(e);
+			return last;
+		}, delay, undefined, true, undefined, disposable);
+	}
+
+	/**
+	 * Filters an event such that some condition is _not_ met more than once in a row, effectively ensuring duplicate
+	 * event objects from different sources do not fire the same event object.
+	 *
+	 * *NOTE* that this function returns an `Event` and it MUST be called with a `DisposableStore` whenever the returned
+	 * event is accessible to "third parties", e.g the event is a public property. Otherwise a leaked listener on the
+	 * returned event causes this utility to leak a listener on the original event.
+	 *
+	 * @param event The event source for the new event.
+	 * @param equals The equality condition.
+	 * @param disposable A disposable store to add the new EventEmitter to.
+	 *
+	 * @example
+	 * ```
+	 * // Fire only one time when a single window is opened or focused
+	 * Event.latch(Event.any(onDidOpenWindow, onDidFocusWindow))
+	 * ```
+	 */
+	export function latch<T>(event: Event<T>, equals: (a: T, b: T) => boolean = (a, b) => a === b, disposable?: DisposableStore): Event<T> {
+		let firstCall = true;
+		let cache: T;
+
+		return filter(event, value => {
+			const shouldEmit = firstCall || !equals(value, cache);
+			firstCall = false;
+			cache = value;
+			return shouldEmit;
+		}, disposable);
+	}
+
+	/**
+	 * Splits an event whose parameter is a union type into 2 separate events for each type in the union.
+	 *
+	 * *NOTE* that this function returns an `Event` and it MUST be called with a `DisposableStore` whenever the returned
+	 * event is accessible to "third parties", e.g the event is a public property. Otherwise a leaked listener on the
+	 * returned event causes this utility to leak a listener on the original event.
+	 *
+	 * @example
+	 * ```
+	 * const event = new EventEmitter<number | undefined>().event;
+	 * const [numberEvent, undefinedEvent] = Event.split(event, isUndefined);
+	 * ```
+	 *
+	 * @param event The event source for the new event.
+	 * @param isT A function that determines what event is of the first type.
+	 * @param disposable A disposable store to add the new EventEmitter to.
+	 */
+	export function split<T, U>(event: Event<T | U>, isT: (e: T | U) => e is T, disposable?: DisposableStore): [Event<T>, Event<U>] {
+		return [
+			Event.filter(event, isT, disposable),
+			Event.filter(event, e => !isT(e), disposable) as Event<U>,
+		];
+	}
+
+	/**
+	 * Buffers an event until it has a listener attached.
+	 *
+	 * *NOTE* that this function returns an `Event` and it MUST be called with a `DisposableStore` whenever the returned
+	 * event is accessible to "third parties", e.g the event is a public property. Otherwise a leaked listener on the
+	 * returned event causes this utility to leak a listener on the original event.
+	 *
+	 * @param event The event source for the new event.
+	 * @param flushAfterTimeout Determines whether to flush the buffer after a timeout immediately or after a
+	 * `setTimeout` when the first event listener is added.
+	 * @param _buffer Internal: A source event array used for tests.
+	 *
+	 * @example
+	 * ```
+	 * // Start accumulating events, when the first listener is attached, flush
+	 * // the event after a timeout such that multiple listeners attached before
+	 * // the timeout would receive the event
+	 * this.onInstallExtension = Event.buffer(service.onInstallExtension, true);
+	 * ```
+	 */
+	export function buffer<T>(event: Event<T>, flushAfterTimeout = false, _buffer: T[] = [], disposable?: DisposableStore): Event<T> {
+		let buffer: T[] | null = _buffer.slice();
+
+		let listener: IDisposable | null = event(e => {
+			if (buffer) {
+				buffer.push(e);
+			} else {
+				emitter.fire(e);
+			}
+		});
+
+		if (disposable) {
+			disposable.add(listener);
+		}
+
+		const flush = () => {
+			buffer?.forEach(e => emitter.fire(e));
+			buffer = null;
+		};
+
+		const emitter = new Emitter<T>({
+			onWillAddFirstListener() {
+				if (!listener) {
+					listener = event(e => emitter.fire(e));
+					if (disposable) {
+						disposable.add(listener);
+					}
+				}
+			},
+
+			onDidAddFirstListener() {
+				if (buffer) {
+					if (flushAfterTimeout) {
+						setTimeout(flush);
+					} else {
+						flush();
+					}
+				}
+			},
+
+			onDidRemoveLastListener() {
+				if (listener) {
+					listener.dispose();
+				}
+				listener = null;
+			}
+		});
+
+		if (disposable) {
+			disposable.add(emitter);
+		}
+
+		return emitter.event;
+	}
+	/**
+	 * Wraps the event in an {@link IChainableEvent}, allowing a more functional programming style.
+	 *
+	 * @example
+	 * ```
+	 * // Normal
+	 * const onEnterPressNormal = Event.filter(
+	 *   Event.map(onKeyPress.event, e => new StandardKeyboardEvent(e)),
+	 *   e.keyCode === KeyCode.Enter
+	 * ).event;
+	 *
+	 * // Using chain
+	 * const onEnterPressChain = Event.chain(onKeyPress.event, $ => $
+	 *   .map(e => new StandardKeyboardEvent(e))
+	 *   .filter(e => e.keyCode === KeyCode.Enter)
+	 * );
+	 * ```
+	 */
+	export function chain<T, R>(event: Event<T>, sythensize: ($: IChainableSythensis<T>) => IChainableSythensis<R>): Event<R> {
+		const fn: Event<R> = (listener, thisArgs, disposables) => {
+			const cs = sythensize(new ChainableSynthesis()) as ChainableSynthesis;
+			return event(function (value) {
+				const result = cs.evaluate(value);
+				if (result !== HaltChainable) {
+					listener.call(thisArgs, result);
+				}
+			}, undefined, disposables);
+		};
+
+		return fn;
+	}
+
+	const HaltChainable = Symbol('HaltChainable');
+
+	class ChainableSynthesis implements IChainableSythensis<any> {
+		private readonly steps: ((input: any) => any)[] = [];
+
+		map<O>(fn: (i: any) => O): this {
+			this.steps.push(fn);
+			return this;
+		}
+
+		forEach(fn: (i: any) => void): this {
+			this.steps.push(v => {
+				fn(v);
+				return v;
+			});
+			return this;
+		}
+
+		filter(fn: (e: any) => boolean): this {
+			this.steps.push(v => fn(v) ? v : HaltChainable);
+			return this;
+		}
+
+		reduce<R>(merge: (last: R | undefined, event: any) => R, initial?: R | undefined): this {
+			let last = initial;
+			this.steps.push(v => {
+				last = merge(last, v);
+				return last;
+			});
+			return this;
+		}
+
+		latch(equals: (a: any, b: any) => boolean = (a, b) => a === b): ChainableSynthesis {
+			let firstCall = true;
+			let cache: any;
+			this.steps.push(value => {
+				const shouldEmit = firstCall || !equals(value, cache);
+				firstCall = false;
+				cache = value;
+				return shouldEmit ? value : HaltChainable;
+			});
+
+			return this;
+		}
+
+		public evaluate(value: any) {
+			for (const step of this.steps) {
+				value = step(value);
+				if (value === HaltChainable) {
+					break;
+				}
+			}
+
+			return value;
+		}
+	}
+
+	export interface IChainableSythensis<T> {
+		map<O>(fn: (i: T) => O): IChainableSythensis<O>;
+		forEach(fn: (i: T) => void): IChainableSythensis<T>;
+		filter<R extends T>(fn: (e: T) => e is R): IChainableSythensis<R>;
+		filter(fn: (e: T) => boolean): IChainableSythensis<T>;
+		reduce<R>(merge: (last: R, event: T) => R, initial: R): IChainableSythensis<R>;
+		reduce<R>(merge: (last: R | undefined, event: T) => R): IChainableSythensis<R>;
+		latch(equals?: (a: T, b: T) => boolean): IChainableSythensis<T>;
+	}
+
+	export interface NodeEventEmitter {
+		on(event: string | symbol, listener: Function): unknown;
+		removeListener(event: string | symbol, listener: Function): unknown;
+	}
+
+	/**
+	 * Creates an {@link Event} from a node event emitter.
+	 */
+	export function fromNodeEventEmitter<T>(emitter: NodeEventEmitter, eventName: string, map: (...args: any[]) => T = id => id): Event<T> {
+		const fn = (...args: any[]) => result.fire(map(...args));
+		const onFirstListenerAdd = () => emitter.on(eventName, fn);
+		const onLastListenerRemove = () => emitter.removeListener(eventName, fn);
+		const result = new Emitter<T>({ onWillAddFirstListener: onFirstListenerAdd, onDidRemoveLastListener: onLastListenerRemove });
+
+		return result.event;
+	}
+
+	export interface DOMEventEmitter {
+		addEventListener(event: string | symbol, listener: Function): void;
+		removeEventListener(event: string | symbol, listener: Function): void;
+	}
+
+	/**
+	 * Creates an {@link Event} from a DOM event emitter.
+	 */
+	export function fromDOMEventEmitter<T>(emitter: DOMEventEmitter, eventName: string, map: (...args: any[]) => T = id => id): Event<T> {
+		const fn = (...args: any[]) => result.fire(map(...args));
+		const onFirstListenerAdd = () => emitter.addEventListener(eventName, fn);
+		const onLastListenerRemove = () => emitter.removeEventListener(eventName, fn);
+		const result = new Emitter<T>({ onWillAddFirstListener: onFirstListenerAdd, onDidRemoveLastListener: onLastListenerRemove });
+
+		return result.event;
+	}
+
+	/**
+	 * Creates a promise out of an event, using the {@link Event.once} helper.
+	 */
+	export function toPromise<T>(event: Event<T>): Promise<T> {
+		return new Promise(resolve => once(event)(resolve));
+	}
+
+	/**
+	 * Creates an event out of a promise that fires once when the promise is
+	 * resolved with the result of the promise or `undefined`.
+	 */
+	export function fromPromise<T>(promise: Promise<T>): Event<T | undefined> {
+		const result = new Emitter<T | undefined>();
+
+		promise.then(res => {
+			result.fire(res);
+		}, () => {
+			result.fire(undefined);
+		}).finally(() => {
+			result.dispose();
+		});
+
+		return result.event;
+	}
+
+	/**
+	 * Adds a listener to an event and calls the listener immediately with undefined as the event object.
+	 *
+	 * @example
+	 * ```
+	 * // Initialize the UI and update it when dataChangeEvent fires
+	 * runAndSubscribe(dataChangeEvent, () => this._updateUI());
+	 * ```
+	 */
+	export function runAndSubscribe<T>(event: Event<T>, handler: (e: T) => any, initial: T): IDisposable;
+	export function runAndSubscribe<T>(event: Event<T>, handler: (e: T | undefined) => any): IDisposable;
+	export function runAndSubscribe<T>(event: Event<T>, handler: (e: T | undefined) => any, initial?: T): IDisposable {
+		handler(initial);
+		return event(e => handler(e));
+	}
+
+	class EmitterObserver<T> implements IObserver {
+
+		readonly emitter: Emitter<T>;
+
+		private _counter = 0;
+		private _hasChanged = false;
+
+		constructor(readonly _observable: IObservable<T, any>, store: DisposableStore | undefined) {
+			const options: EmitterOptions = {
+				onWillAddFirstListener: () => {
+					_observable.addObserver(this);
+				},
+				onDidRemoveLastListener: () => {
+					_observable.removeObserver(this);
+				}
+			};
+			if (!store) {
+				_addLeakageTraceLogic(options);
+			}
+			this.emitter = new Emitter<T>(options);
+			if (store) {
+				store.add(this.emitter);
+			}
+		}
+
+		beginUpdate<T>(_observable: IObservable<T, void>): void {
+			// assert(_observable === this.obs);
+			this._counter++;
+		}
+
+		handlePossibleChange<T>(_observable: IObservable<T, unknown>): void {
+			// assert(_observable === this.obs);
+		}
+
+		handleChange<T, TChange>(_observable: IObservable<T, TChange>, _change: TChange): void {
+			// assert(_observable === this.obs);
+			this._hasChanged = true;
+		}
+
+		endUpdate<T>(_observable: IObservable<T, void>): void {
+			// assert(_observable === this.obs);
+			this._counter--;
+			if (this._counter === 0) {
+				this._observable.reportChanges();
+				if (this._hasChanged) {
+					this._hasChanged = false;
+					this.emitter.fire(this._observable.get());
+				}
+			}
+		}
+	}
+
+	/**
+	 * Creates an event emitter that is fired when the observable changes.
+	 * Each listeners subscribes to the emitter.
+	 */
+	export function fromObservable<T>(obs: IObservable<T, any>, store?: DisposableStore): Event<T> {
+		const observer = new EmitterObserver(obs, store);
+		return observer.emitter.event;
+	}
+
+	/**
+	 * Each listener is attached to the observable directly.
+	 */
+	export function fromObservableLight(observable: IObservable<any>): Event<void> {
+		return (listener, thisArgs, disposables) => {
+			let count = 0;
+			let didChange = false;
+			const observer: IObserver = {
+				beginUpdate() {
+					count++;
+				},
+				endUpdate() {
+					count--;
+					if (count === 0) {
+						observable.reportChanges();
+						if (didChange) {
+							didChange = false;
+							listener.call(thisArgs);
+						}
+					}
+				},
+				handlePossibleChange() {
+					// noop
+				},
+				handleChange() {
+					didChange = true;
+				}
+			};
+			observable.addObserver(observer);
+			observable.reportChanges();
+			const disposable = {
+				dispose() {
+					observable.removeObserver(observer);
+				}
+			};
+
+			if (disposables instanceof DisposableStore) {
+				disposables.add(disposable);
+			} else if (Array.isArray(disposables)) {
+				disposables.push(disposable);
+			}
+
+			return disposable;
+		};
+	}
+}
+
+export interface EmitterOptions {
+	/**
+	 * Optional function that's called *before* the very first listener is added
+	 */
+	onWillAddFirstListener?: Function;
+	/**
+	 * Optional function that's called *after* the very first listener is added
+	 */
+	onDidAddFirstListener?: Function;
+	/**
+	 * Optional function that's called after a listener is added
+	 */
+	onDidAddListener?: Function;
+	/**
+	 * Optional function that's called *after* remove the very last listener
+	 */
+	onDidRemoveLastListener?: Function;
+	/**
+	 * Optional function that's called *before* a listener is removed
+	 */
+	onWillRemoveListener?: Function;
+	/**
+	 * Optional function that's called when a listener throws an error. Defaults to
+	 * {@link onUnexpectedError}
+	 */
+	onListenerError?: (e: any) => void;
+	/**
+	 * Number of listeners that are allowed before assuming a leak. Default to
+	 * a globally configured value
+	 *
+	 * @see setGlobalLeakWarningThreshold
+	 */
+	leakWarningThreshold?: number;
+	/**
+	 * Pass in a delivery queue, which is useful for ensuring
+	 * in order event delivery across multiple emitters.
+	 */
+	deliveryQueue?: EventDeliveryQueue;
+
+	/** ONLY enable this during development */
+	_profName?: string;
+}
+
+
+export class EventProfiling {
+
+	static readonly all = new Set<EventProfiling>();
+
+	private static _idPool = 0;
+
+	readonly name: string;
+	public listenerCount: number = 0;
+	public invocationCount = 0;
+	public elapsedOverall = 0;
+	public durations: number[] = [];
+
+	private _stopWatch?: StopWatch;
+
+	constructor(name: string) {
+		this.name = `${name}_${EventProfiling._idPool++}`;
+		EventProfiling.all.add(this);
+	}
+
+	start(listenerCount: number): void {
+		this._stopWatch = new StopWatch();
+		this.listenerCount = listenerCount;
+	}
+
+	stop(): void {
+		if (this._stopWatch) {
+			const elapsed = this._stopWatch.elapsed();
+			this.durations.push(elapsed);
+			this.elapsedOverall += elapsed;
+			this.invocationCount += 1;
+			this._stopWatch = undefined;
+		}
+	}
+}
+
+let _globalLeakWarningThreshold = -1;
+export function setGlobalLeakWarningThreshold(n: number): IDisposable {
+	const oldValue = _globalLeakWarningThreshold;
+	_globalLeakWarningThreshold = n;
+	return {
+		dispose() {
+			_globalLeakWarningThreshold = oldValue;
+		}
+	};
+}
+
+class LeakageMonitor {
+
+	private static _idPool = 1;
+
+	private _stacks: Map<string, number> | undefined;
+	private _warnCountdown: number = 0;
+
+	constructor(
+		private readonly _errorHandler: (err: Error) => void,
+		readonly threshold: number,
+		readonly name: string = (LeakageMonitor._idPool++).toString(16).padStart(3, '0')
+	) { }
+
+	dispose(): void {
+		this._stacks?.clear();
+	}
+
+	check(stack: Stacktrace, listenerCount: number): undefined | (() => void) {
+
+		const threshold = this.threshold;
+		if (threshold <= 0 || listenerCount < threshold) {
+			return undefined;
+		}
+
+		if (!this._stacks) {
+			this._stacks = new Map();
+		}
+		const count = (this._stacks.get(stack.value) || 0);
+		this._stacks.set(stack.value, count + 1);
+		this._warnCountdown -= 1;
+
+		if (this._warnCountdown <= 0) {
+			// only warn on first exceed and then every time the limit
+			// is exceeded by 50% again
+			this._warnCountdown = threshold * 0.5;
+
+			const [topStack, topCount] = this.getMostFrequentStack()!;
+			const message = `[${this.name}] potential listener LEAK detected, having ${listenerCount} listeners already. MOST frequent listener (${topCount}):`;
+			console.warn(message);
+			console.warn(topStack!);
+
+			const error = new ListenerLeakError(message, topStack);
+			this._errorHandler(error);
+		}
+
+		return () => {
+			const count = (this._stacks!.get(stack.value) || 0);
+			this._stacks!.set(stack.value, count - 1);
+		};
+	}
+
+	getMostFrequentStack(): [string, number] | undefined {
+		if (!this._stacks) {
+			return undefined;
+		}
+		let topStack: [string, number] | undefined;
+		let topCount: number = 0;
+		for (const [stack, count] of this._stacks) {
+			if (!topStack || topCount < count) {
+				topStack = [stack, count];
+				topCount = count;
+			}
+		}
+		return topStack;
+	}
+}
+
+class Stacktrace {
+
+	static create() {
+		const err = new Error();
+		return new Stacktrace(err.stack ?? '');
+	}
+
+	private constructor(readonly value: string) { }
+
+	print() {
+		console.warn(this.value.split('\n').slice(2).join('\n'));
+	}
+}
+
+// error that is logged when going over the configured listener threshold
+export class ListenerLeakError extends Error {
+	constructor(message: string, stack: string) {
+		super(message);
+		this.name = 'ListenerLeakError';
+		this.stack = stack;
+	}
+}
+
+// SEVERE error that is logged when having gone way over the configured listener
+// threshold so that the emitter refuses to accept more listeners
+export class ListenerRefusalError extends Error {
+	constructor(message: string, stack: string) {
+		super(message);
+		this.name = 'ListenerRefusalError';
+		this.stack = stack;
+	}
+}
+
+let id = 0;
+class UniqueContainer<T> {
+	stack?: Stacktrace;
+	public id = id++;
+	constructor(public readonly value: T) { }
+}
+const compactionThreshold = 2;
+
+type ListenerContainer<T> = UniqueContainer<(data: T) => void>;
+type ListenerOrListeners<T> = (ListenerContainer<T> | undefined)[] | ListenerContainer<T>;
+
+const forEachListener = <T>(listeners: ListenerOrListeners<T>, fn: (c: ListenerContainer<T>) => void) => {
+	if (listeners instanceof UniqueContainer) {
+		fn(listeners);
+	} else {
+		for (let i = 0; i < listeners.length; i++) {
+			const l = listeners[i];
+			if (l) {
+				fn(l);
+			}
+		}
+	}
+};
+
+
+let _listenerFinalizers: FinalizationRegistry<string> | undefined;
+
+if (_enableListenerGCedWarning) {
+	const leaks: string[] = [];
+
+	setInterval(() => {
+		if (leaks.length === 0) {
+			return;
+		}
+		console.warn('[LEAKING LISTENERS] GC\'ed these listeners that were NOT yet disposed:');
+		console.warn(leaks.join('\n'));
+		leaks.length = 0;
+	}, 3000);
+
+	_listenerFinalizers = new FinalizationRegistry(heldValue => {
+		if (typeof heldValue === 'string') {
+			leaks.push(heldValue);
+		}
+	});
+}
+
+/**
+ * The Emitter can be used to expose an Event to the public
+ * to fire it from the insides.
+ * Sample:
+	class Document {
+
+		private readonly _onDidChange = new Emitter<(value:string)=>any>();
+
+		public onDidChange = this._onDidChange.event;
+
+		// getter-style
+		// get onDidChange(): Event<(value:string)=>any> {
+		// 	return this._onDidChange.event;
+		// }
+
+		private _doIt() {
+			//...
+			this._onDidChange.fire(value);
+		}
+	}
+ */
+export class Emitter<T> {
+
+	private readonly _options?: EmitterOptions;
+	private readonly _leakageMon?: LeakageMonitor;
+	private readonly _perfMon?: EventProfiling;
+	private _disposed?: true;
+	private _event?: Event<T>;
+
+	/**
+	 * A listener, or list of listeners. A single listener is the most common
+	 * for event emitters (#185789), so we optimize that special case to avoid
+	 * wrapping it in an array (just like Node.js itself.)
+	 *
+	 * A list of listeners never 'downgrades' back to a plain function if
+	 * listeners are removed, for two reasons:
+	 *
+	 *  1. That's complicated (especially with the deliveryQueue)
+	 *  2. A listener with >1 listener is likely to have >1 listener again at
+	 *     some point, and swapping between arrays and functions may[citation needed]
+	 *     introduce unnecessary work and garbage.
+	 *
+	 * The array listeners can be 'sparse', to avoid reallocating the array
+	 * whenever any listener is added or removed. If more than `1 / compactionThreshold`
+	 * of the array is empty, only then is it resized.
+	 */
+	protected _listeners?: ListenerOrListeners<T>;
+
+	/**
+	 * Always to be defined if _listeners is an array. It's no longer a true
+	 * queue, but holds the dispatching 'state'. If `fire()` is called on an
+	 * emitter, any work left in the _deliveryQueue is finished first.
+	 */
+	private _deliveryQueue?: EventDeliveryQueuePrivate;
+	protected _size = 0;
+
+	constructor(options?: EmitterOptions) {
+		this._options = options;
+		this._leakageMon = (_globalLeakWarningThreshold > 0 || this._options?.leakWarningThreshold)
+			? new LeakageMonitor(options?.onListenerError ?? onUnexpectedError, this._options?.leakWarningThreshold ?? _globalLeakWarningThreshold) :
+			undefined;
+		this._perfMon = this._options?._profName ? new EventProfiling(this._options._profName) : undefined;
+		this._deliveryQueue = this._options?.deliveryQueue as EventDeliveryQueuePrivate | undefined;
+	}
+
+	dispose() {
+		if (!this._disposed) {
+			this._disposed = true;
+
+			// It is bad to have listeners at the time of disposing an emitter, it is worst to have listeners keep the emitter
+			// alive via the reference that's embedded in their disposables. Therefore we loop over all remaining listeners and
+			// unset their subscriptions/disposables. Looping and blaming remaining listeners is done on next tick because the
+			// the following programming pattern is very popular:
+			//
+			// const someModel = this._disposables.add(new ModelObject()); // (1) create and register model
+			// this._disposables.add(someModel.onDidChange(() => { ... }); // (2) subscribe and register model-event listener
+			// ...later...
+			// this._disposables.dispose(); disposes (1) then (2): don't warn after (1) but after the "overall dispose" is done
+
+			if (this._deliveryQueue?.current === this) {
+				this._deliveryQueue.reset();
+			}
+			if (this._listeners) {
+				if (_enableDisposeWithListenerWarning) {
+					const listeners = this._listeners;
+					queueMicrotask(() => {
+						forEachListener(listeners, l => l.stack?.print());
+					});
+				}
+
+				this._listeners = undefined;
+				this._size = 0;
+			}
+			this._options?.onDidRemoveLastListener?.();
+			this._leakageMon?.dispose();
+		}
+	}
+
+	/**
+	 * For the public to allow to subscribe
+	 * to events from this Emitter
+	 */
+	get event(): Event<T> {
+		this._event ??= (callback: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => {
+			if (this._leakageMon && this._size > this._leakageMon.threshold ** 2) {
+				const message = `[${this._leakageMon.name}] REFUSES to accept new listeners because it exceeded its threshold by far (${this._size} vs ${this._leakageMon.threshold})`;
+				console.warn(message);
+
+				const tuple = this._leakageMon.getMostFrequentStack() ?? ['UNKNOWN stack', -1];
+				const error = new ListenerRefusalError(`${message}. HINT: Stack shows most frequent listener (${tuple[1]}-times)`, tuple[0]);
+				const errorHandler = this._options?.onListenerError || onUnexpectedError;
+				errorHandler(error);
+
+				return Disposable.None;
+			}
+
+			if (this._disposed) {
+				// todo: should we warn if a listener is added to a disposed emitter? This happens often
+				return Disposable.None;
+			}
+
+			if (thisArgs) {
+				callback = callback.bind(thisArgs);
+			}
+
+			const contained = new UniqueContainer(callback);
+
+			let removeMonitor: Function | undefined;
+			let stack: Stacktrace | undefined;
+			if (this._leakageMon && this._size >= Math.ceil(this._leakageMon.threshold * 0.2)) {
+				// check and record this emitter for potential leakage
+				contained.stack = Stacktrace.create();
+				removeMonitor = this._leakageMon.check(contained.stack, this._size + 1);
+			}
+
+			if (_enableDisposeWithListenerWarning) {
+				contained.stack = stack ?? Stacktrace.create();
+			}
+
+			if (!this._listeners) {
+				this._options?.onWillAddFirstListener?.(this);
+				this._listeners = contained;
+				this._options?.onDidAddFirstListener?.(this);
+			} else if (this._listeners instanceof UniqueContainer) {
+				this._deliveryQueue ??= new EventDeliveryQueuePrivate();
+				this._listeners = [this._listeners, contained];
+			} else {
+				this._listeners.push(contained);
+			}
+
+			this._size++;
+
+
+			const result = toDisposable(() => {
+				_listenerFinalizers?.unregister(result);
+				removeMonitor?.();
+				this._removeListener(contained);
+			});
+			if (disposables instanceof DisposableStore) {
+				disposables.add(result);
+			} else if (Array.isArray(disposables)) {
+				disposables.push(result);
+			}
+
+			if (_listenerFinalizers) {
+				const stack = new Error().stack!.split('\n').slice(2, 3).join('\n').trim();
+				const match = /(file:|vscode-file:\/\/vscode-app)?(\/[^:]*:\d+:\d+)/.exec(stack);
+				_listenerFinalizers.register(result, match?.[2] ?? stack, result);
+			}
+
+			return result;
+		};
+
+		return this._event;
+	}
+
+	private _removeListener(listener: ListenerContainer<T>) {
+		this._options?.onWillRemoveListener?.(this);
+
+		if (!this._listeners) {
+			return; // expected if a listener gets disposed
+		}
+
+		if (this._size === 1) {
+			this._listeners = undefined;
+			this._options?.onDidRemoveLastListener?.(this);
+			this._size = 0;
+			return;
+		}
+
+		// size > 1 which requires that listeners be a list:
+		const listeners = this._listeners as (ListenerContainer<T> | undefined)[];
+
+		const index = listeners.indexOf(listener);
+		if (index === -1) {
+			console.log('disposed?', this._disposed);
+			console.log('size?', this._size);
+			console.log('arr?', JSON.stringify(this._listeners));
+			throw new Error('Attempted to dispose unknown listener');
+		}
+
+		this._size--;
+		listeners[index] = undefined;
+
+		const adjustDeliveryQueue = this._deliveryQueue!.current === this;
+		if (this._size * compactionThreshold <= listeners.length) {
+			let n = 0;
+			for (let i = 0; i < listeners.length; i++) {
+				if (listeners[i]) {
+					listeners[n++] = listeners[i];
+				} else if (adjustDeliveryQueue) {
+					this._deliveryQueue!.end--;
+					if (n < this._deliveryQueue!.i) {
+						this._deliveryQueue!.i--;
+					}
+				}
+			}
+			listeners.length = n;
+		}
+	}
+
+	private _deliver(listener: undefined | UniqueContainer<(value: T) => void>, value: T) {
+		if (!listener) {
+			return;
+		}
+
+		const errorHandler = this._options?.onListenerError || onUnexpectedError;
+		if (!errorHandler) {
+			listener.value(value);
+			return;
+		}
+
+		try {
+			listener.value(value);
+		} catch (e) {
+			errorHandler(e);
+		}
+	}
+
+	/** Delivers items in the queue. Assumes the queue is ready to go. */
+	private _deliverQueue(dq: EventDeliveryQueuePrivate) {
+		const listeners = dq.current!._listeners! as (ListenerContainer<T> | undefined)[];
+		while (dq.i < dq.end) {
+			// important: dq.i is incremented before calling deliver() because it might reenter deliverQueue()
+			this._deliver(listeners[dq.i++], dq.value as T);
+		}
+		dq.reset();
+	}
+
+	/**
+	 * To be kept private to fire an event to
+	 * subscribers
+	 */
+	fire(event: T): void {
+		if (this._deliveryQueue?.current) {
+			this._deliverQueue(this._deliveryQueue);
+			this._perfMon?.stop(); // last fire() will have starting perfmon, stop it before starting the next dispatch
+		}
+
+		this._perfMon?.start(this._size);
+
+		if (!this._listeners) {
+			// no-op
+		} else if (this._listeners instanceof UniqueContainer) {
+			this._deliver(this._listeners, event);
+		} else {
+			const dq = this._deliveryQueue!;
+			dq.enqueue(this, event, this._listeners.length);
+			this._deliverQueue(dq);
+		}
+
+		this._perfMon?.stop();
+	}
+
+	hasListeners(): boolean {
+		return this._size > 0;
+	}
+}
+
+export interface EventDeliveryQueue {
+	_isEventDeliveryQueue: true;
+}
+
+export const createEventDeliveryQueue = (): EventDeliveryQueue => new EventDeliveryQueuePrivate();
+
+class EventDeliveryQueuePrivate implements EventDeliveryQueue {
+	declare _isEventDeliveryQueue: true;
+
+	/**
+	 * Index in current's listener list.
+	 */
+	public i = -1;
+
+	/**
+	 * The last index in the listener's list to deliver.
+	 */
+	public end = 0;
+
+	/**
+	 * Emitter currently being dispatched on. Emitter._listeners is always an array.
+	 */
+	public current?: Emitter<any>;
+	/**
+	 * Currently emitting value. Defined whenever `current` is.
+	 */
+	public value?: unknown;
+
+	public enqueue<T>(emitter: Emitter<T>, value: T, end: number) {
+		this.i = 0;
+		this.end = end;
+		this.current = emitter;
+		this.value = value;
+	}
+
+	public reset() {
+		this.i = this.end; // force any current emission loop to stop, mainly for during dispose
+		this.current = undefined;
+		this.value = undefined;
+	}
+}
+
+export interface IWaitUntil {
+	token: CancellationToken;
+	waitUntil(thenable: Promise<unknown>): void;
+}
+
+export type IWaitUntilData<T> = Omit<Omit<T, 'waitUntil'>, 'token'>;
+
+export class AsyncEmitter<T extends IWaitUntil> extends Emitter<T> {
+
+	private _asyncDeliveryQueue?: LinkedList<[(ev: T) => void, IWaitUntilData<T>]>;
+
+	async fireAsync(data: IWaitUntilData<T>, token: CancellationToken, promiseJoin?: (p: Promise<unknown>, listener: Function) => Promise<unknown>): Promise<void> {
+		if (!this._listeners) {
+			return;
+		}
+
+		if (!this._asyncDeliveryQueue) {
+			this._asyncDeliveryQueue = new LinkedList();
+		}
+
+		forEachListener(this._listeners, listener => this._asyncDeliveryQueue!.push([listener.value, data]));
+
+		while (this._asyncDeliveryQueue.size > 0 && !token.isCancellationRequested) {
+
+			const [listener, data] = this._asyncDeliveryQueue.shift()!;
+			const thenables: Promise<unknown>[] = [];
+
+			const event = <T>{
+				...data,
+				token,
+				waitUntil: (p: Promise<unknown>): void => {
+					if (Object.isFrozen(thenables)) {
+						throw new Error('waitUntil can NOT be called asynchronous');
+					}
+					if (promiseJoin) {
+						p = promiseJoin(p, listener);
+					}
+					thenables.push(p);
+				}
+			};
+
+			try {
+				listener(event);
+			} catch (e) {
+				onUnexpectedError(e);
+				continue;
+			}
+
+			// freeze thenables-collection to enforce sync-calls to
+			// wait until and then wait for all thenables to resolve
+			Object.freeze(thenables);
+
+			await Promise.allSettled(thenables).then(values => {
+				for (const value of values) {
+					if (value.status === 'rejected') {
+						onUnexpectedError(value.reason);
+					}
+				}
+			});
+		}
+	}
+}
+
+
+export class PauseableEmitter<T> extends Emitter<T> {
+
+	private _isPaused = 0;
+	protected _eventQueue = new LinkedList<T>();
+	private _mergeFn?: (input: T[]) => T;
+
+	public get isPaused(): boolean {
+		return this._isPaused !== 0;
+	}
+
+	constructor(options?: EmitterOptions & { merge?: (input: T[]) => T }) {
+		super(options);
+		this._mergeFn = options?.merge;
+	}
+
+	pause(): void {
+		this._isPaused++;
+	}
+
+	resume(): void {
+		if (this._isPaused !== 0 && --this._isPaused === 0) {
+			if (this._mergeFn) {
+				// use the merge function to create a single composite
+				// event. make a copy in case firing pauses this emitter
+				if (this._eventQueue.size > 0) {
+					const events = Array.from(this._eventQueue);
+					this._eventQueue.clear();
+					super.fire(this._mergeFn(events));
+				}
+
+			} else {
+				// no merging, fire each event individually and test
+				// that this emitter isn't paused halfway through
+				while (!this._isPaused && this._eventQueue.size !== 0) {
+					super.fire(this._eventQueue.shift()!);
+				}
+			}
+		}
+	}
+
+	override fire(event: T): void {
+		if (this._size) {
+			if (this._isPaused !== 0) {
+				this._eventQueue.push(event);
+			} else {
+				super.fire(event);
+			}
+		}
+	}
+}
+
+export class DebounceEmitter<T> extends PauseableEmitter<T> {
+
+	private readonly _delay: number;
+	private _handle: any | undefined;
+
+	constructor(options: EmitterOptions & { merge: (input: T[]) => T; delay?: number }) {
+		super(options);
+		this._delay = options.delay ?? 100;
+	}
+
+	override fire(event: T): void {
+		if (!this._handle) {
+			this.pause();
+			this._handle = setTimeout(() => {
+				this._handle = undefined;
+				this.resume();
+			}, this._delay);
+		}
+		super.fire(event);
+	}
+}
+
+/**
+ * An emitter which queue all events and then process them at the
+ * end of the event loop.
+ */
+export class MicrotaskEmitter<T> extends Emitter<T> {
+	private _queuedEvents: T[] = [];
+	private _mergeFn?: (input: T[]) => T;
+
+	constructor(options?: EmitterOptions & { merge?: (input: T[]) => T }) {
+		super(options);
+		this._mergeFn = options?.merge;
+	}
+	override fire(event: T): void {
+
+		if (!this.hasListeners()) {
+			return;
+		}
+
+		this._queuedEvents.push(event);
+		if (this._queuedEvents.length === 1) {
+			queueMicrotask(() => {
+				if (this._mergeFn) {
+					super.fire(this._mergeFn(this._queuedEvents));
+				} else {
+					this._queuedEvents.forEach(e => super.fire(e));
+				}
+				this._queuedEvents = [];
+			});
+		}
+	}
+}
+
+/**
+ * An event emitter that multiplexes many events into a single event.
+ *
+ * @example Listen to the `onData` event of all `Thing`s, dynamically adding and removing `Thing`s
+ * to the multiplexer as needed.
+ *
+ * ```typescript
+ * const anythingDataMultiplexer = new EventMultiplexer<{ data: string }>();
+ *
+ * const thingListeners = DisposableMap<Thing, IDisposable>();
+ *
+ * thingService.onDidAddThing(thing => {
+ *   thingListeners.set(thing, anythingDataMultiplexer.add(thing.onData);
+ * });
+ * thingService.onDidRemoveThing(thing => {
+ *   thingListeners.deleteAndDispose(thing);
+ * });
+ *
+ * anythingDataMultiplexer.event(e => {
+ *   console.log('Something fired data ' + e.data)
+ * });
+ * ```
+ */
+export class EventMultiplexer<T> implements IDisposable {
+
+	private readonly emitter: Emitter<T>;
+	private hasListeners = false;
+	private events: { event: Event<T>; listener: IDisposable | null }[] = [];
+
+	constructor() {
+		this.emitter = new Emitter<T>({
+			onWillAddFirstListener: () => this.onFirstListenerAdd(),
+			onDidRemoveLastListener: () => this.onLastListenerRemove()
+		});
+	}
+
+	get event(): Event<T> {
+		return this.emitter.event;
+	}
+
+	add(event: Event<T>): IDisposable {
+		const e = { event: event, listener: null };
+		this.events.push(e);
+
+		if (this.hasListeners) {
+			this.hook(e);
+		}
+
+		const dispose = () => {
+			if (this.hasListeners) {
+				this.unhook(e);
+			}
+
+			const idx = this.events.indexOf(e);
+			this.events.splice(idx, 1);
+		};
+
+		return toDisposable(createSingleCallFunction(dispose));
+	}
+
+	private onFirstListenerAdd(): void {
+		this.hasListeners = true;
+		this.events.forEach(e => this.hook(e));
+	}
+
+	private onLastListenerRemove(): void {
+		this.hasListeners = false;
+		this.events.forEach(e => this.unhook(e));
+	}
+
+	private hook(e: { event: Event<T>; listener: IDisposable | null }): void {
+		e.listener = e.event(r => this.emitter.fire(r));
+	}
+
+	private unhook(e: { event: Event<T>; listener: IDisposable | null }): void {
+		e.listener?.dispose();
+		e.listener = null;
+	}
+
+	dispose(): void {
+		this.emitter.dispose();
+
+		for (const e of this.events) {
+			e.listener?.dispose();
+		}
+		this.events = [];
+	}
+}
+
+export interface IDynamicListEventMultiplexer<TEventType> extends IDisposable {
+	readonly event: Event<TEventType>;
+}
+export class DynamicListEventMultiplexer<TItem, TEventType> implements IDynamicListEventMultiplexer<TEventType> {
+	private readonly _store = new DisposableStore();
+
+	readonly event: Event<TEventType>;
+
+	constructor(
+		items: TItem[],
+		onAddItem: Event<TItem>,
+		onRemoveItem: Event<TItem>,
+		getEvent: (item: TItem) => Event<TEventType>
+	) {
+		const multiplexer = this._store.add(new EventMultiplexer<TEventType>());
+		const itemListeners = this._store.add(new DisposableMap<TItem, IDisposable>());
+
+		function addItem(instance: TItem) {
+			itemListeners.set(instance, multiplexer.add(getEvent(instance)));
+		}
+
+		// Existing items
+		for (const instance of items) {
+			addItem(instance);
+		}
+
+		// Added items
+		this._store.add(onAddItem(instance => {
+			addItem(instance);
+		}));
+
+		// Removed items
+		this._store.add(onRemoveItem(instance => {
+			itemListeners.deleteAndDispose(instance);
+		}));
+
+		this.event = multiplexer.event;
+	}
+
+	dispose() {
+		this._store.dispose();
+	}
+}
+
+/**
+ * The EventBufferer is useful in situations in which you want
+ * to delay firing your events during some code.
+ * You can wrap that code and be sure that the event will not
+ * be fired during that wrap.
+ *
+ * ```
+ * const emitter: Emitter;
+ * const delayer = new EventDelayer();
+ * const delayedEvent = delayer.wrapEvent(emitter.event);
+ *
+ * delayedEvent(console.log);
+ *
+ * delayer.bufferEvents(() => {
+ *   emitter.fire(); // event will not be fired yet
+ * });
+ *
+ * // event will only be fired at this point
+ * ```
+ */
+export class EventBufferer {
+
+	private data: { buffers: Function[] }[] = [];
+
+	wrapEvent<T>(event: Event<T>): Event<T>;
+	wrapEvent<T>(event: Event<T>, reduce: (last: T | undefined, event: T) => T): Event<T>;
+	wrapEvent<T, O>(event: Event<T>, reduce: (last: O | undefined, event: T) => O, initial: O): Event<O>;
+	wrapEvent<T, O>(event: Event<T>, reduce?: (last: T | O | undefined, event: T) => T | O, initial?: O): Event<O | T> {
+		return (listener, thisArgs?, disposables?) => {
+			return event(i => {
+				const data = this.data[this.data.length - 1];
+
+				// Non-reduce scenario
+				if (!reduce) {
+					// Buffering case
+					if (data) {
+						data.buffers.push(() => listener.call(thisArgs, i));
+					} else {
+						// Not buffering case
+						listener.call(thisArgs, i);
+					}
+					return;
+				}
+
+				// Reduce scenario
+				const reduceData = data as typeof data & {
+					/**
+					 * The accumulated items that will be reduced.
+					 */
+					items?: T[];
+					/**
+					 * The reduced result cached to be shared with other listeners.
+					 */
+					reducedResult?: T | O;
+				};
+
+				// Not buffering case
+				if (!reduceData) {
+					// TODO: Is there a way to cache this reduce call for all listeners?
+					listener.call(thisArgs, reduce(initial, i));
+					return;
+				}
+
+				// Buffering case
+				reduceData.items ??= [];
+				reduceData.items.push(i);
+				if (reduceData.buffers.length === 0) {
+					// Include a single buffered function that will reduce all events when we're done buffering events
+					data.buffers.push(() => {
+						// cache the reduced result so that the value can be shared across all listeners
+						reduceData.reducedResult ??= initial
+							? reduceData.items!.reduce(reduce as (last: O | undefined, event: T) => O, initial)
+							: reduceData.items!.reduce(reduce as (last: T | undefined, event: T) => T);
+						listener.call(thisArgs, reduceData.reducedResult);
+					});
+				}
+			}, undefined, disposables);
+		};
+	}
+
+	bufferEvents<R = void>(fn: () => R): R {
+		const data = { buffers: new Array<Function>() };
+		this.data.push(data);
+		const r = fn();
+		this.data.pop();
+		data.buffers.forEach(flush => flush());
+		return r;
+	}
+}
+
+/**
+ * A Relay is an event forwarder which functions as a replugabble event pipe.
+ * Once created, you can connect an input event to it and it will simply forward
+ * events from that input event through its own `event` property. The `input`
+ * can be changed at any point in time.
+ */
+export class Relay<T> implements IDisposable {
+
+	private listening = false;
+	private inputEvent: Event<T> = Event.None;
+	private inputEventListener: IDisposable = Disposable.None;
+
+	private readonly emitter = new Emitter<T>({
+		onDidAddFirstListener: () => {
+			this.listening = true;
+			this.inputEventListener = this.inputEvent(this.emitter.fire, this.emitter);
+		},
+		onDidRemoveLastListener: () => {
+			this.listening = false;
+			this.inputEventListener.dispose();
+		}
+	});
+
+	readonly event: Event<T> = this.emitter.event;
+
+	set input(event: Event<T>) {
+		this.inputEvent = event;
+
+		if (this.listening) {
+			this.inputEventListener.dispose();
+			this.inputEventListener = event(this.emitter.fire, this.emitter);
+		}
+	}
+
+	dispose() {
+		this.inputEventListener.dispose();
+		this.emitter.dispose();
+	}
+}
+
+export interface IValueWithChangeEvent<T> {
+	readonly onDidChange: Event<void>;
+	get value(): T;
+}
+
+export class ValueWithChangeEvent<T> implements IValueWithChangeEvent<T> {
+	public static const<T>(value: T): IValueWithChangeEvent<T> {
+		return new ConstValueWithChangeEvent(value);
+	}
+
+	private readonly _onDidChange = new Emitter<void>();
+	readonly onDidChange: Event<void> = this._onDidChange.event;
+
+	constructor(private _value: T) { }
+
+	get value(): T {
+		return this._value;
+	}
+
+	set value(value: T) {
+		if (value !== this._value) {
+			this._value = value;
+			this._onDidChange.fire(undefined);
+		}
+	}
+}
+
+class ConstValueWithChangeEvent<T> implements IValueWithChangeEvent<T> {
+	public readonly onDidChange: Event<void> = Event.None;
+
+	constructor(readonly value: T) { }
+}
diff --git a/src/vs/base/common/functional.ts b/src/vs/base/common/functional.ts
new file mode 100644
index 0000000000..d580cf37f1
--- /dev/null
+++ b/src/vs/base/common/functional.ts
@@ -0,0 +1,32 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+/**
+ * Given a function, returns a function that is only calling that function once.
+ */
+export function createSingleCallFunction<T extends Function>(this: unknown, fn: T, fnDidRunCallback?: () => void): T {
+	const _this = this;
+	let didCall = false;
+	let result: unknown;
+
+	return function () {
+		if (didCall) {
+			return result;
+		}
+
+		didCall = true;
+		if (fnDidRunCallback) {
+			try {
+				result = fn.apply(_this, arguments);
+			} finally {
+				fnDidRunCallback();
+			}
+		} else {
+			result = fn.apply(_this, arguments);
+		}
+
+		return result;
+	} as unknown as T;
+}
diff --git a/src/vs/base/common/hash.ts b/src/vs/base/common/hash.ts
new file mode 100644
index 0000000000..76217c4730
--- /dev/null
+++ b/src/vs/base/common/hash.ts
@@ -0,0 +1,316 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import * as strings from 'vs/base/common/strings';
+
+/**
+ * Return a hash value for an object.
+ */
+export function hash(obj: any): number {
+	return doHash(obj, 0);
+}
+
+export function doHash(obj: any, hashVal: number): number {
+	switch (typeof obj) {
+		case 'object':
+			if (obj === null) {
+				return numberHash(349, hashVal);
+			} else if (Array.isArray(obj)) {
+				return arrayHash(obj, hashVal);
+			}
+			return objectHash(obj, hashVal);
+		case 'string':
+			return stringHash(obj, hashVal);
+		case 'boolean':
+			return booleanHash(obj, hashVal);
+		case 'number':
+			return numberHash(obj, hashVal);
+		case 'undefined':
+			return numberHash(937, hashVal);
+		default:
+			return numberHash(617, hashVal);
+	}
+}
+
+export function numberHash(val: number, initialHashVal: number): number {
+	return (((initialHashVal << 5) - initialHashVal) + val) | 0;  // hashVal * 31 + ch, keep as int32
+}
+
+function booleanHash(b: boolean, initialHashVal: number): number {
+	return numberHash(b ? 433 : 863, initialHashVal);
+}
+
+export function stringHash(s: string, hashVal: number) {
+	hashVal = numberHash(149417, hashVal);
+	for (let i = 0, length = s.length; i < length; i++) {
+		hashVal = numberHash(s.charCodeAt(i), hashVal);
+	}
+	return hashVal;
+}
+
+function arrayHash(arr: any[], initialHashVal: number): number {
+	initialHashVal = numberHash(104579, initialHashVal);
+	return arr.reduce((hashVal, item) => doHash(item, hashVal), initialHashVal);
+}
+
+function objectHash(obj: any, initialHashVal: number): number {
+	initialHashVal = numberHash(181387, initialHashVal);
+	return Object.keys(obj).sort().reduce((hashVal, key) => {
+		hashVal = stringHash(key, hashVal);
+		return doHash(obj[key], hashVal);
+	}, initialHashVal);
+}
+
+export class Hasher {
+
+	private _value = 0;
+
+	get value(): number {
+		return this._value;
+	}
+
+	hash(obj: any): number {
+		this._value = doHash(obj, this._value);
+		return this._value;
+	}
+}
+
+const enum SHA1Constant {
+	BLOCK_SIZE = 64, // 512 / 8
+	UNICODE_REPLACEMENT = 0xFFFD,
+}
+
+function leftRotate(value: number, bits: number, totalBits: number = 32): number {
+	// delta + bits = totalBits
+	const delta = totalBits - bits;
+
+	// All ones, expect `delta` zeros aligned to the right
+	const mask = ~((1 << delta) - 1);
+
+	// Join (value left-shifted `bits` bits) with (masked value right-shifted `delta` bits)
+	return ((value << bits) | ((mask & value) >>> delta)) >>> 0;
+}
+
+function fill(dest: Uint8Array, index: number = 0, count: number = dest.byteLength, value: number = 0): void {
+	for (let i = 0; i < count; i++) {
+		dest[index + i] = value;
+	}
+}
+
+function leftPad(value: string, length: number, char: string = '0'): string {
+	while (value.length < length) {
+		value = char + value;
+	}
+	return value;
+}
+
+export function toHexString(buffer: ArrayBuffer): string;
+export function toHexString(value: number, bitsize?: number): string;
+export function toHexString(bufferOrValue: ArrayBuffer | number, bitsize: number = 32): string {
+	if (bufferOrValue instanceof ArrayBuffer) {
+		return Array.from(new Uint8Array(bufferOrValue)).map(b => b.toString(16).padStart(2, '0')).join('');
+	}
+
+	return leftPad((bufferOrValue >>> 0).toString(16), bitsize / 4);
+}
+
+/**
+ * A SHA1 implementation that works with strings and does not allocate.
+ */
+export class StringSHA1 {
+	private static _bigBlock32 = new DataView(new ArrayBuffer(320)); // 80 * 4 = 320
+
+	private _h0 = 0x67452301;
+	private _h1 = 0xEFCDAB89;
+	private _h2 = 0x98BADCFE;
+	private _h3 = 0x10325476;
+	private _h4 = 0xC3D2E1F0;
+
+	private readonly _buff: Uint8Array;
+	private readonly _buffDV: DataView;
+	private _buffLen: number;
+	private _totalLen: number;
+	private _leftoverHighSurrogate: number;
+	private _finished: boolean;
+
+	constructor() {
+		this._buff = new Uint8Array(SHA1Constant.BLOCK_SIZE + 3 /* to fit any utf-8 */);
+		this._buffDV = new DataView(this._buff.buffer);
+		this._buffLen = 0;
+		this._totalLen = 0;
+		this._leftoverHighSurrogate = 0;
+		this._finished = false;
+	}
+
+	public update(str: string): void {
+		const strLen = str.length;
+		if (strLen === 0) {
+			return;
+		}
+
+		const buff = this._buff;
+		let buffLen = this._buffLen;
+		let leftoverHighSurrogate = this._leftoverHighSurrogate;
+		let charCode: number;
+		let offset: number;
+
+		if (leftoverHighSurrogate !== 0) {
+			charCode = leftoverHighSurrogate;
+			offset = -1;
+			leftoverHighSurrogate = 0;
+		} else {
+			charCode = str.charCodeAt(0);
+			offset = 0;
+		}
+
+		while (true) {
+			let codePoint = charCode;
+			if (strings.isHighSurrogate(charCode)) {
+				if (offset + 1 < strLen) {
+					const nextCharCode = str.charCodeAt(offset + 1);
+					if (strings.isLowSurrogate(nextCharCode)) {
+						offset++;
+						codePoint = strings.computeCodePoint(charCode, nextCharCode);
+					} else {
+						// illegal => unicode replacement character
+						codePoint = SHA1Constant.UNICODE_REPLACEMENT;
+					}
+				} else {
+					// last character is a surrogate pair
+					leftoverHighSurrogate = charCode;
+					break;
+				}
+			} else if (strings.isLowSurrogate(charCode)) {
+				// illegal => unicode replacement character
+				codePoint = SHA1Constant.UNICODE_REPLACEMENT;
+			}
+
+			buffLen = this._push(buff, buffLen, codePoint);
+			offset++;
+			if (offset < strLen) {
+				charCode = str.charCodeAt(offset);
+			} else {
+				break;
+			}
+		}
+
+		this._buffLen = buffLen;
+		this._leftoverHighSurrogate = leftoverHighSurrogate;
+	}
+
+	private _push(buff: Uint8Array, buffLen: number, codePoint: number): number {
+		if (codePoint < 0x0080) {
+			buff[buffLen++] = codePoint;
+		} else if (codePoint < 0x0800) {
+			buff[buffLen++] = 0b11000000 | ((codePoint & 0b00000000000000000000011111000000) >>> 6);
+			buff[buffLen++] = 0b10000000 | ((codePoint & 0b00000000000000000000000000111111) >>> 0);
+		} else if (codePoint < 0x10000) {
+			buff[buffLen++] = 0b11100000 | ((codePoint & 0b00000000000000001111000000000000) >>> 12);
+			buff[buffLen++] = 0b10000000 | ((codePoint & 0b00000000000000000000111111000000) >>> 6);
+			buff[buffLen++] = 0b10000000 | ((codePoint & 0b00000000000000000000000000111111) >>> 0);
+		} else {
+			buff[buffLen++] = 0b11110000 | ((codePoint & 0b00000000000111000000000000000000) >>> 18);
+			buff[buffLen++] = 0b10000000 | ((codePoint & 0b00000000000000111111000000000000) >>> 12);
+			buff[buffLen++] = 0b10000000 | ((codePoint & 0b00000000000000000000111111000000) >>> 6);
+			buff[buffLen++] = 0b10000000 | ((codePoint & 0b00000000000000000000000000111111) >>> 0);
+		}
+
+		if (buffLen >= SHA1Constant.BLOCK_SIZE) {
+			this._step();
+			buffLen -= SHA1Constant.BLOCK_SIZE;
+			this._totalLen += SHA1Constant.BLOCK_SIZE;
+			// take last 3 in case of UTF8 overflow
+			buff[0] = buff[SHA1Constant.BLOCK_SIZE + 0];
+			buff[1] = buff[SHA1Constant.BLOCK_SIZE + 1];
+			buff[2] = buff[SHA1Constant.BLOCK_SIZE + 2];
+		}
+
+		return buffLen;
+	}
+
+	public digest(): string {
+		if (!this._finished) {
+			this._finished = true;
+			if (this._leftoverHighSurrogate) {
+				// illegal => unicode replacement character
+				this._leftoverHighSurrogate = 0;
+				this._buffLen = this._push(this._buff, this._buffLen, SHA1Constant.UNICODE_REPLACEMENT);
+			}
+			this._totalLen += this._buffLen;
+			this._wrapUp();
+		}
+
+		return toHexString(this._h0) + toHexString(this._h1) + toHexString(this._h2) + toHexString(this._h3) + toHexString(this._h4);
+	}
+
+	private _wrapUp(): void {
+		this._buff[this._buffLen++] = 0x80;
+		fill(this._buff, this._buffLen);
+
+		if (this._buffLen > 56) {
+			this._step();
+			fill(this._buff);
+		}
+
+		// this will fit because the mantissa can cover up to 52 bits
+		const ml = 8 * this._totalLen;
+
+		this._buffDV.setUint32(56, Math.floor(ml / 4294967296), false);
+		this._buffDV.setUint32(60, ml % 4294967296, false);
+
+		this._step();
+	}
+
+	private _step(): void {
+		const bigBlock32 = StringSHA1._bigBlock32;
+		const data = this._buffDV;
+
+		for (let j = 0; j < 64 /* 16*4 */; j += 4) {
+			bigBlock32.setUint32(j, data.getUint32(j, false), false);
+		}
+
+		for (let j = 64; j < 320 /* 80*4 */; j += 4) {
+			bigBlock32.setUint32(j, leftRotate((bigBlock32.getUint32(j - 12, false) ^ bigBlock32.getUint32(j - 32, false) ^ bigBlock32.getUint32(j - 56, false) ^ bigBlock32.getUint32(j - 64, false)), 1), false);
+		}
+
+		let a = this._h0;
+		let b = this._h1;
+		let c = this._h2;
+		let d = this._h3;
+		let e = this._h4;
+
+		let f: number, k: number;
+		let temp: number;
+
+		for (let j = 0; j < 80; j++) {
+			if (j < 20) {
+				f = (b & c) | ((~b) & d);
+				k = 0x5A827999;
+			} else if (j < 40) {
+				f = b ^ c ^ d;
+				k = 0x6ED9EBA1;
+			} else if (j < 60) {
+				f = (b & c) | (b & d) | (c & d);
+				k = 0x8F1BBCDC;
+			} else {
+				f = b ^ c ^ d;
+				k = 0xCA62C1D6;
+			}
+
+			temp = (leftRotate(a, 5) + f + e + k + bigBlock32.getUint32(j * 4, false)) & 0xffffffff;
+			e = d;
+			d = c;
+			c = leftRotate(b, 30);
+			b = a;
+			a = temp;
+		}
+
+		this._h0 = (this._h0 + a) & 0xffffffff;
+		this._h1 = (this._h1 + b) & 0xffffffff;
+		this._h2 = (this._h2 + c) & 0xffffffff;
+		this._h3 = (this._h3 + d) & 0xffffffff;
+		this._h4 = (this._h4 + e) & 0xffffffff;
+	}
+}
diff --git a/src/vs/base/common/iterator.ts b/src/vs/base/common/iterator.ts
new file mode 100644
index 0000000000..c329ed6dc7
--- /dev/null
+++ b/src/vs/base/common/iterator.ts
@@ -0,0 +1,159 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+export namespace Iterable {
+
+	export function is<T = any>(thing: any): thing is Iterable<T> {
+		return thing && typeof thing === 'object' && typeof thing[Symbol.iterator] === 'function';
+	}
+
+	const _empty: Iterable<any> = Object.freeze([]);
+	export function empty<T = any>(): Iterable<T> {
+		return _empty;
+	}
+
+	export function* single<T>(element: T): Iterable<T> {
+		yield element;
+	}
+
+	export function wrap<T>(iterableOrElement: Iterable<T> | T): Iterable<T> {
+		if (is(iterableOrElement)) {
+			return iterableOrElement;
+		} else {
+			return single(iterableOrElement);
+		}
+	}
+
+	export function from<T>(iterable: Iterable<T> | undefined | null): Iterable<T> {
+		return iterable || _empty;
+	}
+
+	export function* reverse<T>(array: Array<T>): Iterable<T> {
+		for (let i = array.length - 1; i >= 0; i--) {
+			yield array[i];
+		}
+	}
+
+	export function isEmpty<T>(iterable: Iterable<T> | undefined | null): boolean {
+		return !iterable || iterable[Symbol.iterator]().next().done === true;
+	}
+
+	export function first<T>(iterable: Iterable<T>): T | undefined {
+		return iterable[Symbol.iterator]().next().value;
+	}
+
+	export function some<T>(iterable: Iterable<T>, predicate: (t: T, i: number) => unknown): boolean {
+		let i = 0;
+		for (const element of iterable) {
+			if (predicate(element, i++)) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+	export function find<T, R extends T>(iterable: Iterable<T>, predicate: (t: T) => t is R): R | undefined;
+	export function find<T>(iterable: Iterable<T>, predicate: (t: T) => boolean): T | undefined;
+	export function find<T>(iterable: Iterable<T>, predicate: (t: T) => boolean): T | undefined {
+		for (const element of iterable) {
+			if (predicate(element)) {
+				return element;
+			}
+		}
+
+		return undefined;
+	}
+
+	export function filter<T, R extends T>(iterable: Iterable<T>, predicate: (t: T) => t is R): Iterable<R>;
+	export function filter<T>(iterable: Iterable<T>, predicate: (t: T) => boolean): Iterable<T>;
+	export function* filter<T>(iterable: Iterable<T>, predicate: (t: T) => boolean): Iterable<T> {
+		for (const element of iterable) {
+			if (predicate(element)) {
+				yield element;
+			}
+		}
+	}
+
+	export function* map<T, R>(iterable: Iterable<T>, fn: (t: T, index: number) => R): Iterable<R> {
+		let index = 0;
+		for (const element of iterable) {
+			yield fn(element, index++);
+		}
+	}
+
+	export function* flatMap<T, R>(iterable: Iterable<T>, fn: (t: T, index: number) => Iterable<R>): Iterable<R> {
+		let index = 0;
+		for (const element of iterable) {
+			yield* fn(element, index++);
+		}
+	}
+
+	export function* concat<T>(...iterables: Iterable<T>[]): Iterable<T> {
+		for (const iterable of iterables) {
+			yield* iterable;
+		}
+	}
+
+	export function reduce<T, R>(iterable: Iterable<T>, reducer: (previousValue: R, currentValue: T) => R, initialValue: R): R {
+		let value = initialValue;
+		for (const element of iterable) {
+			value = reducer(value, element);
+		}
+		return value;
+	}
+
+	/**
+	 * Returns an iterable slice of the array, with the same semantics as `array.slice()`.
+	 */
+	export function* slice<T>(arr: ReadonlyArray<T>, from: number, to = arr.length): Iterable<T> {
+		if (from < 0) {
+			from += arr.length;
+		}
+
+		if (to < 0) {
+			to += arr.length;
+		} else if (to > arr.length) {
+			to = arr.length;
+		}
+
+		for (; from < to; from++) {
+			yield arr[from];
+		}
+	}
+
+	/**
+	 * Consumes `atMost` elements from iterable and returns the consumed elements,
+	 * and an iterable for the rest of the elements.
+	 */
+	export function consume<T>(iterable: Iterable<T>, atMost: number = Number.POSITIVE_INFINITY): [T[], Iterable<T>] {
+		const consumed: T[] = [];
+
+		if (atMost === 0) {
+			return [consumed, iterable];
+		}
+
+		const iterator = iterable[Symbol.iterator]();
+
+		for (let i = 0; i < atMost; i++) {
+			const next = iterator.next();
+
+			if (next.done) {
+				return [consumed, Iterable.empty()];
+			}
+
+			consumed.push(next.value);
+		}
+
+		return [consumed, { [Symbol.iterator]() { return iterator; } }];
+	}
+
+	export async function asyncToArray<T>(iterable: AsyncIterable<T>): Promise<T[]> {
+		const result: T[] = [];
+		for await (const item of iterable) {
+			result.push(item);
+		}
+		return Promise.resolve(result);
+	}
+}
diff --git a/src/vs/base/common/keyCodes.ts b/src/vs/base/common/keyCodes.ts
new file mode 100644
index 0000000000..1d336dfaee
--- /dev/null
+++ b/src/vs/base/common/keyCodes.ts
@@ -0,0 +1,526 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+/**
+ * Virtual Key Codes, the value does not hold any inherent meaning.
+ * Inspired somewhat from https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx
+ * But these are "more general", as they should work across browsers & OS`s.
+ */
+export const enum KeyCode {
+	DependsOnKbLayout = -1,
+
+	/**
+	 * Placed first to cover the 0 value of the enum.
+	 */
+	Unknown = 0,
+
+	Backspace,
+	Tab,
+	Enter,
+	Shift,
+	Ctrl,
+	Alt,
+	PauseBreak,
+	CapsLock,
+	Escape,
+	Space,
+	PageUp,
+	PageDown,
+	End,
+	Home,
+	LeftArrow,
+	UpArrow,
+	RightArrow,
+	DownArrow,
+	Insert,
+	Delete,
+
+	Digit0,
+	Digit1,
+	Digit2,
+	Digit3,
+	Digit4,
+	Digit5,
+	Digit6,
+	Digit7,
+	Digit8,
+	Digit9,
+
+	KeyA,
+	KeyB,
+	KeyC,
+	KeyD,
+	KeyE,
+	KeyF,
+	KeyG,
+	KeyH,
+	KeyI,
+	KeyJ,
+	KeyK,
+	KeyL,
+	KeyM,
+	KeyN,
+	KeyO,
+	KeyP,
+	KeyQ,
+	KeyR,
+	KeyS,
+	KeyT,
+	KeyU,
+	KeyV,
+	KeyW,
+	KeyX,
+	KeyY,
+	KeyZ,
+
+	Meta,
+	ContextMenu,
+
+	F1,
+	F2,
+	F3,
+	F4,
+	F5,
+	F6,
+	F7,
+	F8,
+	F9,
+	F10,
+	F11,
+	F12,
+	F13,
+	F14,
+	F15,
+	F16,
+	F17,
+	F18,
+	F19,
+	F20,
+	F21,
+	F22,
+	F23,
+	F24,
+
+	NumLock,
+	ScrollLock,
+
+	/**
+	 * Used for miscellaneous characters; it can vary by keyboard.
+	 * For the US standard keyboard, the ';:' key
+	 */
+	Semicolon,
+	/**
+	 * For any country/region, the '+' key
+	 * For the US standard keyboard, the '=+' key
+	 */
+	Equal,
+	/**
+	 * For any country/region, the ',' key
+	 * For the US standard keyboard, the ',<' key
+	 */
+	Comma,
+	/**
+	 * For any country/region, the '-' key
+	 * For the US standard keyboard, the '-_' key
+	 */
+	Minus,
+	/**
+	 * For any country/region, the '.' key
+	 * For the US standard keyboard, the '.>' key
+	 */
+	Period,
+	/**
+	 * Used for miscellaneous characters; it can vary by keyboard.
+	 * For the US standard keyboard, the '/?' key
+	 */
+	Slash,
+	/**
+	 * Used for miscellaneous characters; it can vary by keyboard.
+	 * For the US standard keyboard, the '`~' key
+	 */
+	Backquote,
+	/**
+	 * Used for miscellaneous characters; it can vary by keyboard.
+	 * For the US standard keyboard, the '[{' key
+	 */
+	BracketLeft,
+	/**
+	 * Used for miscellaneous characters; it can vary by keyboard.
+	 * For the US standard keyboard, the '\|' key
+	 */
+	Backslash,
+	/**
+	 * Used for miscellaneous characters; it can vary by keyboard.
+	 * For the US standard keyboard, the ']}' key
+	 */
+	BracketRight,
+	/**
+	 * Used for miscellaneous characters; it can vary by keyboard.
+	 * For the US standard keyboard, the ''"' key
+	 */
+	Quote,
+	/**
+	 * Used for miscellaneous characters; it can vary by keyboard.
+	 */
+	OEM_8,
+	/**
+	 * Either the angle bracket key or the backslash key on the RT 102-key keyboard.
+	 */
+	IntlBackslash,
+
+	Numpad0, // VK_NUMPAD0, 0x60, Numeric keypad 0 key
+	Numpad1, // VK_NUMPAD1, 0x61, Numeric keypad 1 key
+	Numpad2, // VK_NUMPAD2, 0x62, Numeric keypad 2 key
+	Numpad3, // VK_NUMPAD3, 0x63, Numeric keypad 3 key
+	Numpad4, // VK_NUMPAD4, 0x64, Numeric keypad 4 key
+	Numpad5, // VK_NUMPAD5, 0x65, Numeric keypad 5 key
+	Numpad6, // VK_NUMPAD6, 0x66, Numeric keypad 6 key
+	Numpad7, // VK_NUMPAD7, 0x67, Numeric keypad 7 key
+	Numpad8, // VK_NUMPAD8, 0x68, Numeric keypad 8 key
+	Numpad9, // VK_NUMPAD9, 0x69, Numeric keypad 9 key
+
+	NumpadMultiply,	// VK_MULTIPLY, 0x6A, Multiply key
+	NumpadAdd,		// VK_ADD, 0x6B, Add key
+	NUMPAD_SEPARATOR,	// VK_SEPARATOR, 0x6C, Separator key
+	NumpadSubtract,	// VK_SUBTRACT, 0x6D, Subtract key
+	NumpadDecimal,	// VK_DECIMAL, 0x6E, Decimal key
+	NumpadDivide,	// VK_DIVIDE, 0x6F,
+
+	/**
+	 * Cover all key codes when IME is processing input.
+	 */
+	KEY_IN_COMPOSITION,
+
+	ABNT_C1, // Brazilian (ABNT) Keyboard
+	ABNT_C2, // Brazilian (ABNT) Keyboard
+
+	AudioVolumeMute,
+	AudioVolumeUp,
+	AudioVolumeDown,
+
+	BrowserSearch,
+	BrowserHome,
+	BrowserBack,
+	BrowserForward,
+
+	MediaTrackNext,
+	MediaTrackPrevious,
+	MediaStop,
+	MediaPlayPause,
+	LaunchMediaPlayer,
+	LaunchMail,
+	LaunchApp2,
+
+	/**
+	 * VK_CLEAR, 0x0C, CLEAR key
+	 */
+	Clear,
+
+	/**
+	 * Placed last to cover the length of the enum.
+	 * Please do not depend on this value!
+	 */
+	MAX_VALUE
+}
+
+/**
+ * keyboardEvent.code
+ */
+export const enum ScanCode {
+	DependsOnKbLayout = -1,
+	None,
+	Hyper,
+	Super,
+	Fn,
+	FnLock,
+	Suspend,
+	Resume,
+	Turbo,
+	Sleep,
+	WakeUp,
+	KeyA,
+	KeyB,
+	KeyC,
+	KeyD,
+	KeyE,
+	KeyF,
+	KeyG,
+	KeyH,
+	KeyI,
+	KeyJ,
+	KeyK,
+	KeyL,
+	KeyM,
+	KeyN,
+	KeyO,
+	KeyP,
+	KeyQ,
+	KeyR,
+	KeyS,
+	KeyT,
+	KeyU,
+	KeyV,
+	KeyW,
+	KeyX,
+	KeyY,
+	KeyZ,
+	Digit1,
+	Digit2,
+	Digit3,
+	Digit4,
+	Digit5,
+	Digit6,
+	Digit7,
+	Digit8,
+	Digit9,
+	Digit0,
+	Enter,
+	Escape,
+	Backspace,
+	Tab,
+	Space,
+	Minus,
+	Equal,
+	BracketLeft,
+	BracketRight,
+	Backslash,
+	IntlHash,
+	Semicolon,
+	Quote,
+	Backquote,
+	Comma,
+	Period,
+	Slash,
+	CapsLock,
+	F1,
+	F2,
+	F3,
+	F4,
+	F5,
+	F6,
+	F7,
+	F8,
+	F9,
+	F10,
+	F11,
+	F12,
+	PrintScreen,
+	ScrollLock,
+	Pause,
+	Insert,
+	Home,
+	PageUp,
+	Delete,
+	End,
+	PageDown,
+	ArrowRight,
+	ArrowLeft,
+	ArrowDown,
+	ArrowUp,
+	NumLock,
+	NumpadDivide,
+	NumpadMultiply,
+	NumpadSubtract,
+	NumpadAdd,
+	NumpadEnter,
+	Numpad1,
+	Numpad2,
+	Numpad3,
+	Numpad4,
+	Numpad5,
+	Numpad6,
+	Numpad7,
+	Numpad8,
+	Numpad9,
+	Numpad0,
+	NumpadDecimal,
+	IntlBackslash,
+	ContextMenu,
+	Power,
+	NumpadEqual,
+	F13,
+	F14,
+	F15,
+	F16,
+	F17,
+	F18,
+	F19,
+	F20,
+	F21,
+	F22,
+	F23,
+	F24,
+	Open,
+	Help,
+	Select,
+	Again,
+	Undo,
+	Cut,
+	Copy,
+	Paste,
+	Find,
+	AudioVolumeMute,
+	AudioVolumeUp,
+	AudioVolumeDown,
+	NumpadComma,
+	IntlRo,
+	KanaMode,
+	IntlYen,
+	Convert,
+	NonConvert,
+	Lang1,
+	Lang2,
+	Lang3,
+	Lang4,
+	Lang5,
+	Abort,
+	Props,
+	NumpadParenLeft,
+	NumpadParenRight,
+	NumpadBackspace,
+	NumpadMemoryStore,
+	NumpadMemoryRecall,
+	NumpadMemoryClear,
+	NumpadMemoryAdd,
+	NumpadMemorySubtract,
+	NumpadClear,
+	NumpadClearEntry,
+	ControlLeft,
+	ShiftLeft,
+	AltLeft,
+	MetaLeft,
+	ControlRight,
+	ShiftRight,
+	AltRight,
+	MetaRight,
+	BrightnessUp,
+	BrightnessDown,
+	MediaPlay,
+	MediaRecord,
+	MediaFastForward,
+	MediaRewind,
+	MediaTrackNext,
+	MediaTrackPrevious,
+	MediaStop,
+	Eject,
+	MediaPlayPause,
+	MediaSelect,
+	LaunchMail,
+	LaunchApp2,
+	LaunchApp1,
+	SelectTask,
+	LaunchScreenSaver,
+	BrowserSearch,
+	BrowserHome,
+	BrowserBack,
+	BrowserForward,
+	BrowserStop,
+	BrowserRefresh,
+	BrowserFavorites,
+	ZoomToggle,
+	MailReply,
+	MailForward,
+	MailSend,
+
+	MAX_VALUE
+}
+
+class KeyCodeStrMap {
+
+	public _keyCodeToStr: string[];
+	public _strToKeyCode: { [str: string]: KeyCode };
+
+	constructor() {
+		this._keyCodeToStr = [];
+		this._strToKeyCode = Object.create(null);
+	}
+
+	define(keyCode: KeyCode, str: string): void {
+		this._keyCodeToStr[keyCode] = str;
+		this._strToKeyCode[str.toLowerCase()] = keyCode;
+	}
+
+	keyCodeToStr(keyCode: KeyCode): string {
+		return this._keyCodeToStr[keyCode];
+	}
+
+	strToKeyCode(str: string): KeyCode {
+		return this._strToKeyCode[str.toLowerCase()] || KeyCode.Unknown;
+	}
+}
+
+const uiMap = new KeyCodeStrMap();
+const userSettingsUSMap = new KeyCodeStrMap();
+const userSettingsGeneralMap = new KeyCodeStrMap();
+export const EVENT_KEY_CODE_MAP: { [keyCode: number]: KeyCode } = new Array(230);
+export const NATIVE_WINDOWS_KEY_CODE_TO_KEY_CODE: { [nativeKeyCode: string]: KeyCode } = {};
+const scanCodeIntToStr: string[] = [];
+const scanCodeStrToInt: { [code: string]: number } = Object.create(null);
+const scanCodeLowerCaseStrToInt: { [code: string]: number } = Object.create(null);
+
+export const ScanCodeUtils = {
+	lowerCaseToEnum: (scanCode: string) => scanCodeLowerCaseStrToInt[scanCode] || ScanCode.None,
+	toEnum: (scanCode: string) => scanCodeStrToInt[scanCode] || ScanCode.None,
+	toString: (scanCode: ScanCode) => scanCodeIntToStr[scanCode] || 'None'
+};
+
+
+export namespace KeyCodeUtils {
+	export function toString(keyCode: KeyCode): string {
+		return uiMap.keyCodeToStr(keyCode);
+	}
+	export function fromString(key: string): KeyCode {
+		return uiMap.strToKeyCode(key);
+	}
+
+	export function toUserSettingsUS(keyCode: KeyCode): string {
+		return userSettingsUSMap.keyCodeToStr(keyCode);
+	}
+	export function toUserSettingsGeneral(keyCode: KeyCode): string {
+		return userSettingsGeneralMap.keyCodeToStr(keyCode);
+	}
+	export function fromUserSettings(key: string): KeyCode {
+		return userSettingsUSMap.strToKeyCode(key) || userSettingsGeneralMap.strToKeyCode(key);
+	}
+
+	export function toElectronAccelerator(keyCode: KeyCode): string | null {
+		if (keyCode >= KeyCode.Numpad0 && keyCode <= KeyCode.NumpadDivide) {
+			// [Electron Accelerators] Electron is able to parse numpad keys, but unfortunately it
+			// renders them just as regular keys in menus. For example, num0 is rendered as "0",
+			// numdiv is rendered as "/", numsub is rendered as "-".
+			//
+			// This can lead to incredible confusion, as it makes numpad based keybindings indistinguishable
+			// from keybindings based on regular keys.
+			//
+			// We therefore need to fall back to custom rendering for numpad keys.
+			return null;
+		}
+
+		switch (keyCode) {
+			case KeyCode.UpArrow:
+				return 'Up';
+			case KeyCode.DownArrow:
+				return 'Down';
+			case KeyCode.LeftArrow:
+				return 'Left';
+			case KeyCode.RightArrow:
+				return 'Right';
+		}
+
+		return uiMap.keyCodeToStr(keyCode);
+	}
+}
+
+export const enum KeyMod {
+	CtrlCmd = (1 << 11) >>> 0,
+	Shift = (1 << 10) >>> 0,
+	Alt = (1 << 9) >>> 0,
+	WinCtrl = (1 << 8) >>> 0,
+}
+
+export function KeyChord(firstPart: number, secondPart: number): number {
+	const chordPart = ((secondPart & 0x0000FFFF) << 16) >>> 0;
+	return (firstPart | chordPart) >>> 0;
+}
diff --git a/src/vs/base/common/keybindings.ts b/src/vs/base/common/keybindings.ts
new file mode 100644
index 0000000000..375256237a
--- /dev/null
+++ b/src/vs/base/common/keybindings.ts
@@ -0,0 +1,284 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { illegalArgument } from 'vs/base/common/errors';
+import { KeyCode, ScanCode } from 'vs/base/common/keyCodes';
+import { OperatingSystem } from 'vs/base/common/platform';
+
+/**
+ * Binary encoding strategy:
+ * ```
+ *    1111 11
+ *    5432 1098 7654 3210
+ *    ---- CSAW KKKK KKKK
+ *  C = bit 11 = ctrlCmd flag
+ *  S = bit 10 = shift flag
+ *  A = bit 9 = alt flag
+ *  W = bit 8 = winCtrl flag
+ *  K = bits 0-7 = key code
+ * ```
+ */
+const enum BinaryKeybindingsMask {
+	CtrlCmd = (1 << 11) >>> 0,
+	Shift = (1 << 10) >>> 0,
+	Alt = (1 << 9) >>> 0,
+	WinCtrl = (1 << 8) >>> 0,
+	KeyCode = 0x000000FF
+}
+
+export function decodeKeybinding(keybinding: number | number[], OS: OperatingSystem): Keybinding | null {
+	if (typeof keybinding === 'number') {
+		if (keybinding === 0) {
+			return null;
+		}
+		const firstChord = (keybinding & 0x0000FFFF) >>> 0;
+		const secondChord = (keybinding & 0xFFFF0000) >>> 16;
+		if (secondChord !== 0) {
+			return new Keybinding([
+				createSimpleKeybinding(firstChord, OS),
+				createSimpleKeybinding(secondChord, OS)
+			]);
+		}
+		return new Keybinding([createSimpleKeybinding(firstChord, OS)]);
+	} else {
+		const chords = [];
+		for (let i = 0; i < keybinding.length; i++) {
+			chords.push(createSimpleKeybinding(keybinding[i], OS));
+		}
+		return new Keybinding(chords);
+	}
+}
+
+export function createSimpleKeybinding(keybinding: number, OS: OperatingSystem): KeyCodeChord {
+
+	const ctrlCmd = (keybinding & BinaryKeybindingsMask.CtrlCmd ? true : false);
+	const winCtrl = (keybinding & BinaryKeybindingsMask.WinCtrl ? true : false);
+
+	const ctrlKey = (OS === OperatingSystem.Macintosh ? winCtrl : ctrlCmd);
+	const shiftKey = (keybinding & BinaryKeybindingsMask.Shift ? true : false);
+	const altKey = (keybinding & BinaryKeybindingsMask.Alt ? true : false);
+	const metaKey = (OS === OperatingSystem.Macintosh ? ctrlCmd : winCtrl);
+	const keyCode = (keybinding & BinaryKeybindingsMask.KeyCode);
+
+	return new KeyCodeChord(ctrlKey, shiftKey, altKey, metaKey, keyCode);
+}
+
+export interface Modifiers {
+	readonly ctrlKey: boolean;
+	readonly shiftKey: boolean;
+	readonly altKey: boolean;
+	readonly metaKey: boolean;
+}
+
+/**
+ * Represents a chord which uses the `keyCode` field of keyboard events.
+ * A chord is a combination of keys pressed simultaneously.
+ */
+export class KeyCodeChord implements Modifiers {
+
+	constructor(
+		public readonly ctrlKey: boolean,
+		public readonly shiftKey: boolean,
+		public readonly altKey: boolean,
+		public readonly metaKey: boolean,
+		public readonly keyCode: KeyCode
+	) { }
+
+	public equals(other: Chord): boolean {
+		return (
+			other instanceof KeyCodeChord
+			&& this.ctrlKey === other.ctrlKey
+			&& this.shiftKey === other.shiftKey
+			&& this.altKey === other.altKey
+			&& this.metaKey === other.metaKey
+			&& this.keyCode === other.keyCode
+		);
+	}
+
+	public getHashCode(): string {
+		const ctrl = this.ctrlKey ? '1' : '0';
+		const shift = this.shiftKey ? '1' : '0';
+		const alt = this.altKey ? '1' : '0';
+		const meta = this.metaKey ? '1' : '0';
+		return `K${ctrl}${shift}${alt}${meta}${this.keyCode}`;
+	}
+
+	public isModifierKey(): boolean {
+		return (
+			this.keyCode === KeyCode.Unknown
+			|| this.keyCode === KeyCode.Ctrl
+			|| this.keyCode === KeyCode.Meta
+			|| this.keyCode === KeyCode.Alt
+			|| this.keyCode === KeyCode.Shift
+		);
+	}
+
+	public toKeybinding(): Keybinding {
+		return new Keybinding([this]);
+	}
+
+	/**
+	 * Does this keybinding refer to the key code of a modifier and it also has the modifier flag?
+	 */
+	public isDuplicateModifierCase(): boolean {
+		return (
+			(this.ctrlKey && this.keyCode === KeyCode.Ctrl)
+			|| (this.shiftKey && this.keyCode === KeyCode.Shift)
+			|| (this.altKey && this.keyCode === KeyCode.Alt)
+			|| (this.metaKey && this.keyCode === KeyCode.Meta)
+		);
+	}
+}
+
+/**
+ * Represents a chord which uses the `code` field of keyboard events.
+ * A chord is a combination of keys pressed simultaneously.
+ */
+export class ScanCodeChord implements Modifiers {
+
+	constructor(
+		public readonly ctrlKey: boolean,
+		public readonly shiftKey: boolean,
+		public readonly altKey: boolean,
+		public readonly metaKey: boolean,
+		public readonly scanCode: ScanCode
+	) { }
+
+	public equals(other: Chord): boolean {
+		return (
+			other instanceof ScanCodeChord
+			&& this.ctrlKey === other.ctrlKey
+			&& this.shiftKey === other.shiftKey
+			&& this.altKey === other.altKey
+			&& this.metaKey === other.metaKey
+			&& this.scanCode === other.scanCode
+		);
+	}
+
+	public getHashCode(): string {
+		const ctrl = this.ctrlKey ? '1' : '0';
+		const shift = this.shiftKey ? '1' : '0';
+		const alt = this.altKey ? '1' : '0';
+		const meta = this.metaKey ? '1' : '0';
+		return `S${ctrl}${shift}${alt}${meta}${this.scanCode}`;
+	}
+
+	/**
+	 * Does this keybinding refer to the key code of a modifier and it also has the modifier flag?
+	 */
+	public isDuplicateModifierCase(): boolean {
+		return (
+			(this.ctrlKey && (this.scanCode === ScanCode.ControlLeft || this.scanCode === ScanCode.ControlRight))
+			|| (this.shiftKey && (this.scanCode === ScanCode.ShiftLeft || this.scanCode === ScanCode.ShiftRight))
+			|| (this.altKey && (this.scanCode === ScanCode.AltLeft || this.scanCode === ScanCode.AltRight))
+			|| (this.metaKey && (this.scanCode === ScanCode.MetaLeft || this.scanCode === ScanCode.MetaRight))
+		);
+	}
+}
+
+export type Chord = KeyCodeChord | ScanCodeChord;
+
+/**
+ * A keybinding is a sequence of chords.
+ */
+export class Keybinding {
+
+	public readonly chords: Chord[];
+
+	constructor(chords: Chord[]) {
+		if (chords.length === 0) {
+			throw illegalArgument(`chords`);
+		}
+		this.chords = chords;
+	}
+
+	public getHashCode(): string {
+		let result = '';
+		for (let i = 0, len = this.chords.length; i < len; i++) {
+			if (i !== 0) {
+				result += ';';
+			}
+			result += this.chords[i].getHashCode();
+		}
+		return result;
+	}
+
+	public equals(other: Keybinding | null): boolean {
+		if (other === null) {
+			return false;
+		}
+		if (this.chords.length !== other.chords.length) {
+			return false;
+		}
+		for (let i = 0; i < this.chords.length; i++) {
+			if (!this.chords[i].equals(other.chords[i])) {
+				return false;
+			}
+		}
+		return true;
+	}
+}
+
+export class ResolvedChord {
+	constructor(
+		public readonly ctrlKey: boolean,
+		public readonly shiftKey: boolean,
+		public readonly altKey: boolean,
+		public readonly metaKey: boolean,
+		public readonly keyLabel: string | null,
+		public readonly keyAriaLabel: string | null
+	) { }
+}
+
+export type SingleModifierChord = 'ctrl' | 'shift' | 'alt' | 'meta';
+
+/**
+ * A resolved keybinding. Consists of one or multiple chords.
+ */
+export abstract class ResolvedKeybinding {
+	/**
+	 * This prints the binding in a format suitable for displaying in the UI.
+	 */
+	public abstract getLabel(): string | null;
+	/**
+	 * This prints the binding in a format suitable for ARIA.
+	 */
+	public abstract getAriaLabel(): string | null;
+	/**
+	 * This prints the binding in a format suitable for electron's accelerators.
+	 * See https://github.com/electron/electron/blob/master/docs/api/accelerator.md
+	 */
+	public abstract getElectronAccelerator(): string | null;
+	/**
+	 * This prints the binding in a format suitable for user settings.
+	 */
+	public abstract getUserSettingsLabel(): string | null;
+	/**
+	 * Is the user settings label reflecting the label?
+	 */
+	public abstract isWYSIWYG(): boolean;
+	/**
+	 * Does the keybinding consist of more than one chord?
+	 */
+	public abstract hasMultipleChords(): boolean;
+	/**
+	 * Returns the chords that comprise of the keybinding.
+	 */
+	public abstract getChords(): ResolvedChord[];
+	/**
+	 * Returns the chords as strings useful for dispatching.
+	 * Returns null for modifier only chords.
+	 * @example keybinding "Shift" -> null
+	 * @example keybinding ("D" with shift == true) -> "shift+D"
+	 */
+	public abstract getDispatchChords(): (string | null)[];
+	/**
+	 * Returns the modifier only chords as strings useful for dispatching.
+	 * Returns null for chords that contain more than one modifier or a regular key.
+	 * @example keybinding "Shift" -> "shift"
+	 * @example keybinding ("D" with shift == true") -> null
+	 */
+	public abstract getSingleModifierDispatchChords(): (SingleModifierChord | null)[];
+}
diff --git a/src/vs/base/common/lazy.ts b/src/vs/base/common/lazy.ts
new file mode 100644
index 0000000000..7114ece99b
--- /dev/null
+++ b/src/vs/base/common/lazy.ts
@@ -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.
+ *--------------------------------------------------------------------------------------------*/
+
+export class Lazy<T> {
+
+	private _didRun: boolean = false;
+	private _value?: T;
+	private _error: Error | undefined;
+
+	constructor(
+		private readonly executor: () => T,
+	) { }
+
+	/**
+	 * True if the lazy value has been resolved.
+	 */
+	get hasValue() { return this._didRun; }
+
+	/**
+	 * Get the wrapped value.
+	 *
+	 * This will force evaluation of the lazy value if it has not been resolved yet. Lazy values are only
+	 * resolved once. `getValue` will re-throw exceptions that are hit while resolving the value
+	 */
+	get value(): T {
+		if (!this._didRun) {
+			try {
+				this._value = this.executor();
+			} catch (err) {
+				this._error = err;
+			} finally {
+				this._didRun = true;
+			}
+		}
+		if (this._error) {
+			throw this._error;
+		}
+		return this._value!;
+	}
+
+	/**
+	 * Get the wrapped value without forcing evaluation.
+	 */
+	get rawValue(): T | undefined { return this._value; }
+}
diff --git a/src/vs/base/common/lifecycle.ts b/src/vs/base/common/lifecycle.ts
new file mode 100644
index 0000000000..568a0124c1
--- /dev/null
+++ b/src/vs/base/common/lifecycle.ts
@@ -0,0 +1,801 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { compareBy, numberComparator } from 'vs/base/common/arrays';
+import { groupBy } from 'vs/base/common/collections';
+import { SetMap } from './map';
+import { createSingleCallFunction } from 'vs/base/common/functional';
+import { Iterable } from 'vs/base/common/iterator';
+
+// #region Disposable Tracking
+
+/**
+ * Enables logging of potentially leaked disposables.
+ *
+ * A disposable is considered leaked if it is not disposed or not registered as the child of
+ * another disposable. This tracking is very simple an only works for classes that either
+ * extend Disposable or use a DisposableStore. This means there are a lot of false positives.
+ */
+const TRACK_DISPOSABLES = false;
+let disposableTracker: IDisposableTracker | null = null;
+
+export interface IDisposableTracker {
+	/**
+	 * Is called on construction of a disposable.
+	*/
+	trackDisposable(disposable: IDisposable): void;
+
+	/**
+	 * Is called when a disposable is registered as child of another disposable (e.g. {@link DisposableStore}).
+	 * If parent is `null`, the disposable is removed from its former parent.
+	*/
+	setParent(child: IDisposable, parent: IDisposable | null): void;
+
+	/**
+	 * Is called after a disposable is disposed.
+	*/
+	markAsDisposed(disposable: IDisposable): void;
+
+	/**
+	 * Indicates that the given object is a singleton which does not need to be disposed.
+	*/
+	markAsSingleton(disposable: IDisposable): void;
+}
+
+export interface DisposableInfo {
+	value: IDisposable;
+	source: string | null;
+	parent: IDisposable | null;
+	isSingleton: boolean;
+	idx: number;
+}
+
+export class DisposableTracker implements IDisposableTracker {
+	private static idx = 0;
+
+	private readonly livingDisposables = new Map<IDisposable, DisposableInfo>();
+
+	private getDisposableData(d: IDisposable): DisposableInfo {
+		let val = this.livingDisposables.get(d);
+		if (!val) {
+			val = { parent: null, source: null, isSingleton: false, value: d, idx: DisposableTracker.idx++ };
+			this.livingDisposables.set(d, val);
+		}
+		return val;
+	}
+
+	trackDisposable(d: IDisposable): void {
+		const data = this.getDisposableData(d);
+		if (!data.source) {
+			data.source =
+				new Error().stack!;
+		}
+	}
+
+	setParent(child: IDisposable, parent: IDisposable | null): void {
+		const data = this.getDisposableData(child);
+		data.parent = parent;
+	}
+
+	markAsDisposed(x: IDisposable): void {
+		this.livingDisposables.delete(x);
+	}
+
+	markAsSingleton(disposable: IDisposable): void {
+		this.getDisposableData(disposable).isSingleton = true;
+	}
+
+	private getRootParent(data: DisposableInfo, cache: Map<DisposableInfo, DisposableInfo>): DisposableInfo {
+		const cacheValue = cache.get(data);
+		if (cacheValue) {
+			return cacheValue;
+		}
+
+		const result = data.parent ? this.getRootParent(this.getDisposableData(data.parent), cache) : data;
+		cache.set(data, result);
+		return result;
+	}
+
+	getTrackedDisposables(): IDisposable[] {
+		const rootParentCache = new Map<DisposableInfo, DisposableInfo>();
+
+		const leaking = [...this.livingDisposables.entries()]
+			.filter(([, v]) => v.source !== null && !this.getRootParent(v, rootParentCache).isSingleton)
+			.flatMap(([k]) => k);
+
+		return leaking;
+	}
+
+	computeLeakingDisposables(maxReported = 10, preComputedLeaks?: DisposableInfo[]): { leaks: DisposableInfo[]; details: string } | undefined {
+		let uncoveredLeakingObjs: DisposableInfo[] | undefined;
+		if (preComputedLeaks) {
+			uncoveredLeakingObjs = preComputedLeaks;
+		} else {
+			const rootParentCache = new Map<DisposableInfo, DisposableInfo>();
+
+			const leakingObjects = [...this.livingDisposables.values()]
+				.filter((info) => info.source !== null && !this.getRootParent(info, rootParentCache).isSingleton);
+
+			if (leakingObjects.length === 0) {
+				return;
+			}
+			const leakingObjsSet = new Set(leakingObjects.map(o => o.value));
+
+			// Remove all objects that are a child of other leaking objects. Assumes there are no cycles.
+			uncoveredLeakingObjs = leakingObjects.filter(l => {
+				return !(l.parent && leakingObjsSet.has(l.parent));
+			});
+
+			if (uncoveredLeakingObjs.length === 0) {
+				throw new Error('There are cyclic diposable chains!');
+			}
+		}
+
+		if (!uncoveredLeakingObjs) {
+			return undefined;
+		}
+
+		function getStackTracePath(leaking: DisposableInfo): string[] {
+			function removePrefix(array: string[], linesToRemove: (string | RegExp)[]) {
+				while (array.length > 0 && linesToRemove.some(regexp => typeof regexp === 'string' ? regexp === array[0] : array[0].match(regexp))) {
+					array.shift();
+				}
+			}
+
+			const lines = leaking.source!.split('\n').map(p => p.trim().replace('at ', '')).filter(l => l !== '');
+			removePrefix(lines, ['Error', /^trackDisposable \(.*\)$/, /^DisposableTracker.trackDisposable \(.*\)$/]);
+			return lines.reverse();
+		}
+
+		const stackTraceStarts = new SetMap<string, DisposableInfo>();
+		for (const leaking of uncoveredLeakingObjs) {
+			const stackTracePath = getStackTracePath(leaking);
+			for (let i = 0; i <= stackTracePath.length; i++) {
+				stackTraceStarts.add(stackTracePath.slice(0, i).join('\n'), leaking);
+			}
+		}
+
+		// Put earlier leaks first
+		uncoveredLeakingObjs.sort(compareBy(l => l.idx, numberComparator));
+
+		let message = '';
+
+		let i = 0;
+		for (const leaking of uncoveredLeakingObjs.slice(0, maxReported)) {
+			i++;
+			const stackTracePath = getStackTracePath(leaking);
+			const stackTraceFormattedLines = [];
+
+			for (let i = 0; i < stackTracePath.length; i++) {
+				let line = stackTracePath[i];
+				const starts = stackTraceStarts.get(stackTracePath.slice(0, i + 1).join('\n'));
+				line = `(shared with ${starts.size}/${uncoveredLeakingObjs.length} leaks) at ${line}`;
+
+				const prevStarts = stackTraceStarts.get(stackTracePath.slice(0, i).join('\n'));
+				const continuations = groupBy([...prevStarts].map(d => getStackTracePath(d)[i]), v => v);
+				delete continuations[stackTracePath[i]];
+				for (const [cont, set] of Object.entries(continuations)) {
+					stackTraceFormattedLines.unshift(`    - stacktraces of ${set.length} other leaks continue with ${cont}`);
+				}
+
+				stackTraceFormattedLines.unshift(line);
+			}
+
+			message += `\n\n\n==================== Leaking disposable ${i}/${uncoveredLeakingObjs.length}: ${leaking.value.constructor.name} ====================\n${stackTraceFormattedLines.join('\n')}\n============================================================\n\n`;
+		}
+
+		if (uncoveredLeakingObjs.length > maxReported) {
+			message += `\n\n\n... and ${uncoveredLeakingObjs.length - maxReported} more leaking disposables\n\n`;
+		}
+
+		return { leaks: uncoveredLeakingObjs, details: message };
+	}
+}
+
+export function setDisposableTracker(tracker: IDisposableTracker | null): void {
+	disposableTracker = tracker;
+}
+
+if (TRACK_DISPOSABLES) {
+	const __is_disposable_tracked__ = '__is_disposable_tracked__';
+	setDisposableTracker(new class implements IDisposableTracker {
+		trackDisposable(x: IDisposable): void {
+			const stack = new Error('Potentially leaked disposable').stack!;
+			setTimeout(() => {
+				if (!(x as any)[__is_disposable_tracked__]) {
+					console.log(stack);
+				}
+			}, 3000);
+		}
+
+		setParent(child: IDisposable, parent: IDisposable | null): void {
+			if (child && child !== Disposable.None) {
+				try {
+					(child as any)[__is_disposable_tracked__] = true;
+				} catch {
+					// noop
+				}
+			}
+		}
+
+		markAsDisposed(disposable: IDisposable): void {
+			if (disposable && disposable !== Disposable.None) {
+				try {
+					(disposable as any)[__is_disposable_tracked__] = true;
+				} catch {
+					// noop
+				}
+			}
+		}
+		markAsSingleton(disposable: IDisposable): void { }
+	});
+}
+
+export function trackDisposable<T extends IDisposable>(x: T): T {
+	disposableTracker?.trackDisposable(x);
+	return x;
+}
+
+export function markAsDisposed(disposable: IDisposable): void {
+	disposableTracker?.markAsDisposed(disposable);
+}
+
+function setParentOfDisposable(child: IDisposable, parent: IDisposable | null): void {
+	disposableTracker?.setParent(child, parent);
+}
+
+function setParentOfDisposables(children: IDisposable[], parent: IDisposable | null): void {
+	if (!disposableTracker) {
+		return;
+	}
+	for (const child of children) {
+		disposableTracker.setParent(child, parent);
+	}
+}
+
+/**
+ * Indicates that the given object is a singleton which does not need to be disposed.
+*/
+export function markAsSingleton<T extends IDisposable>(singleton: T): T {
+	disposableTracker?.markAsSingleton(singleton);
+	return singleton;
+}
+
+// #endregion
+
+/**
+ * An object that performs a cleanup operation when `.dispose()` is called.
+ *
+ * Some examples of how disposables are used:
+ *
+ * - An event listener that removes itself when `.dispose()` is called.
+ * - A resource such as a file system watcher that cleans up the resource when `.dispose()` is called.
+ * - The return value from registering a provider. When `.dispose()` is called, the provider is unregistered.
+ */
+export interface IDisposable {
+	dispose(): void;
+}
+
+/**
+ * Check if `thing` is {@link IDisposable disposable}.
+ */
+export function isDisposable<E extends any>(thing: E): thing is E & IDisposable {
+	return typeof thing === 'object' && thing !== null && typeof (<IDisposable><any>thing).dispose === 'function' && (<IDisposable><any>thing).dispose.length === 0;
+}
+
+/**
+ * Disposes of the value(s) passed in.
+ */
+export function dispose<T extends IDisposable>(disposable: T): T;
+export function dispose<T extends IDisposable>(disposable: T | undefined): T | undefined;
+export function dispose<T extends IDisposable, A extends Iterable<T> = Iterable<T>>(disposables: A): A;
+export function dispose<T extends IDisposable>(disposables: Array<T>): Array<T>;
+export function dispose<T extends IDisposable>(disposables: ReadonlyArray<T>): ReadonlyArray<T>;
+export function dispose<T extends IDisposable>(arg: T | Iterable<T> | undefined): any {
+	if (Iterable.is(arg)) {
+		const errors: any[] = [];
+
+		for (const d of arg) {
+			if (d) {
+				try {
+					d.dispose();
+				} catch (e) {
+					errors.push(e);
+				}
+			}
+		}
+
+		if (errors.length === 1) {
+			throw errors[0];
+		} else if (errors.length > 1) {
+			throw new AggregateError(errors, 'Encountered errors while disposing of store');
+		}
+
+		return Array.isArray(arg) ? [] : arg;
+	} else if (arg) {
+		arg.dispose();
+		return arg;
+	}
+}
+
+export function disposeIfDisposable<T extends IDisposable | object>(disposables: Array<T>): Array<T> {
+	for (const d of disposables) {
+		if (isDisposable(d)) {
+			d.dispose();
+		}
+	}
+	return [];
+}
+
+/**
+ * Combine multiple disposable values into a single {@link IDisposable}.
+ */
+export function combinedDisposable(...disposables: IDisposable[]): IDisposable {
+	const parent = toDisposable(() => dispose(disposables));
+	setParentOfDisposables(disposables, parent);
+	return parent;
+}
+
+/**
+ * Turn a function that implements dispose into an {@link IDisposable}.
+ *
+ * @param fn Clean up function, guaranteed to be called only **once**.
+ */
+export function toDisposable(fn: () => void): IDisposable {
+	const self = trackDisposable({
+		dispose: createSingleCallFunction(() => {
+			markAsDisposed(self);
+			fn();
+		})
+	});
+	return self;
+}
+
+/**
+ * Manages a collection of disposable values.
+ *
+ * This is the preferred way to manage multiple disposables. A `DisposableStore` is safer to work with than an
+ * `IDisposable[]` as it considers edge cases, such as registering the same value multiple times or adding an item to a
+ * store that has already been disposed of.
+ */
+export class DisposableStore implements IDisposable {
+
+	static DISABLE_DISPOSED_WARNING = false;
+
+	private readonly _toDispose = new Set<IDisposable>();
+	private _isDisposed = false;
+
+	constructor() {
+		trackDisposable(this);
+	}
+
+	/**
+	 * Dispose of all registered disposables and mark this object as disposed.
+	 *
+	 * Any future disposables added to this object will be disposed of on `add`.
+	 */
+	public dispose(): void {
+		if (this._isDisposed) {
+			return;
+		}
+
+		markAsDisposed(this);
+		this._isDisposed = true;
+		this.clear();
+	}
+
+	/**
+	 * @return `true` if this object has been disposed of.
+	 */
+	public get isDisposed(): boolean {
+		return this._isDisposed;
+	}
+
+	/**
+	 * Dispose of all registered disposables but do not mark this object as disposed.
+	 */
+	public clear(): void {
+		if (this._toDispose.size === 0) {
+			return;
+		}
+
+		try {
+			dispose(this._toDispose);
+		} finally {
+			this._toDispose.clear();
+		}
+	}
+
+	/**
+	 * Add a new {@link IDisposable disposable} to the collection.
+	 */
+	public add<T extends IDisposable>(o: T): T {
+		if (!o) {
+			return o;
+		}
+		if ((o as unknown as DisposableStore) === this) {
+			throw new Error('Cannot register a disposable on itself!');
+		}
+
+		setParentOfDisposable(o, this);
+		if (this._isDisposed) {
+			if (!DisposableStore.DISABLE_DISPOSED_WARNING) {
+				console.warn(new Error('Trying to add a disposable to a DisposableStore that has already been disposed of. The added object will be leaked!').stack);
+			}
+		} else {
+			this._toDispose.add(o);
+		}
+
+		return o;
+	}
+
+	/**
+	 * Deletes a disposable from store and disposes of it. This will not throw or warn and proceed to dispose the
+	 * disposable even when the disposable is not part in the store.
+	 */
+	public delete<T extends IDisposable>(o: T): void {
+		if (!o) {
+			return;
+		}
+		if ((o as unknown as DisposableStore) === this) {
+			throw new Error('Cannot dispose a disposable on itself!');
+		}
+		this._toDispose.delete(o);
+		o.dispose();
+	}
+
+	/**
+	 * Deletes the value from the store, but does not dispose it.
+	 */
+	public deleteAndLeak<T extends IDisposable>(o: T): void {
+		if (!o) {
+			return;
+		}
+		if (this._toDispose.has(o)) {
+			this._toDispose.delete(o);
+			setParentOfDisposable(o, null);
+		}
+	}
+}
+
+/**
+ * Abstract base class for a {@link IDisposable disposable} object.
+ *
+ * Subclasses can {@linkcode _register} disposables that will be automatically cleaned up when this object is disposed of.
+ */
+export abstract class Disposable implements IDisposable {
+
+	/**
+	 * A disposable that does nothing when it is disposed of.
+	 *
+	 * TODO: This should not be a static property.
+	 */
+	static readonly None = Object.freeze<IDisposable>({ dispose() { } });
+
+	protected readonly _store = new DisposableStore();
+
+	constructor() {
+		trackDisposable(this);
+		setParentOfDisposable(this._store, this);
+	}
+
+	public dispose(): void {
+		markAsDisposed(this);
+
+		this._store.dispose();
+	}
+
+	/**
+	 * Adds `o` to the collection of disposables managed by this object.
+	 */
+	protected _register<T extends IDisposable>(o: T): T {
+		if ((o as unknown as Disposable) === this) {
+			throw new Error('Cannot register a disposable on itself!');
+		}
+		return this._store.add(o);
+	}
+}
+
+/**
+ * Manages the lifecycle of a disposable value that may be changed.
+ *
+ * This ensures that when the disposable value is changed, the previously held disposable is disposed of. You can
+ * also register a `MutableDisposable` on a `Disposable` to ensure it is automatically cleaned up.
+ */
+export class MutableDisposable<T extends IDisposable> implements IDisposable {
+	private _value?: T;
+	private _isDisposed = false;
+
+	constructor() {
+		trackDisposable(this);
+	}
+
+	get value(): T | undefined {
+		return this._isDisposed ? undefined : this._value;
+	}
+
+	set value(value: T | undefined) {
+		if (this._isDisposed || value === this._value) {
+			return;
+		}
+
+		this._value?.dispose();
+		if (value) {
+			setParentOfDisposable(value, this);
+		}
+		this._value = value;
+	}
+
+	/**
+	 * Resets the stored value and disposed of the previously stored value.
+	 */
+	clear(): void {
+		this.value = undefined;
+	}
+
+	dispose(): void {
+		this._isDisposed = true;
+		markAsDisposed(this);
+		this._value?.dispose();
+		this._value = undefined;
+	}
+
+	/**
+	 * Clears the value, but does not dispose it.
+	 * The old value is returned.
+	*/
+	clearAndLeak(): T | undefined {
+		const oldValue = this._value;
+		this._value = undefined;
+		if (oldValue) {
+			setParentOfDisposable(oldValue, null);
+		}
+		return oldValue;
+	}
+}
+
+/**
+ * Manages the lifecycle of a disposable value that may be changed like {@link MutableDisposable}, but the value must
+ * exist and cannot be undefined.
+ */
+export class MandatoryMutableDisposable<T extends IDisposable> implements IDisposable {
+	private readonly _disposable = new MutableDisposable<T>();
+	private _isDisposed = false;
+
+	constructor(initialValue: T) {
+		this._disposable.value = initialValue;
+	}
+
+	get value(): T {
+		return this._disposable.value!;
+	}
+
+	set value(value: T) {
+		if (this._isDisposed || value === this._disposable.value) {
+			return;
+		}
+		this._disposable.value = value;
+	}
+
+	dispose() {
+		this._isDisposed = true;
+		this._disposable.dispose();
+	}
+}
+
+export class RefCountedDisposable {
+
+	private _counter: number = 1;
+
+	constructor(
+		private readonly _disposable: IDisposable,
+	) { }
+
+	acquire() {
+		this._counter++;
+		return this;
+	}
+
+	release() {
+		if (--this._counter === 0) {
+			this._disposable.dispose();
+		}
+		return this;
+	}
+}
+
+/**
+ * A safe disposable can be `unset` so that a leaked reference (listener)
+ * can be cut-off.
+ */
+export class SafeDisposable implements IDisposable {
+
+	dispose: () => void = () => { };
+	unset: () => void = () => { };
+	isset: () => boolean = () => false;
+
+	constructor() {
+		trackDisposable(this);
+	}
+
+	set(fn: Function) {
+		let callback: Function | undefined = fn;
+		this.unset = () => callback = undefined;
+		this.isset = () => callback !== undefined;
+		this.dispose = () => {
+			if (callback) {
+				callback();
+				callback = undefined;
+				markAsDisposed(this);
+			}
+		};
+		return this;
+	}
+}
+
+export interface IReference<T> extends IDisposable {
+	readonly object: T;
+}
+
+export abstract class ReferenceCollection<T> {
+
+	private readonly references: Map<string, { readonly object: T; counter: number }> = new Map();
+
+	acquire(key: string, ...args: any[]): IReference<T> {
+		let reference = this.references.get(key);
+
+		if (!reference) {
+			reference = { counter: 0, object: this.createReferencedObject(key, ...args) };
+			this.references.set(key, reference);
+		}
+
+		const { object } = reference;
+		const dispose = createSingleCallFunction(() => {
+			if (--reference.counter === 0) {
+				this.destroyReferencedObject(key, reference.object);
+				this.references.delete(key);
+			}
+		});
+
+		reference.counter++;
+
+		return { object, dispose };
+	}
+
+	protected abstract createReferencedObject(key: string, ...args: any[]): T;
+	protected abstract destroyReferencedObject(key: string, object: T): void;
+}
+
+/**
+ * Unwraps a reference collection of promised values. Makes sure
+ * references are disposed whenever promises get rejected.
+ */
+export class AsyncReferenceCollection<T> {
+
+	constructor(private referenceCollection: ReferenceCollection<Promise<T>>) { }
+
+	async acquire(key: string, ...args: any[]): Promise<IReference<T>> {
+		const ref = this.referenceCollection.acquire(key, ...args);
+
+		try {
+			const object = await ref.object;
+
+			return {
+				object,
+				dispose: () => ref.dispose()
+			};
+		} catch (error) {
+			ref.dispose();
+			throw error;
+		}
+	}
+}
+
+export class ImmortalReference<T> implements IReference<T> {
+	constructor(public object: T) { }
+	dispose(): void { /* noop */ }
+}
+
+export function disposeOnReturn(fn: (store: DisposableStore) => void): void {
+	const store = new DisposableStore();
+	try {
+		fn(store);
+	} finally {
+		store.dispose();
+	}
+}
+
+/**
+ * A map the manages the lifecycle of the values that it stores.
+ */
+export class DisposableMap<K, V extends IDisposable = IDisposable> implements IDisposable {
+
+	private readonly _store = new Map<K, V>();
+	private _isDisposed = false;
+
+	constructor() {
+		trackDisposable(this);
+	}
+
+	/**
+	 * Disposes of all stored values and mark this object as disposed.
+	 *
+	 * Trying to use this object after it has been disposed of is an error.
+	 */
+	dispose(): void {
+		markAsDisposed(this);
+		this._isDisposed = true;
+		this.clearAndDisposeAll();
+	}
+
+	/**
+	 * Disposes of all stored values and clear the map, but DO NOT mark this object as disposed.
+	 */
+	clearAndDisposeAll(): void {
+		if (!this._store.size) {
+			return;
+		}
+
+		try {
+			dispose(this._store.values());
+		} finally {
+			this._store.clear();
+		}
+	}
+
+	has(key: K): boolean {
+		return this._store.has(key);
+	}
+
+	get size(): number {
+		return this._store.size;
+	}
+
+	get(key: K): V | undefined {
+		return this._store.get(key);
+	}
+
+	set(key: K, value: V, skipDisposeOnOverwrite = false): void {
+		if (this._isDisposed) {
+			console.warn(new Error('Trying to add a disposable to a DisposableMap that has already been disposed of. The added object will be leaked!').stack);
+		}
+
+		if (!skipDisposeOnOverwrite) {
+			this._store.get(key)?.dispose();
+		}
+
+		this._store.set(key, value);
+	}
+
+	/**
+	 * Delete the value stored for `key` from this map and also dispose of it.
+	 */
+	deleteAndDispose(key: K): void {
+		this._store.get(key)?.dispose();
+		this._store.delete(key);
+	}
+
+	/**
+	 * Delete the value stored for `key` from this map but return it. The caller is
+	 * responsible for disposing of the value.
+	 */
+	deleteAndLeak(key: K): V | undefined {
+		const value = this._store.get(key);
+		this._store.delete(key);
+		return value;
+	}
+
+	keys(): IterableIterator<K> {
+		return this._store.keys();
+	}
+
+	values(): IterableIterator<V> {
+		return this._store.values();
+	}
+
+	[Symbol.iterator](): IterableIterator<[K, V]> {
+		return this._store[Symbol.iterator]();
+	}
+}
diff --git a/src/vs/base/common/linkedList.ts b/src/vs/base/common/linkedList.ts
new file mode 100644
index 0000000000..42a1c2aad9
--- /dev/null
+++ b/src/vs/base/common/linkedList.ts
@@ -0,0 +1,142 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+class Node<E> {
+
+	static readonly Undefined = new Node<any>(undefined);
+
+	element: E;
+	next: Node<E>;
+	prev: Node<E>;
+
+	constructor(element: E) {
+		this.element = element;
+		this.next = Node.Undefined;
+		this.prev = Node.Undefined;
+	}
+}
+
+export class LinkedList<E> {
+
+	private _first: Node<E> = Node.Undefined;
+	private _last: Node<E> = Node.Undefined;
+	private _size: number = 0;
+
+	get size(): number {
+		return this._size;
+	}
+
+	isEmpty(): boolean {
+		return this._first === Node.Undefined;
+	}
+
+	clear(): void {
+		let node = this._first;
+		while (node !== Node.Undefined) {
+			const next = node.next;
+			node.prev = Node.Undefined;
+			node.next = Node.Undefined;
+			node = next;
+		}
+
+		this._first = Node.Undefined;
+		this._last = Node.Undefined;
+		this._size = 0;
+	}
+
+	unshift(element: E): () => void {
+		return this._insert(element, false);
+	}
+
+	push(element: E): () => void {
+		return this._insert(element, true);
+	}
+
+	private _insert(element: E, atTheEnd: boolean): () => void {
+		const newNode = new Node(element);
+		if (this._first === Node.Undefined) {
+			this._first = newNode;
+			this._last = newNode;
+
+		} else if (atTheEnd) {
+			// push
+			const oldLast = this._last;
+			this._last = newNode;
+			newNode.prev = oldLast;
+			oldLast.next = newNode;
+
+		} else {
+			// unshift
+			const oldFirst = this._first;
+			this._first = newNode;
+			newNode.next = oldFirst;
+			oldFirst.prev = newNode;
+		}
+		this._size += 1;
+
+		let didRemove = false;
+		return () => {
+			if (!didRemove) {
+				didRemove = true;
+				this._remove(newNode);
+			}
+		};
+	}
+
+	shift(): E | undefined {
+		if (this._first === Node.Undefined) {
+			return undefined;
+		} else {
+			const res = this._first.element;
+			this._remove(this._first);
+			return res;
+		}
+	}
+
+	pop(): E | undefined {
+		if (this._last === Node.Undefined) {
+			return undefined;
+		} else {
+			const res = this._last.element;
+			this._remove(this._last);
+			return res;
+		}
+	}
+
+	private _remove(node: Node<E>): void {
+		if (node.prev !== Node.Undefined && node.next !== Node.Undefined) {
+			// middle
+			const anchor = node.prev;
+			anchor.next = node.next;
+			node.next.prev = anchor;
+
+		} else if (node.prev === Node.Undefined && node.next === Node.Undefined) {
+			// only node
+			this._first = Node.Undefined;
+			this._last = Node.Undefined;
+
+		} else if (node.next === Node.Undefined) {
+			// last
+			this._last = this._last.prev!;
+			this._last.next = Node.Undefined;
+
+		} else if (node.prev === Node.Undefined) {
+			// first
+			this._first = this._first.next!;
+			this._first.prev = Node.Undefined;
+		}
+
+		// done
+		this._size -= 1;
+	}
+
+	*[Symbol.iterator](): Iterator<E> {
+		let node = this._first;
+		while (node !== Node.Undefined) {
+			yield node.element;
+			node = node.next;
+		}
+	}
+}
diff --git a/src/vs/base/common/map.ts b/src/vs/base/common/map.ts
new file mode 100644
index 0000000000..5aa55f4842
--- /dev/null
+++ b/src/vs/base/common/map.ts
@@ -0,0 +1,202 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+export function getOrSet<K, V>(map: Map<K, V>, key: K, value: V): V {
+	let result = map.get(key);
+	if (result === undefined) {
+		result = value;
+		map.set(key, result);
+	}
+
+	return result;
+}
+
+export function mapToString<K, V>(map: Map<K, V>): string {
+	const entries: string[] = [];
+	map.forEach((value, key) => {
+		entries.push(`${key} => ${value}`);
+	});
+
+	return `Map(${map.size}) {${entries.join(', ')}}`;
+}
+
+export function setToString<K>(set: Set<K>): string {
+	const entries: K[] = [];
+	set.forEach(value => {
+		entries.push(value);
+	});
+
+	return `Set(${set.size}) {${entries.join(', ')}}`;
+}
+
+export const enum Touch {
+	None = 0,
+	AsOld = 1,
+	AsNew = 2
+}
+
+export class CounterSet<T> {
+
+	private map = new Map<T, number>();
+
+	add(value: T): CounterSet<T> {
+		this.map.set(value, (this.map.get(value) || 0) + 1);
+		return this;
+	}
+
+	delete(value: T): boolean {
+		let counter = this.map.get(value) || 0;
+
+		if (counter === 0) {
+			return false;
+		}
+
+		counter--;
+
+		if (counter === 0) {
+			this.map.delete(value);
+		} else {
+			this.map.set(value, counter);
+		}
+
+		return true;
+	}
+
+	has(value: T): boolean {
+		return this.map.has(value);
+	}
+}
+
+/**
+ * A map that allows access both by keys and values.
+ * **NOTE**: values need to be unique.
+ */
+export class BidirectionalMap<K, V> {
+
+	private readonly _m1 = new Map<K, V>();
+	private readonly _m2 = new Map<V, K>();
+
+	constructor(entries?: readonly (readonly [K, V])[]) {
+		if (entries) {
+			for (const [key, value] of entries) {
+				this.set(key, value);
+			}
+		}
+	}
+
+	clear(): void {
+		this._m1.clear();
+		this._m2.clear();
+	}
+
+	set(key: K, value: V): void {
+		this._m1.set(key, value);
+		this._m2.set(value, key);
+	}
+
+	get(key: K): V | undefined {
+		return this._m1.get(key);
+	}
+
+	getKey(value: V): K | undefined {
+		return this._m2.get(value);
+	}
+
+	delete(key: K): boolean {
+		const value = this._m1.get(key);
+		if (value === undefined) {
+			return false;
+		}
+		this._m1.delete(key);
+		this._m2.delete(value);
+		return true;
+	}
+
+	forEach(callbackfn: (value: V, key: K, map: BidirectionalMap<K, V>) => void, thisArg?: any): void {
+		this._m1.forEach((value, key) => {
+			callbackfn.call(thisArg, value, key, this);
+		});
+	}
+
+	keys(): IterableIterator<K> {
+		return this._m1.keys();
+	}
+
+	values(): IterableIterator<V> {
+		return this._m1.values();
+	}
+}
+
+export class SetMap<K, V> {
+
+	private map = new Map<K, Set<V>>();
+
+	add(key: K, value: V): void {
+		let values = this.map.get(key);
+
+		if (!values) {
+			values = new Set<V>();
+			this.map.set(key, values);
+		}
+
+		values.add(value);
+	}
+
+	delete(key: K, value: V): void {
+		const values = this.map.get(key);
+
+		if (!values) {
+			return;
+		}
+
+		values.delete(value);
+
+		if (values.size === 0) {
+			this.map.delete(key);
+		}
+	}
+
+	forEach(key: K, fn: (value: V) => void): void {
+		const values = this.map.get(key);
+
+		if (!values) {
+			return;
+		}
+
+		values.forEach(fn);
+	}
+
+	get(key: K): ReadonlySet<V> {
+		const values = this.map.get(key);
+		if (!values) {
+			return new Set<V>();
+		}
+		return values;
+	}
+}
+
+export function mapsStrictEqualIgnoreOrder(a: Map<unknown, unknown>, b: Map<unknown, unknown>): boolean {
+	if (a === b) {
+		return true;
+	}
+
+	if (a.size !== b.size) {
+		return false;
+	}
+
+	for (const [key, value] of a) {
+		if (!b.has(key) || b.get(key) !== value) {
+			return false;
+		}
+	}
+
+	for (const [key] of b) {
+		if (!a.has(key)) {
+			return false;
+		}
+	}
+
+	return true;
+}
diff --git a/src/vs/base/common/numbers.ts b/src/vs/base/common/numbers.ts
new file mode 100644
index 0000000000..ab4c9f92e0
--- /dev/null
+++ b/src/vs/base/common/numbers.ts
@@ -0,0 +1,98 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+export function clamp(value: number, min: number, max: number): number {
+	return Math.min(Math.max(value, min), max);
+}
+
+export function rot(index: number, modulo: number): number {
+	return (modulo + (index % modulo)) % modulo;
+}
+
+export class Counter {
+	private _next = 0;
+
+	getNext(): number {
+		return this._next++;
+	}
+}
+
+export class MovingAverage {
+
+	private _n = 1;
+	private _val = 0;
+
+	update(value: number): number {
+		this._val = this._val + (value - this._val) / this._n;
+		this._n += 1;
+		return this._val;
+	}
+
+	get value(): number {
+		return this._val;
+	}
+}
+
+export class SlidingWindowAverage {
+
+	private _n: number = 0;
+	private _val = 0;
+
+	private readonly _values: number[] = [];
+	private _index: number = 0;
+	private _sum = 0;
+
+	constructor(size: number) {
+		this._values = new Array(size);
+		this._values.fill(0, 0, size);
+	}
+
+	update(value: number): number {
+		const oldValue = this._values[this._index];
+		this._values[this._index] = value;
+		this._index = (this._index + 1) % this._values.length;
+
+		this._sum -= oldValue;
+		this._sum += value;
+
+		if (this._n < this._values.length) {
+			this._n += 1;
+		}
+
+		this._val = this._sum / this._n;
+		return this._val;
+	}
+
+	get value(): number {
+		return this._val;
+	}
+}
+
+/** Returns whether the point is within the triangle formed by the following 6 x/y point pairs */
+export function isPointWithinTriangle(
+	x: number, y: number,
+	ax: number, ay: number,
+	bx: number, by: number,
+	cx: number, cy: number
+) {
+	const v0x = cx - ax;
+	const v0y = cy - ay;
+	const v1x = bx - ax;
+	const v1y = by - ay;
+	const v2x = x - ax;
+	const v2y = y - ay;
+
+	const dot00 = v0x * v0x + v0y * v0y;
+	const dot01 = v0x * v1x + v0y * v1y;
+	const dot02 = v0x * v2x + v0y * v2y;
+	const dot11 = v1x * v1x + v1y * v1y;
+	const dot12 = v1x * v2x + v1y * v2y;
+
+	const invDenom = 1 / (dot00 * dot11 - dot01 * dot01);
+	const u = (dot11 * dot02 - dot01 * dot12) * invDenom;
+	const v = (dot00 * dot12 - dot01 * dot02) * invDenom;
+
+	return u >= 0 && v >= 0 && u + v < 1;
+}
diff --git a/src/vs/base/common/observable.ts b/src/vs/base/common/observable.ts
new file mode 100644
index 0000000000..c090a27206
--- /dev/null
+++ b/src/vs/base/common/observable.ts
@@ -0,0 +1,76 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+// This is a facade for the observable implementation. Only import from here!
+
+export type {
+	IObservable,
+	IObserver,
+	IReader,
+	ISettable,
+	ISettableObservable,
+	ITransaction,
+	IChangeContext,
+	IChangeTracker,
+} from 'vs/base/common/observableInternal/base';
+
+export {
+	observableValue,
+	disposableObservableValue,
+	transaction,
+	subtransaction,
+} from 'vs/base/common/observableInternal/base';
+export {
+	derived,
+	derivedOpts,
+	derivedHandleChanges,
+	derivedWithStore,
+} from 'vs/base/common/observableInternal/derived';
+export {
+	autorun,
+	autorunDelta,
+	autorunHandleChanges,
+	autorunWithStore,
+	autorunOpts,
+	autorunWithStoreHandleChanges,
+} from 'vs/base/common/observableInternal/autorun';
+export type {
+	IObservableSignal,
+} from 'vs/base/common/observableInternal/utils';
+export {
+	constObservable,
+	debouncedObservable,
+	derivedObservableWithCache,
+	derivedObservableWithWritableCache,
+	keepObserved,
+	recomputeInitiallyAndOnChange,
+	observableFromEvent,
+	observableFromPromise,
+	observableSignal,
+	observableSignalFromEvent,
+	wasEventTriggeredRecently,
+} from 'vs/base/common/observableInternal/utils';
+export {
+	ObservableLazy,
+	ObservableLazyPromise,
+	ObservablePromise,
+	PromiseResult,
+	waitForState,
+	derivedWithCancellationToken,
+} from 'vs/base/common/observableInternal/promise';
+export {
+	observableValueOpts
+} from 'vs/base/common/observableInternal/api';
+
+import { ConsoleObservableLogger, setLogger } from 'vs/base/common/observableInternal/logging';
+
+// Remove "//" in the next line to enable logging
+const enableLogging = false
+	// || Boolean("true") // done "weirdly" so that a lint warning prevents you from pushing this
+	;
+
+if (enableLogging) {
+	setLogger(new ConsoleObservableLogger());
+}
diff --git a/src/vs/base/common/observableInternal/api.ts b/src/vs/base/common/observableInternal/api.ts
new file mode 100644
index 0000000000..6e56671b7e
--- /dev/null
+++ b/src/vs/base/common/observableInternal/api.ts
@@ -0,0 +1,31 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { EqualityComparer, strictEquals } from 'vs/base/common/equals';
+import { ISettableObservable } from 'vs/base/common/observable';
+import { ObservableValue } from 'vs/base/common/observableInternal/base';
+import { IDebugNameData, DebugNameData } from 'vs/base/common/observableInternal/debugName';
+import { LazyObservableValue } from 'vs/base/common/observableInternal/lazyObservableValue';
+
+export function observableValueOpts<T, TChange = void>(
+	options: IDebugNameData & {
+		equalsFn?: EqualityComparer<T>;
+		lazy?: boolean;
+	},
+	initialValue: T
+): ISettableObservable<T, TChange> {
+	if (options.lazy) {
+		return new LazyObservableValue(
+			new DebugNameData(options.owner, options.debugName, undefined),
+			initialValue,
+			options.equalsFn ?? strictEquals,
+		);
+	}
+	return new ObservableValue(
+		new DebugNameData(options.owner, options.debugName, undefined),
+		initialValue,
+		options.equalsFn ?? strictEquals,
+	);
+}
diff --git a/src/vs/base/common/observableInternal/autorun.ts b/src/vs/base/common/observableInternal/autorun.ts
new file mode 100644
index 0000000000..845e870d65
--- /dev/null
+++ b/src/vs/base/common/observableInternal/autorun.ts
@@ -0,0 +1,281 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { assertFn } from 'vs/base/common/assert';
+import { DisposableStore, IDisposable, markAsDisposed, toDisposable, trackDisposable } from 'vs/base/common/lifecycle';
+import { IReader, IObservable, IObserver, IChangeContext } from 'vs/base/common/observableInternal/base';
+import { DebugNameData, IDebugNameData } from 'vs/base/common/observableInternal/debugName';
+import { getLogger } from 'vs/base/common/observableInternal/logging';
+
+/**
+ * Runs immediately and whenever a transaction ends and an observed observable changed.
+ * {@link fn} should start with a JS Doc using `@description` to name the autorun.
+ */
+export function autorun(fn: (reader: IReader) => void): IDisposable {
+	return new AutorunObserver(
+		new DebugNameData(undefined, undefined, fn),
+		fn,
+		undefined,
+		undefined
+	);
+}
+
+/**
+ * Runs immediately and whenever a transaction ends and an observed observable changed.
+ * {@link fn} should start with a JS Doc using `@description` to name the autorun.
+ */
+export function autorunOpts(options: IDebugNameData & {}, fn: (reader: IReader) => void): IDisposable {
+	return new AutorunObserver(
+		new DebugNameData(options.owner, options.debugName, options.debugReferenceFn ?? fn),
+		fn,
+		undefined,
+		undefined
+	);
+}
+
+/**
+ * Runs immediately and whenever a transaction ends and an observed observable changed.
+ * {@link fn} should start with a JS Doc using `@description` to name the autorun.
+ *
+ * Use `createEmptyChangeSummary` to create a "change summary" that can collect the changes.
+ * Use `handleChange` to add a reported change to the change summary.
+ * The run function is given the last change summary.
+ * The change summary is discarded after the run function was called.
+ *
+ * @see autorun
+ */
+export function autorunHandleChanges<TChangeSummary>(
+	options: IDebugNameData & {
+		createEmptyChangeSummary?: () => TChangeSummary;
+		handleChange: (context: IChangeContext, changeSummary: TChangeSummary) => boolean;
+	},
+	fn: (reader: IReader, changeSummary: TChangeSummary) => void
+): IDisposable {
+	return new AutorunObserver(
+		new DebugNameData(options.owner, options.debugName, options.debugReferenceFn ?? fn),
+		fn,
+		options.createEmptyChangeSummary,
+		options.handleChange
+	);
+}
+
+/**
+ * @see autorunHandleChanges (but with a disposable store that is cleared before the next run or on dispose)
+ */
+export function autorunWithStoreHandleChanges<TChangeSummary>(
+	options: IDebugNameData & {
+		createEmptyChangeSummary?: () => TChangeSummary;
+		handleChange: (context: IChangeContext, changeSummary: TChangeSummary) => boolean;
+	},
+	fn: (reader: IReader, changeSummary: TChangeSummary, store: DisposableStore) => void
+): IDisposable {
+	const store = new DisposableStore();
+	const disposable = autorunHandleChanges(
+		{
+			owner: options.owner,
+			debugName: options.debugName,
+			debugReferenceFn: options.debugReferenceFn ?? fn,
+			createEmptyChangeSummary: options.createEmptyChangeSummary,
+			handleChange: options.handleChange,
+		},
+		(reader, changeSummary) => {
+			store.clear();
+			fn(reader, changeSummary, store);
+		}
+	);
+	return toDisposable(() => {
+		disposable.dispose();
+		store.dispose();
+	});
+}
+
+/**
+ * @see autorun (but with a disposable store that is cleared before the next run or on dispose)
+ */
+export function autorunWithStore(fn: (reader: IReader, store: DisposableStore) => void): IDisposable {
+	const store = new DisposableStore();
+	const disposable = autorunOpts(
+		{
+			owner: undefined,
+			debugName: undefined,
+			debugReferenceFn: fn,
+		},
+		reader => {
+			store.clear();
+			fn(reader, store);
+		}
+	);
+	return toDisposable(() => {
+		disposable.dispose();
+		store.dispose();
+	});
+}
+
+export function autorunDelta<T>(
+	observable: IObservable<T>,
+	handler: (args: { lastValue: T | undefined; newValue: T }) => void
+): IDisposable {
+	let _lastValue: T | undefined;
+	return autorunOpts({ debugReferenceFn: handler }, (reader) => {
+		const newValue = observable.read(reader);
+		const lastValue = _lastValue;
+		_lastValue = newValue;
+		handler({ lastValue, newValue });
+	});
+}
+
+
+const enum AutorunState {
+	/**
+	 * A dependency could have changed.
+	 * We need to explicitly ask them if at least one dependency changed.
+	 */
+	dependenciesMightHaveChanged = 1,
+
+	/**
+	 * A dependency changed and we need to recompute.
+	 */
+	stale = 2,
+	upToDate = 3,
+}
+
+export class AutorunObserver<TChangeSummary = any> implements IObserver, IReader, IDisposable {
+	private state = AutorunState.stale;
+	private updateCount = 0;
+	private disposed = false;
+	private dependencies = new Set<IObservable<any>>();
+	private dependenciesToBeRemoved = new Set<IObservable<any>>();
+	private changeSummary: TChangeSummary | undefined;
+
+	public get debugName(): string {
+		return this._debugNameData.getDebugName(this) ?? '(anonymous)';
+	}
+
+	constructor(
+		public readonly _debugNameData: DebugNameData,
+		public readonly _runFn: (reader: IReader, changeSummary: TChangeSummary) => void,
+		private readonly createChangeSummary: (() => TChangeSummary) | undefined,
+		private readonly _handleChange: ((context: IChangeContext, summary: TChangeSummary) => boolean) | undefined,
+	) {
+		this.changeSummary = this.createChangeSummary?.();
+		getLogger()?.handleAutorunCreated(this);
+		this._runIfNeeded();
+
+		trackDisposable(this);
+	}
+
+	public dispose(): void {
+		this.disposed = true;
+		for (const o of this.dependencies) {
+			o.removeObserver(this);
+		}
+		this.dependencies.clear();
+
+		markAsDisposed(this);
+	}
+
+	private _runIfNeeded() {
+		if (this.state === AutorunState.upToDate) {
+			return;
+		}
+
+		const emptySet = this.dependenciesToBeRemoved;
+		this.dependenciesToBeRemoved = this.dependencies;
+		this.dependencies = emptySet;
+
+		this.state = AutorunState.upToDate;
+
+		const isDisposed = this.disposed;
+		try {
+			if (!isDisposed) {
+				getLogger()?.handleAutorunTriggered(this);
+				const changeSummary = this.changeSummary!;
+				this.changeSummary = this.createChangeSummary?.();
+				this._runFn(this, changeSummary);
+			}
+		} finally {
+			if (!isDisposed) {
+				getLogger()?.handleAutorunFinished(this);
+			}
+			// We don't want our observed observables to think that they are (not even temporarily) not being observed.
+			// Thus, we only unsubscribe from observables that are definitely not read anymore.
+			for (const o of this.dependenciesToBeRemoved) {
+				o.removeObserver(this);
+			}
+			this.dependenciesToBeRemoved.clear();
+		}
+	}
+
+	public toString(): string {
+		return `Autorun<${this.debugName}>`;
+	}
+
+	// IObserver implementation
+	public beginUpdate(): void {
+		if (this.state === AutorunState.upToDate) {
+			this.state = AutorunState.dependenciesMightHaveChanged;
+		}
+		this.updateCount++;
+	}
+
+	public endUpdate(): void {
+		if (this.updateCount === 1) {
+			do {
+				if (this.state === AutorunState.dependenciesMightHaveChanged) {
+					this.state = AutorunState.upToDate;
+					for (const d of this.dependencies) {
+						d.reportChanges();
+						if (this.state as AutorunState === AutorunState.stale) {
+							// The other dependencies will refresh on demand
+							break;
+						}
+					}
+				}
+
+				this._runIfNeeded();
+			} while (this.state !== AutorunState.upToDate);
+		}
+		this.updateCount--;
+
+		assertFn(() => this.updateCount >= 0);
+	}
+
+	public handlePossibleChange(observable: IObservable<any>): void {
+		if (this.state === AutorunState.upToDate && this.dependencies.has(observable) && !this.dependenciesToBeRemoved.has(observable)) {
+			this.state = AutorunState.dependenciesMightHaveChanged;
+		}
+	}
+
+	public handleChange<T, TChange>(observable: IObservable<T, TChange>, change: TChange): void {
+		if (this.dependencies.has(observable) && !this.dependenciesToBeRemoved.has(observable)) {
+			const shouldReact = this._handleChange ? this._handleChange({
+				changedObservable: observable,
+				change,
+				didChange: (o): this is any => o === observable as any,
+			}, this.changeSummary!) : true;
+			if (shouldReact) {
+				this.state = AutorunState.stale;
+			}
+		}
+	}
+
+	// IReader implementation
+	public readObservable<T>(observable: IObservable<T>): T {
+		// In case the run action disposes the autorun
+		if (this.disposed) {
+			return observable.get();
+		}
+
+		observable.addObserver(this);
+		const value = observable.get();
+		this.dependencies.add(observable);
+		this.dependenciesToBeRemoved.delete(observable);
+		return value;
+	}
+}
+
+export namespace autorun {
+	export const Observer = AutorunObserver;
+}
diff --git a/src/vs/base/common/observableInternal/base.ts b/src/vs/base/common/observableInternal/base.ts
new file mode 100644
index 0000000000..3c63a20116
--- /dev/null
+++ b/src/vs/base/common/observableInternal/base.ts
@@ -0,0 +1,489 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { strictEquals, EqualityComparer } from 'vs/base/common/equals';
+import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
+import { keepObserved, recomputeInitiallyAndOnChange } from 'vs/base/common/observable';
+import { DebugNameData, DebugOwner, getFunctionName } from 'vs/base/common/observableInternal/debugName';
+import type { derivedOpts } from 'vs/base/common/observableInternal/derived';
+import { getLogger } from 'vs/base/common/observableInternal/logging';
+
+/**
+ * Represents an observable value.
+ *
+ * @template T The type of the values the observable can hold.
+ * @template TChange The type used to describe value changes
+ * (usually `void` and only used in advanced scenarios).
+ * While observers can miss temporary values of an observable,
+ * they will receive all change values (as long as they are subscribed)!
+ */
+export interface IObservable<T, TChange = unknown> {
+	/**
+	 * Returns the current value.
+	 *
+	 * Calls {@link IObserver.handleChange} if the observable notices that the value changed.
+	 * Must not be called from {@link IObserver.handleChange}!
+	 */
+	get(): T;
+
+	/**
+	 * Forces the observable to check for changes and report them.
+	 *
+	 * Has the same effect as calling {@link IObservable.get}, but does not force the observable
+	 * to actually construct the value, e.g. if change deltas are used.
+	 * Calls {@link IObserver.handleChange} if the observable notices that the value changed.
+	 * Must not be called from {@link IObserver.handleChange}!
+	 */
+	reportChanges(): void;
+
+	/**
+	 * Adds the observer to the set of subscribed observers.
+	 * This method is idempotent.
+	 */
+	addObserver(observer: IObserver): void;
+
+	/**
+	 * Removes the observer from the set of subscribed observers.
+	 * This method is idempotent.
+	 */
+	removeObserver(observer: IObserver): void;
+
+	/**
+	 * Reads the current value and subscribes the reader to this observable.
+	 *
+	 * Calls {@link IReader.readObservable} if a reader is given, otherwise {@link IObservable.get}
+	 * (see {@link ConvenientObservable.read} for the implementation).
+	 */
+	read(reader: IReader | undefined): T;
+
+	/**
+	 * Creates a derived observable that depends on this observable.
+	 * Use the reader to read other observables
+	 * (see {@link ConvenientObservable.map} for the implementation).
+	 */
+	map<TNew>(fn: (value: T, reader: IReader) => TNew): IObservable<TNew>;
+	map<TNew>(owner: object, fn: (value: T, reader: IReader) => TNew): IObservable<TNew>;
+
+	/**
+	 * Makes sure this value is computed eagerly.
+	 */
+	recomputeInitiallyAndOnChange(store: DisposableStore, handleValue?: (value: T) => void): IObservable<T>;
+
+	/**
+	 * Makes sure this value is cached.
+	 */
+	keepObserved(store: DisposableStore): IObservable<T>;
+
+	/**
+	 * A human-readable name for debugging purposes.
+	 */
+	readonly debugName: string;
+
+	/**
+	 * This property captures the type of the change object. Do not use it at runtime!
+	 */
+	readonly TChange: TChange;
+}
+
+export interface IReader {
+	/**
+	 * Reads the value of an observable and subscribes to it.
+	 */
+	readObservable<T>(observable: IObservable<T, any>): T;
+}
+
+/**
+ * Represents an observer that can be subscribed to an observable.
+ *
+ * If an observer is subscribed to an observable and that observable didn't signal
+ * a change through one of the observer methods, the observer can assume that the
+ * observable didn't change.
+ * If an observable reported a possible change, {@link IObservable.reportChanges} forces
+ * the observable to report an actual change if there was one.
+ */
+export interface IObserver {
+	/**
+	 * Signals that the given observable might have changed and a transaction potentially modifying that observable started.
+	 * Before the given observable can call this method again, is must call {@link IObserver.endUpdate}.
+	 *
+	 * Implementations must not get/read the value of other observables, as they might not have received this event yet!
+	 * The method {@link IObservable.reportChanges} can be used to force the observable to report the changes.
+	 */
+	beginUpdate<T>(observable: IObservable<T>): void;
+
+	/**
+	 * Signals that the transaction that potentially modified the given observable ended.
+	 * This is a good place to react to (potential) changes.
+	 */
+	endUpdate<T>(observable: IObservable<T>): void;
+
+	/**
+	 * Signals that the given observable might have changed.
+	 * The method {@link IObservable.reportChanges} can be used to force the observable to report the changes.
+	 *
+	 * Implementations must not get/read the value of other observables, as they might not have received this event yet!
+	 * The change should be processed lazily or in {@link IObserver.endUpdate}.
+	 */
+	handlePossibleChange<T>(observable: IObservable<T>): void;
+
+	/**
+	 * Signals that the given {@link observable} changed.
+	 *
+	 * Implementations must not get/read the value of other observables, as they might not have received this event yet!
+	 * The change should be processed lazily or in {@link IObserver.endUpdate}.
+	 *
+	 * @param change Indicates how or why the value changed.
+	 */
+	handleChange<T, TChange>(observable: IObservable<T, TChange>, change: TChange): void;
+}
+
+export interface ISettable<T, TChange = void> {
+	/**
+	 * Sets the value of the observable.
+	 * Use a transaction to batch multiple changes (with a transaction, observers only react at the end of the transaction).
+	 *
+	 * @param transaction When given, value changes are handled on demand or when the transaction ends.
+	 * @param change Describes how or why the value changed.
+	 */
+	set(value: T, transaction: ITransaction | undefined, change: TChange): void;
+}
+
+export interface ITransaction {
+	/**
+	 * Calls {@link Observer.beginUpdate} immediately
+	 * and {@link Observer.endUpdate} when the transaction ends.
+	 */
+	updateObserver(observer: IObserver, observable: IObservable<any, any>): void;
+}
+
+let _recomputeInitiallyAndOnChange: typeof recomputeInitiallyAndOnChange;
+export function _setRecomputeInitiallyAndOnChange(recomputeInitiallyAndOnChange: typeof _recomputeInitiallyAndOnChange) {
+	_recomputeInitiallyAndOnChange = recomputeInitiallyAndOnChange;
+}
+
+let _keepObserved: typeof keepObserved;
+export function _setKeepObserved(keepObserved: typeof _keepObserved) {
+	_keepObserved = keepObserved;
+}
+
+
+let _derived: typeof derivedOpts;
+/**
+ * @internal
+ * This is to allow splitting files.
+*/
+export function _setDerivedOpts(derived: typeof _derived) {
+	_derived = derived;
+}
+
+export abstract class ConvenientObservable<T, TChange> implements IObservable<T, TChange> {
+	get TChange(): TChange { return null!; }
+
+	public abstract get(): T;
+
+	public reportChanges(): void {
+		this.get();
+	}
+
+	public abstract addObserver(observer: IObserver): void;
+	public abstract removeObserver(observer: IObserver): void;
+
+	/** @sealed */
+	public read(reader: IReader | undefined): T {
+		if (reader) {
+			return reader.readObservable(this);
+		} else {
+			return this.get();
+		}
+	}
+
+	/** @sealed */
+	public map<TNew>(fn: (value: T, reader: IReader) => TNew): IObservable<TNew>;
+	public map<TNew>(owner: DebugOwner, fn: (value: T, reader: IReader) => TNew): IObservable<TNew>;
+	public map<TNew>(fnOrOwner: DebugOwner | ((value: T, reader: IReader) => TNew), fnOrUndefined?: (value: T, reader: IReader) => TNew): IObservable<TNew> {
+		const owner = fnOrUndefined === undefined ? undefined : fnOrOwner as DebugOwner;
+		const fn = fnOrUndefined === undefined ? fnOrOwner as (value: T, reader: IReader) => TNew : fnOrUndefined;
+
+		return _derived(
+			{
+				owner,
+				debugName: () => {
+					const name = getFunctionName(fn);
+					if (name !== undefined) {
+						return name;
+					}
+
+					// regexp to match `x => x.y` or `x => x?.y` where x and y can be arbitrary identifiers (uses backref):
+					const regexp = /^\s*\(?\s*([a-zA-Z_$][a-zA-Z_$0-9]*)\s*\)?\s*=>\s*\1(?:\??)\.([a-zA-Z_$][a-zA-Z_$0-9]*)\s*$/;
+					const match = regexp.exec(fn.toString());
+					if (match) {
+						return `${this.debugName}.${match[2]}`;
+					}
+					if (!owner) {
+						return `${this.debugName} (mapped)`;
+					}
+					return undefined;
+				},
+				debugReferenceFn: fn,
+			},
+			(reader) => fn(this.read(reader), reader),
+		);
+	}
+
+	public recomputeInitiallyAndOnChange(store: DisposableStore, handleValue?: (value: T) => void): IObservable<T> {
+		store.add(_recomputeInitiallyAndOnChange!(this, handleValue));
+		return this;
+	}
+
+	/**
+	 * Ensures that this observable is observed. This keeps the cache alive.
+	 * However, in case of deriveds, it does not force eager evaluation (only when the value is read/get).
+	 * Use `recomputeInitiallyAndOnChange` for eager evaluation.
+	 */
+	public keepObserved(store: DisposableStore): IObservable<T> {
+		store.add(_keepObserved!(this));
+		return this;
+	}
+
+	public abstract get debugName(): string;
+
+	protected get debugValue() {
+		return this.get();
+	}
+}
+
+export abstract class BaseObservable<T, TChange = void> extends ConvenientObservable<T, TChange> {
+	protected readonly observers = new Set<IObserver>();
+
+	public addObserver(observer: IObserver): void {
+		const len = this.observers.size;
+		this.observers.add(observer);
+		if (len === 0) {
+			this.onFirstObserverAdded();
+		}
+	}
+
+	public removeObserver(observer: IObserver): void {
+		const deleted = this.observers.delete(observer);
+		if (deleted && this.observers.size === 0) {
+			this.onLastObserverRemoved();
+		}
+	}
+
+	protected onFirstObserverAdded(): void { }
+	protected onLastObserverRemoved(): void { }
+}
+
+/**
+ * Starts a transaction in which many observables can be changed at once.
+ * {@link fn} should start with a JS Doc using `@description` to give the transaction a debug name.
+ * Reaction run on demand or when the transaction ends.
+ */
+
+export function transaction(fn: (tx: ITransaction) => void, getDebugName?: () => string): void {
+	const tx = new TransactionImpl(fn, getDebugName);
+	try {
+		fn(tx);
+	} finally {
+		tx.finish();
+	}
+}
+
+let _globalTransaction: ITransaction | undefined = undefined;
+
+export function globalTransaction(fn: (tx: ITransaction) => void) {
+	if (_globalTransaction) {
+		fn(_globalTransaction);
+	} else {
+		const tx = new TransactionImpl(fn, undefined);
+		_globalTransaction = tx;
+		try {
+			fn(tx);
+		} finally {
+			tx.finish(); // During finish, more actions might be added to the transaction.
+			// Which is why we only clear the global transaction after finish.
+			_globalTransaction = undefined;
+		}
+	}
+}
+
+export async function asyncTransaction(fn: (tx: ITransaction) => Promise<void>, getDebugName?: () => string): Promise<void> {
+	const tx = new TransactionImpl(fn, getDebugName);
+	try {
+		await fn(tx);
+	} finally {
+		tx.finish();
+	}
+}
+
+/**
+ * Allows to chain transactions.
+ */
+export function subtransaction(tx: ITransaction | undefined, fn: (tx: ITransaction) => void, getDebugName?: () => string): void {
+	if (!tx) {
+		transaction(fn, getDebugName);
+	} else {
+		fn(tx);
+	}
+}
+
+export class TransactionImpl implements ITransaction {
+	private updatingObservers: { observer: IObserver; observable: IObservable<any> }[] | null = [];
+
+	constructor(public readonly _fn: Function, private readonly _getDebugName?: () => string) {
+		getLogger()?.handleBeginTransaction(this);
+	}
+
+	public getDebugName(): string | undefined {
+		if (this._getDebugName) {
+			return this._getDebugName();
+		}
+		return getFunctionName(this._fn);
+	}
+
+	public updateObserver(observer: IObserver, observable: IObservable<any>): void {
+		// When this gets called while finish is active, they will still get considered
+		this.updatingObservers!.push({ observer, observable });
+		observer.beginUpdate(observable);
+	}
+
+	public finish(): void {
+		const updatingObservers = this.updatingObservers!;
+		for (let i = 0; i < updatingObservers.length; i++) {
+			const { observer, observable } = updatingObservers[i];
+			observer.endUpdate(observable);
+		}
+		// Prevent anyone from updating observers from now on.
+		this.updatingObservers = null;
+		getLogger()?.handleEndTransaction();
+	}
+}
+
+/**
+ * A settable observable.
+ */
+export interface ISettableObservable<T, TChange = void> extends IObservable<T, TChange>, ISettable<T, TChange> {
+}
+
+/**
+ * Creates an observable value.
+ * Observers get informed when the value changes.
+ * @template TChange An arbitrary type to describe how or why the value changed. Defaults to `void`.
+ * Observers will receive every single change value.
+ */
+export function observableValue<T, TChange = void>(name: string, initialValue: T): ISettableObservable<T, TChange>;
+export function observableValue<T, TChange = void>(owner: object, initialValue: T): ISettableObservable<T, TChange>;
+export function observableValue<T, TChange = void>(nameOrOwner: string | object, initialValue: T): ISettableObservable<T, TChange> {
+	let debugNameData: DebugNameData;
+	if (typeof nameOrOwner === 'string') {
+		debugNameData = new DebugNameData(undefined, nameOrOwner, undefined);
+	} else {
+		debugNameData = new DebugNameData(nameOrOwner, undefined, undefined);
+	}
+	return new ObservableValue(debugNameData, initialValue, strictEquals);
+}
+
+export class ObservableValue<T, TChange = void>
+	extends BaseObservable<T, TChange>
+	implements ISettableObservable<T, TChange> {
+	protected _value: T;
+
+	get debugName() {
+		return this._debugNameData.getDebugName(this) ?? 'ObservableValue';
+	}
+
+	constructor(
+		private readonly _debugNameData: DebugNameData,
+		initialValue: T,
+		private readonly _equalityComparator: EqualityComparer<T>,
+	) {
+		super();
+		this._value = initialValue;
+	}
+	public override get(): T {
+		return this._value;
+	}
+
+	public set(value: T, tx: ITransaction | undefined, change: TChange): void {
+		if (change === undefined && this._equalityComparator(this._value, value)) {
+			return;
+		}
+
+		let _tx: TransactionImpl | undefined;
+		if (!tx) {
+			tx = _tx = new TransactionImpl(() => { }, () => `Setting ${this.debugName}`);
+		}
+		try {
+			const oldValue = this._value;
+			this._setValue(value);
+			getLogger()?.handleObservableChanged(this, { oldValue, newValue: value, change, didChange: true, hadValue: true });
+
+			for (const observer of this.observers) {
+				tx.updateObserver(observer, this);
+				observer.handleChange(this, change);
+			}
+		} finally {
+			if (_tx) {
+				_tx.finish();
+			}
+		}
+	}
+
+	override toString(): string {
+		return `${this.debugName}: ${this._value}`;
+	}
+
+	protected _setValue(newValue: T): void {
+		this._value = newValue;
+	}
+}
+
+/**
+ * A disposable observable. When disposed, its value is also disposed.
+ * When a new value is set, the previous value is disposed.
+ */
+export function disposableObservableValue<T extends IDisposable | undefined, TChange = void>(nameOrOwner: string | object, initialValue: T): ISettableObservable<T, TChange> & IDisposable {
+	let debugNameData: DebugNameData;
+	if (typeof nameOrOwner === 'string') {
+		debugNameData = new DebugNameData(undefined, nameOrOwner, undefined);
+	} else {
+		debugNameData = new DebugNameData(nameOrOwner, undefined, undefined);
+	}
+	return new DisposableObservableValue(debugNameData, initialValue, strictEquals);
+}
+
+export class DisposableObservableValue<T extends IDisposable | undefined, TChange = void> extends ObservableValue<T, TChange> implements IDisposable {
+	protected override _setValue(newValue: T): void {
+		if (this._value === newValue) {
+			return;
+		}
+		if (this._value) {
+			this._value.dispose();
+		}
+		this._value = newValue;
+	}
+
+	public dispose(): void {
+		this._value?.dispose();
+	}
+}
+
+export interface IChangeTracker {
+	/**
+	 * Returns if this change should cause an invalidation.
+	 * Implementations can record changes.
+	*/
+	handleChange(context: IChangeContext): boolean;
+}
+
+export interface IChangeContext {
+	readonly changedObservable: IObservable<any, any>;
+	readonly change: unknown;
+
+	/**
+	 * Returns if the given observable caused the change.
+	 */
+	didChange<T, TChange>(observable: IObservable<T, TChange>): this is { change: TChange };
+}
diff --git a/src/vs/base/common/observableInternal/debugName.ts b/src/vs/base/common/observableInternal/debugName.ts
new file mode 100644
index 0000000000..1ff1f24435
--- /dev/null
+++ b/src/vs/base/common/observableInternal/debugName.ts
@@ -0,0 +1,145 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+export interface IDebugNameData {
+	/**
+	 * The owner object of an observable.
+	 * Used for debugging only, such as computing a name for the observable by iterating over the fields of the owner.
+	 */
+	readonly owner?: DebugOwner | undefined;
+
+	/**
+	 * A string or function that returns a string that represents the name of the observable.
+	 * Used for debugging only.
+	 */
+	readonly debugName?: DebugNameSource | undefined;
+
+	/**
+	 * A function that points to the defining function of the object.
+	 * Used for debugging only.
+	 */
+	readonly debugReferenceFn?: Function | undefined;
+}
+
+export class DebugNameData {
+	constructor(
+		public readonly owner: DebugOwner | undefined,
+		public readonly debugNameSource: DebugNameSource | undefined,
+		public readonly referenceFn: Function | undefined,
+	) { }
+
+	public getDebugName(target: object): string | undefined {
+		return getDebugName(target, this);
+	}
+}
+
+/**
+ * The owning object of an observable.
+ * Is only used for debugging purposes, such as computing a name for the observable by iterating over the fields of the owner.
+ */
+export type DebugOwner = object | undefined;
+export type DebugNameSource = string | (() => string | undefined);
+
+const countPerName = new Map<string, number>();
+const cachedDebugName = new WeakMap<object, string>();
+
+export function getDebugName(target: object, data: DebugNameData): string | undefined {
+	const cached = cachedDebugName.get(target);
+	if (cached) {
+		return cached;
+	}
+
+	const dbgName = computeDebugName(target, data);
+	if (dbgName) {
+		let count = countPerName.get(dbgName) ?? 0;
+		count++;
+		countPerName.set(dbgName, count);
+		const result = count === 1 ? dbgName : `${dbgName}#${count}`;
+		cachedDebugName.set(target, result);
+		return result;
+	}
+	return undefined;
+}
+
+function computeDebugName(self: object, data: DebugNameData): string | undefined {
+	const cached = cachedDebugName.get(self);
+	if (cached) {
+		return cached;
+	}
+
+	const ownerStr = data.owner ? formatOwner(data.owner) + `.` : '';
+
+	let result: string | undefined;
+	const debugNameSource = data.debugNameSource;
+	if (debugNameSource !== undefined) {
+		if (typeof debugNameSource === 'function') {
+			result = debugNameSource();
+			if (result !== undefined) {
+				return ownerStr + result;
+			}
+		} else {
+			return ownerStr + debugNameSource;
+		}
+	}
+
+	const referenceFn = data.referenceFn;
+	if (referenceFn !== undefined) {
+		result = getFunctionName(referenceFn);
+		if (result !== undefined) {
+			return ownerStr + result;
+		}
+	}
+
+	if (data.owner !== undefined) {
+		const key = findKey(data.owner, self);
+		if (key !== undefined) {
+			return ownerStr + key;
+		}
+	}
+	return undefined;
+}
+
+function findKey(obj: object, value: object): string | undefined {
+	for (const key in obj) {
+		if ((obj as any)[key] === value) {
+			return key;
+		}
+	}
+	return undefined;
+}
+
+const countPerClassName = new Map<string, number>();
+const ownerId = new WeakMap<object, string>();
+
+function formatOwner(owner: object): string {
+	const id = ownerId.get(owner);
+	if (id) {
+		return id;
+	}
+	const className = getClassName(owner);
+	let count = countPerClassName.get(className) ?? 0;
+	count++;
+	countPerClassName.set(className, count);
+	const result = count === 1 ? className : `${className}#${count}`;
+	ownerId.set(owner, result);
+	return result;
+}
+
+function getClassName(obj: object): string {
+	const ctor = obj.constructor;
+	if (ctor) {
+		return ctor.name;
+	}
+	return 'Object';
+}
+
+export function getFunctionName(fn: Function): string | undefined {
+	const fnSrc = fn.toString();
+	// Pattern: /** @description ... */
+	const regexp = /\/\*\*\s*@description\s*([^*]*)\*\//;
+	const match = regexp.exec(fnSrc);
+	const result = match ? match[1] : undefined;
+	return result?.trim();
+}
diff --git a/src/vs/base/common/observableInternal/derived.ts b/src/vs/base/common/observableInternal/derived.ts
new file mode 100644
index 0000000000..8de22247db
--- /dev/null
+++ b/src/vs/base/common/observableInternal/derived.ts
@@ -0,0 +1,428 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { assertFn } from 'vs/base/common/assert';
+import { EqualityComparer, strictEquals } from 'vs/base/common/equals';
+import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
+import { BaseObservable, IChangeContext, IObservable, IObserver, IReader, ISettableObservable, ITransaction, _setDerivedOpts, } from 'vs/base/common/observableInternal/base';
+import { DebugNameData, IDebugNameData, DebugOwner } from 'vs/base/common/observableInternal/debugName';
+import { getLogger } from 'vs/base/common/observableInternal/logging';
+
+/**
+ * Creates an observable that is derived from other observables.
+ * The value is only recomputed when absolutely needed.
+ *
+ * {@link computeFn} should start with a JS Doc using `@description` to name the derived.
+ */
+export function derived<T>(computeFn: (reader: IReader) => T): IObservable<T>;
+export function derived<T>(owner: DebugOwner, computeFn: (reader: IReader) => T): IObservable<T>;
+export function derived<T>(computeFnOrOwner: ((reader: IReader) => T) | DebugOwner, computeFn?: ((reader: IReader) => T) | undefined): IObservable<T> {
+	if (computeFn !== undefined) {
+		return new Derived(
+			new DebugNameData(computeFnOrOwner, undefined, computeFn),
+			computeFn,
+			undefined,
+			undefined,
+			undefined,
+			strictEquals
+		);
+	}
+	return new Derived(
+		new DebugNameData(undefined, undefined, computeFnOrOwner as any),
+		computeFnOrOwner as any,
+		undefined,
+		undefined,
+		undefined,
+		strictEquals
+	);
+}
+
+export function derivedWithSetter<T>(owner: DebugOwner | undefined, computeFn: (reader: IReader) => T, setter: (value: T, transaction: ITransaction | undefined) => void): ISettableObservable<T> {
+	return new DerivedWithSetter(
+		new DebugNameData(owner, undefined, computeFn),
+		computeFn,
+		undefined,
+		undefined,
+		undefined,
+		strictEquals,
+		setter,
+	);
+}
+
+export function derivedOpts<T>(
+	options: IDebugNameData & {
+		equalsFn?: EqualityComparer<T>;
+		onLastObserverRemoved?: (() => void);
+	},
+	computeFn: (reader: IReader) => T
+): IObservable<T> {
+	return new Derived(
+		new DebugNameData(options.owner, options.debugName, options.debugReferenceFn),
+		computeFn,
+		undefined,
+		undefined,
+		options.onLastObserverRemoved,
+		options.equalsFn ?? strictEquals
+	);
+}
+
+_setDerivedOpts(derivedOpts);
+
+/**
+ * Represents an observable that is derived from other observables.
+ * The value is only recomputed when absolutely needed.
+ *
+ * {@link computeFn} should start with a JS Doc using `@description` to name the derived.
+ *
+ * Use `createEmptyChangeSummary` to create a "change summary" that can collect the changes.
+ * Use `handleChange` to add a reported change to the change summary.
+ * The compute function is given the last change summary.
+ * The change summary is discarded after the compute function was called.
+ *
+ * @see derived
+ */
+export function derivedHandleChanges<T, TChangeSummary>(
+	options: IDebugNameData & {
+		createEmptyChangeSummary: () => TChangeSummary;
+		handleChange: (context: IChangeContext, changeSummary: TChangeSummary) => boolean;
+		equalityComparer?: EqualityComparer<T>;
+	},
+	computeFn: (reader: IReader, changeSummary: TChangeSummary) => T
+): IObservable<T> {
+	return new Derived(
+		new DebugNameData(options.owner, options.debugName, undefined),
+		computeFn,
+		options.createEmptyChangeSummary,
+		options.handleChange,
+		undefined,
+		options.equalityComparer ?? strictEquals
+	);
+}
+
+export function derivedWithStore<T>(computeFn: (reader: IReader, store: DisposableStore) => T): IObservable<T>;
+export function derivedWithStore<T>(owner: object, computeFn: (reader: IReader, store: DisposableStore) => T): IObservable<T>;
+export function derivedWithStore<T>(computeFnOrOwner: ((reader: IReader, store: DisposableStore) => T) | object, computeFnOrUndefined?: ((reader: IReader, store: DisposableStore) => T)): IObservable<T> {
+	let computeFn: (reader: IReader, store: DisposableStore) => T;
+	let owner: DebugOwner;
+	if (computeFnOrUndefined === undefined) {
+		computeFn = computeFnOrOwner as any;
+		owner = undefined;
+	} else {
+		owner = computeFnOrOwner;
+		computeFn = computeFnOrUndefined as any;
+	}
+
+	const store = new DisposableStore();
+	return new Derived(
+		new DebugNameData(owner, undefined, computeFn),
+		r => {
+			store.clear();
+			return computeFn(r, store);
+		}, undefined,
+		undefined,
+		() => store.dispose(),
+		strictEquals
+	);
+}
+
+export function derivedDisposable<T extends IDisposable | undefined>(computeFn: (reader: IReader) => T): IObservable<T>;
+export function derivedDisposable<T extends IDisposable | undefined>(owner: DebugOwner, computeFn: (reader: IReader) => T): IObservable<T>;
+export function derivedDisposable<T extends IDisposable | undefined>(computeFnOrOwner: ((reader: IReader) => T) | DebugOwner, computeFnOrUndefined?: ((reader: IReader) => T)): IObservable<T> {
+	let computeFn: (reader: IReader) => T;
+	let owner: DebugOwner;
+	if (computeFnOrUndefined === undefined) {
+		computeFn = computeFnOrOwner as any;
+		owner = undefined;
+	} else {
+		owner = computeFnOrOwner;
+		computeFn = computeFnOrUndefined as any;
+	}
+
+	let store: DisposableStore | undefined = undefined;
+	return new Derived(
+		new DebugNameData(owner, undefined, computeFn),
+		r => {
+			if (!store) {
+				store = new DisposableStore();
+			} else {
+				store.clear();
+			}
+			const result = computeFn(r);
+			if (result) {
+				store.add(result);
+			}
+			return result;
+		}, undefined,
+		undefined,
+		() => {
+			if (store) {
+				store.dispose();
+				store = undefined;
+			}
+		},
+		strictEquals
+	);
+}
+
+const enum DerivedState {
+	/** Initial state, no previous value, recomputation needed */
+	initial = 0,
+
+	/**
+	 * A dependency could have changed.
+	 * We need to explicitly ask them if at least one dependency changed.
+	 */
+	dependenciesMightHaveChanged = 1,
+
+	/**
+	 * A dependency changed and we need to recompute.
+	 * After recomputation, we need to check the previous value to see if we changed as well.
+	 */
+	stale = 2,
+
+	/**
+	 * No change reported, our cached value is up to date.
+	 */
+	upToDate = 3,
+}
+
+export class Derived<T, TChangeSummary = any> extends BaseObservable<T, void> implements IReader, IObserver {
+	private state = DerivedState.initial;
+	private value: T | undefined = undefined;
+	private updateCount = 0;
+	private dependencies = new Set<IObservable<any>>();
+	private dependenciesToBeRemoved = new Set<IObservable<any>>();
+	private changeSummary: TChangeSummary | undefined = undefined;
+
+	public override get debugName(): string {
+		return this._debugNameData.getDebugName(this) ?? '(anonymous)';
+	}
+
+	constructor(
+		public readonly _debugNameData: DebugNameData,
+		public readonly _computeFn: (reader: IReader, changeSummary: TChangeSummary) => T,
+		private readonly createChangeSummary: (() => TChangeSummary) | undefined,
+		private readonly _handleChange: ((context: IChangeContext, summary: TChangeSummary) => boolean) | undefined,
+		private readonly _handleLastObserverRemoved: (() => void) | undefined = undefined,
+		private readonly _equalityComparator: EqualityComparer<T>,
+	) {
+		super();
+		this.changeSummary = this.createChangeSummary?.();
+		getLogger()?.handleDerivedCreated(this);
+	}
+
+	protected override onLastObserverRemoved(): void {
+		/**
+		 * We are not tracking changes anymore, thus we have to assume
+		 * that our cache is invalid.
+		 */
+		this.state = DerivedState.initial;
+		this.value = undefined;
+		for (const d of this.dependencies) {
+			d.removeObserver(this);
+		}
+		this.dependencies.clear();
+
+		this._handleLastObserverRemoved?.();
+	}
+
+	public override get(): T {
+		if (this.observers.size === 0) {
+			// Without observers, we don't know when to clean up stuff.
+			// Thus, we don't cache anything to prevent memory leaks.
+			const result = this._computeFn(this, this.createChangeSummary?.()!);
+			// Clear new dependencies
+			this.onLastObserverRemoved();
+			return result;
+		} else {
+			do {
+				// We might not get a notification for a dependency that changed while it is updating,
+				// thus we also have to ask all our depedencies if they changed in this case.
+				if (this.state === DerivedState.dependenciesMightHaveChanged) {
+					for (const d of this.dependencies) {
+						/** might call {@link handleChange} indirectly, which could make us stale */
+						d.reportChanges();
+
+						if (this.state as DerivedState === DerivedState.stale) {
+							// The other dependencies will refresh on demand, so early break
+							break;
+						}
+					}
+				}
+
+				// We called report changes of all dependencies.
+				// If we are still not stale, we can assume to be up to date again.
+				if (this.state === DerivedState.dependenciesMightHaveChanged) {
+					this.state = DerivedState.upToDate;
+				}
+
+				this._recomputeIfNeeded();
+				// In case recomputation changed one of our dependencies, we need to recompute again.
+			} while (this.state !== DerivedState.upToDate);
+			return this.value!;
+		}
+	}
+
+	private _recomputeIfNeeded() {
+		if (this.state === DerivedState.upToDate) {
+			return;
+		}
+		const emptySet = this.dependenciesToBeRemoved;
+		this.dependenciesToBeRemoved = this.dependencies;
+		this.dependencies = emptySet;
+
+		const hadValue = this.state !== DerivedState.initial;
+		const oldValue = this.value;
+		this.state = DerivedState.upToDate;
+
+		const changeSummary = this.changeSummary!;
+		this.changeSummary = this.createChangeSummary?.();
+		try {
+			/** might call {@link handleChange} indirectly, which could invalidate us */
+			this.value = this._computeFn(this, changeSummary);
+		} finally {
+			// We don't want our observed observables to think that they are (not even temporarily) not being observed.
+			// Thus, we only unsubscribe from observables that are definitely not read anymore.
+			for (const o of this.dependenciesToBeRemoved) {
+				o.removeObserver(this);
+			}
+			this.dependenciesToBeRemoved.clear();
+		}
+
+		const didChange = hadValue && !(this._equalityComparator(oldValue!, this.value));
+
+		getLogger()?.handleDerivedRecomputed(this, {
+			oldValue,
+			newValue: this.value,
+			change: undefined,
+			didChange,
+			hadValue,
+		});
+
+		if (didChange) {
+			for (const r of this.observers) {
+				r.handleChange(this, undefined);
+			}
+		}
+	}
+
+	public override toString(): string {
+		return `LazyDerived<${this.debugName}>`;
+	}
+
+	// IObserver Implementation
+	public beginUpdate<T>(_observable: IObservable<T>): void {
+		this.updateCount++;
+		const propagateBeginUpdate = this.updateCount === 1;
+		if (this.state === DerivedState.upToDate) {
+			this.state = DerivedState.dependenciesMightHaveChanged;
+			// If we propagate begin update, that will already signal a possible change.
+			if (!propagateBeginUpdate) {
+				for (const r of this.observers) {
+					r.handlePossibleChange(this);
+				}
+			}
+		}
+		if (propagateBeginUpdate) {
+			for (const r of this.observers) {
+				r.beginUpdate(this); // This signals a possible change
+			}
+		}
+	}
+
+	public endUpdate<T>(_observable: IObservable<T>): void {
+		this.updateCount--;
+		if (this.updateCount === 0) {
+			// End update could change the observer list.
+			const observers = [...this.observers];
+			for (const r of observers) {
+				r.endUpdate(this);
+			}
+		}
+		assertFn(() => this.updateCount >= 0);
+	}
+
+	public handlePossibleChange<T>(observable: IObservable<T, unknown>): void {
+		// In all other states, observers already know that we might have changed.
+		if (this.state === DerivedState.upToDate && this.dependencies.has(observable) && !this.dependenciesToBeRemoved.has(observable)) {
+			this.state = DerivedState.dependenciesMightHaveChanged;
+			for (const r of this.observers) {
+				r.handlePossibleChange(this);
+			}
+		}
+	}
+
+	public handleChange<T, TChange>(observable: IObservable<T, TChange>, change: TChange): void {
+		if (this.dependencies.has(observable) && !this.dependenciesToBeRemoved.has(observable)) {
+			const shouldReact = this._handleChange ? this._handleChange({
+				changedObservable: observable,
+				change,
+				didChange: (o): this is any => o === observable as any,
+			}, this.changeSummary!) : true;
+			const wasUpToDate = this.state === DerivedState.upToDate;
+			if (shouldReact && (this.state === DerivedState.dependenciesMightHaveChanged || wasUpToDate)) {
+				this.state = DerivedState.stale;
+				if (wasUpToDate) {
+					for (const r of this.observers) {
+						r.handlePossibleChange(this);
+					}
+				}
+			}
+		}
+	}
+
+	// IReader Implementation
+	public readObservable<T>(observable: IObservable<T>): T {
+		// Subscribe before getting the value to enable caching
+		observable.addObserver(this);
+		/** This might call {@link handleChange} indirectly, which could invalidate us */
+		const value = observable.get();
+		// Which is why we only add the observable to the dependencies now.
+		this.dependencies.add(observable);
+		this.dependenciesToBeRemoved.delete(observable);
+		return value;
+	}
+
+	public override addObserver(observer: IObserver): void {
+		const shouldCallBeginUpdate = !this.observers.has(observer) && this.updateCount > 0;
+		super.addObserver(observer);
+
+		if (shouldCallBeginUpdate) {
+			observer.beginUpdate(this);
+		}
+	}
+
+	public override removeObserver(observer: IObserver): void {
+		const shouldCallEndUpdate = this.observers.has(observer) && this.updateCount > 0;
+		super.removeObserver(observer);
+
+		if (shouldCallEndUpdate) {
+			// Calling end update after removing the observer makes sure endUpdate cannot be called twice here.
+			observer.endUpdate(this);
+		}
+	}
+}
+
+
+export class DerivedWithSetter<T, TChangeSummary = any> extends Derived<T, TChangeSummary> implements ISettableObservable<T> {
+	constructor(
+		debugNameData: DebugNameData,
+		computeFn: (reader: IReader, changeSummary: TChangeSummary) => T,
+		createChangeSummary: (() => TChangeSummary) | undefined,
+		handleChange: ((context: IChangeContext, summary: TChangeSummary) => boolean) | undefined,
+		handleLastObserverRemoved: (() => void) | undefined = undefined,
+		equalityComparator: EqualityComparer<T>,
+		public readonly set: (value: T, tx: ITransaction | undefined) => void,
+	) {
+		super(
+			debugNameData,
+			computeFn,
+			createChangeSummary,
+			handleChange,
+			handleLastObserverRemoved,
+			equalityComparator,
+		);
+	}
+}
diff --git a/src/vs/base/common/observableInternal/lazyObservableValue.ts b/src/vs/base/common/observableInternal/lazyObservableValue.ts
new file mode 100644
index 0000000000..1c35f45816
--- /dev/null
+++ b/src/vs/base/common/observableInternal/lazyObservableValue.ts
@@ -0,0 +1,146 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { EqualityComparer } from 'vs/base/common/equals';
+import { ISettableObservable, ITransaction } from 'vs/base/common/observable';
+import { BaseObservable, IObserver, TransactionImpl } from 'vs/base/common/observableInternal/base';
+import { DebugNameData } from 'vs/base/common/observableInternal/debugName';
+
+/**
+ * Holds off updating observers until the value is actually read.
+*/
+export class LazyObservableValue<T, TChange = void>
+	extends BaseObservable<T, TChange>
+	implements ISettableObservable<T, TChange> {
+	protected _value: T;
+	private _isUpToDate = true;
+	private readonly _deltas: TChange[] = [];
+
+	get debugName() {
+		return this._debugNameData.getDebugName(this) ?? 'LazyObservableValue';
+	}
+
+	constructor(
+		private readonly _debugNameData: DebugNameData,
+		initialValue: T,
+		private readonly _equalityComparator: EqualityComparer<T>,
+	) {
+		super();
+		this._value = initialValue;
+	}
+
+	public override get(): T {
+		this._update();
+		return this._value;
+	}
+
+	private _update(): void {
+		if (this._isUpToDate) {
+			return;
+		}
+		this._isUpToDate = true;
+
+		if (this._deltas.length > 0) {
+			for (const observer of this.observers) {
+				for (const change of this._deltas) {
+					observer.handleChange(this, change);
+				}
+			}
+			this._deltas.length = 0;
+		} else {
+			for (const observer of this.observers) {
+				observer.handleChange(this, undefined);
+			}
+		}
+	}
+
+	private _updateCounter = 0;
+
+	private _beginUpdate(): void {
+		this._updateCounter++;
+		if (this._updateCounter === 1) {
+			for (const observer of this.observers) {
+				observer.beginUpdate(this);
+			}
+		}
+	}
+
+	private _endUpdate(): void {
+		this._updateCounter--;
+		if (this._updateCounter === 0) {
+			this._update();
+
+			// End update could change the observer list.
+			const observers = [...this.observers];
+			for (const r of observers) {
+				r.endUpdate(this);
+			}
+		}
+	}
+
+	public override addObserver(observer: IObserver): void {
+		const shouldCallBeginUpdate = !this.observers.has(observer) && this._updateCounter > 0;
+		super.addObserver(observer);
+
+		if (shouldCallBeginUpdate) {
+			observer.beginUpdate(this);
+		}
+	}
+
+	public override removeObserver(observer: IObserver): void {
+		const shouldCallEndUpdate = this.observers.has(observer) && this._updateCounter > 0;
+		super.removeObserver(observer);
+
+		if (shouldCallEndUpdate) {
+			// Calling end update after removing the observer makes sure endUpdate cannot be called twice here.
+			observer.endUpdate(this);
+		}
+	}
+
+	public set(value: T, tx: ITransaction | undefined, change: TChange): void {
+		if (change === undefined && this._equalityComparator(this._value, value)) {
+			return;
+		}
+
+		let _tx: TransactionImpl | undefined;
+		if (!tx) {
+			tx = _tx = new TransactionImpl(() => { }, () => `Setting ${this.debugName}`);
+		}
+		try {
+			this._isUpToDate = false;
+			this._setValue(value);
+			if (change !== undefined) {
+				this._deltas.push(change);
+			}
+
+			tx.updateObserver({
+				beginUpdate: () => this._beginUpdate(),
+				endUpdate: () => this._endUpdate(),
+				handleChange: (observable, change) => { },
+				handlePossibleChange: (observable) => { },
+			}, this);
+
+			if (this._updateCounter > 1) {
+				// We already started begin/end update, so we need to manually call handlePossibleChange
+				for (const observer of this.observers) {
+					observer.handlePossibleChange(this);
+				}
+			}
+
+		} finally {
+			if (_tx) {
+				_tx.finish();
+			}
+		}
+	}
+
+	override toString(): string {
+		return `${this.debugName}: ${this._value}`;
+	}
+
+	protected _setValue(newValue: T): void {
+		this._value = newValue;
+	}
+}
diff --git a/src/vs/base/common/observableInternal/logging.ts b/src/vs/base/common/observableInternal/logging.ts
new file mode 100644
index 0000000000..5e4712e692
--- /dev/null
+++ b/src/vs/base/common/observableInternal/logging.ts
@@ -0,0 +1,328 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { AutorunObserver } from 'vs/base/common/observableInternal/autorun';
+import { IObservable, ObservableValue, TransactionImpl } from 'vs/base/common/observableInternal/base';
+import { Derived } from 'vs/base/common/observableInternal/derived';
+import { FromEventObservable } from 'vs/base/common/observableInternal/utils';
+
+let globalObservableLogger: IObservableLogger | undefined;
+
+export function setLogger(logger: IObservableLogger): void {
+	globalObservableLogger = logger;
+}
+
+export function getLogger(): IObservableLogger | undefined {
+	return globalObservableLogger;
+}
+
+interface IChangeInformation {
+	oldValue: unknown;
+	newValue: unknown;
+	change: unknown;
+	didChange: boolean;
+	hadValue: boolean;
+}
+
+export interface IObservableLogger {
+	handleObservableChanged(observable: ObservableValue<any, any>, info: IChangeInformation): void;
+	handleFromEventObservableTriggered(observable: FromEventObservable<any, any>, info: IChangeInformation): void;
+
+	handleAutorunCreated(autorun: AutorunObserver): void;
+	handleAutorunTriggered(autorun: AutorunObserver): void;
+	handleAutorunFinished(autorun: AutorunObserver): void;
+
+	handleDerivedCreated(observable: Derived<any>): void;
+	handleDerivedRecomputed(observable: Derived<any>, info: IChangeInformation): void;
+
+	handleBeginTransaction(transaction: TransactionImpl): void;
+	handleEndTransaction(): void;
+}
+
+export class ConsoleObservableLogger implements IObservableLogger {
+	private indentation = 0;
+
+	private textToConsoleArgs(text: ConsoleText): unknown[] {
+		return consoleTextToArgs([
+			normalText(repeat('|  ', this.indentation)),
+			text,
+		]);
+	}
+
+	private formatInfo(info: IChangeInformation): ConsoleText[] {
+		if (!info.hadValue) {
+			return [
+				normalText(` `),
+				styled(formatValue(info.newValue, 60), {
+					color: 'green',
+				}),
+				normalText(` (initial)`),
+			];
+		}
+		return info.didChange
+			? [
+				normalText(` `),
+				styled(formatValue(info.oldValue, 70), {
+					color: 'red',
+					strikeThrough: true,
+				}),
+				normalText(` `),
+				styled(formatValue(info.newValue, 60), {
+					color: 'green',
+				}),
+			]
+			: [normalText(` (unchanged)`)];
+	}
+
+	handleObservableChanged(observable: IObservable<unknown, unknown>, info: IChangeInformation): void {
+		console.log(...this.textToConsoleArgs([
+			formatKind('observable value changed'),
+			styled(observable.debugName, { color: 'BlueViolet' }),
+			...this.formatInfo(info),
+		]));
+	}
+
+	private readonly changedObservablesSets = new WeakMap<object, Set<IObservable<any, any>>>();
+
+	formatChanges(changes: Set<IObservable<any, any>>): ConsoleText | undefined {
+		if (changes.size === 0) {
+			return undefined;
+		}
+		return styled(
+			' (changed deps: ' +
+			[...changes].map((o) => o.debugName).join(', ') +
+			')',
+			{ color: 'gray' }
+		);
+	}
+
+	handleDerivedCreated(derived: Derived<unknown>): void {
+		const existingHandleChange = derived.handleChange;
+		this.changedObservablesSets.set(derived, new Set());
+		derived.handleChange = (observable, change) => {
+			this.changedObservablesSets.get(derived)!.add(observable);
+			return existingHandleChange.apply(derived, [observable, change]);
+		};
+	}
+
+	handleDerivedRecomputed(derived: Derived<unknown>, info: IChangeInformation): void {
+		const changedObservables = this.changedObservablesSets.get(derived)!;
+		console.log(...this.textToConsoleArgs([
+			formatKind('derived recomputed'),
+			styled(derived.debugName, { color: 'BlueViolet' }),
+			...this.formatInfo(info),
+			this.formatChanges(changedObservables),
+			{ data: [{ fn: derived._debugNameData.referenceFn ?? derived._computeFn }] }
+		]));
+		changedObservables.clear();
+	}
+
+	handleFromEventObservableTriggered(observable: FromEventObservable<any, any>, info: IChangeInformation): void {
+		console.log(...this.textToConsoleArgs([
+			formatKind('observable from event triggered'),
+			styled(observable.debugName, { color: 'BlueViolet' }),
+			...this.formatInfo(info),
+			{ data: [{ fn: observable._getValue }] }
+		]));
+	}
+
+	handleAutorunCreated(autorun: AutorunObserver): void {
+		const existingHandleChange = autorun.handleChange;
+		this.changedObservablesSets.set(autorun, new Set());
+		autorun.handleChange = (observable, change) => {
+			this.changedObservablesSets.get(autorun)!.add(observable);
+			return existingHandleChange.apply(autorun, [observable, change]);
+		};
+	}
+
+	handleAutorunTriggered(autorun: AutorunObserver): void {
+		const changedObservables = this.changedObservablesSets.get(autorun)!;
+		console.log(...this.textToConsoleArgs([
+			formatKind('autorun'),
+			styled(autorun.debugName, { color: 'BlueViolet' }),
+			this.formatChanges(changedObservables),
+			{ data: [{ fn: autorun._debugNameData.referenceFn ?? autorun._runFn }] }
+		]));
+		changedObservables.clear();
+		this.indentation++;
+	}
+
+	handleAutorunFinished(autorun: AutorunObserver): void {
+		this.indentation--;
+	}
+
+	handleBeginTransaction(transaction: TransactionImpl): void {
+		let transactionName = transaction.getDebugName();
+		if (transactionName === undefined) {
+			transactionName = '';
+		}
+		console.log(...this.textToConsoleArgs([
+			formatKind('transaction'),
+			styled(transactionName, { color: 'BlueViolet' }),
+			{ data: [{ fn: transaction._fn }] }
+		]));
+		this.indentation++;
+	}
+
+	handleEndTransaction(): void {
+		this.indentation--;
+	}
+}
+
+type ConsoleText =
+	| (ConsoleText | undefined)[]
+	| { text: string; style: string; data?: unknown[] }
+	| { data: unknown[] };
+
+function consoleTextToArgs(text: ConsoleText): unknown[] {
+	const styles = new Array<any>();
+	const data: unknown[] = [];
+	let firstArg = '';
+
+	function process(t: ConsoleText): void {
+		if ('length' in t) {
+			for (const item of t) {
+				if (item) {
+					process(item);
+				}
+			}
+		} else if ('text' in t) {
+			firstArg += `%c${t.text}`;
+			styles.push(t.style);
+			if (t.data) {
+				data.push(...t.data);
+			}
+		} else if ('data' in t) {
+			data.push(...t.data);
+		}
+	}
+
+	process(text);
+
+	const result = [firstArg, ...styles];
+	result.push(...data);
+	return result;
+}
+
+function normalText(text: string): ConsoleText {
+	return styled(text, { color: 'black' });
+}
+
+function formatKind(kind: string): ConsoleText {
+	return styled(padStr(`${kind}: `, 10), { color: 'black', bold: true });
+}
+
+function styled(
+	text: string,
+	options: { color: string; strikeThrough?: boolean; bold?: boolean } = {
+		color: 'black',
+	}
+): ConsoleText {
+	function objToCss(styleObj: Record<string, string>): string {
+		return Object.entries(styleObj).reduce(
+			(styleString, [propName, propValue]) => {
+				return `${styleString}${propName}:${propValue};`;
+			},
+			''
+		);
+	}
+
+	const style: Record<string, string> = {
+		color: options.color,
+	};
+	if (options.strikeThrough) {
+		style['text-decoration'] = 'line-through';
+	}
+	if (options.bold) {
+		style['font-weight'] = 'bold';
+	}
+
+	return {
+		text,
+		style: objToCss(style),
+	};
+}
+
+function formatValue(value: unknown, availableLen: number): string {
+	switch (typeof value) {
+		case 'number':
+			return '' + value;
+		case 'string':
+			if (value.length + 2 <= availableLen) {
+				return `"${value}"`;
+			}
+			return `"${value.substr(0, availableLen - 7)}"+...`;
+
+		case 'boolean':
+			return value ? 'true' : 'false';
+		case 'undefined':
+			return 'undefined';
+		case 'object':
+			if (value === null) {
+				return 'null';
+			}
+			if (Array.isArray(value)) {
+				return formatArray(value, availableLen);
+			}
+			return formatObject(value, availableLen);
+		case 'symbol':
+			return value.toString();
+		case 'function':
+			return `[[Function${value.name ? ' ' + value.name : ''}]]`;
+		default:
+			return '' + value;
+	}
+}
+
+function formatArray(value: unknown[], availableLen: number): string {
+	let result = '[ ';
+	let first = true;
+	for (const val of value) {
+		if (!first) {
+			result += ', ';
+		}
+		if (result.length - 5 > availableLen) {
+			result += '...';
+			break;
+		}
+		first = false;
+		result += `${formatValue(val, availableLen - result.length)}`;
+	}
+	result += ' ]';
+	return result;
+}
+
+function formatObject(value: object, availableLen: number): string {
+	let result = '{ ';
+	let first = true;
+	for (const [key, val] of Object.entries(value)) {
+		if (!first) {
+			result += ', ';
+		}
+		if (result.length - 5 > availableLen) {
+			result += '...';
+			break;
+		}
+		first = false;
+		result += `${key}: ${formatValue(val, availableLen - result.length)}`;
+	}
+	result += ' }';
+	return result;
+}
+
+function repeat(str: string, count: number): string {
+	let result = '';
+	for (let i = 1; i <= count; i++) {
+		result += str;
+	}
+	return result;
+}
+
+function padStr(str: string, length: number): string {
+	while (str.length < length) {
+		str += ' ';
+	}
+	return str;
+}
diff --git a/src/vs/base/common/observableInternal/promise.ts b/src/vs/base/common/observableInternal/promise.ts
new file mode 100644
index 0000000000..80d269c16b
--- /dev/null
+++ b/src/vs/base/common/observableInternal/promise.ts
@@ -0,0 +1,209 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+import { autorun } from 'vs/base/common/observableInternal/autorun';
+import { IObservable, IReader, observableValue, transaction } from './base';
+import { Derived, derived } from 'vs/base/common/observableInternal/derived';
+import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
+import { DebugNameData, DebugOwner } from 'vs/base/common/observableInternal/debugName';
+import { strictEquals } from 'vs/base/common/equals';
+import { CancellationError } from 'vs/base/common/errors';
+
+export class ObservableLazy<T> {
+	private readonly _value = observableValue<T | undefined>(this, undefined);
+
+	/**
+	 * The cached value.
+	 * Does not force a computation of the value.
+	 */
+	public get cachedValue(): IObservable<T | undefined> { return this._value; }
+
+	constructor(private readonly _computeValue: () => T) {
+	}
+
+	/**
+	 * Returns the cached value.
+	 * Computes the value if the value has not been cached yet.
+	 */
+	public getValue() {
+		let v = this._value.get();
+		if (!v) {
+			v = this._computeValue();
+			this._value.set(v, undefined);
+		}
+		return v;
+	}
+}
+
+/**
+ * A promise whose state is observable.
+ */
+export class ObservablePromise<T> {
+	public static fromFn<T>(fn: () => Promise<T>): ObservablePromise<T> {
+		return new ObservablePromise(fn());
+	}
+
+	private readonly _value = observableValue<PromiseResult<T> | undefined>(this, undefined);
+
+	/**
+	 * The promise that this object wraps.
+	 */
+	public readonly promise: Promise<T>;
+
+	/**
+	 * The current state of the promise.
+	 * Is `undefined` if the promise didn't resolve yet.
+	 */
+	public readonly promiseResult: IObservable<PromiseResult<T> | undefined> = this._value;
+
+	constructor(promise: Promise<T>) {
+		this.promise = promise.then(value => {
+			transaction(tx => {
+				/** @description onPromiseResolved */
+				this._value.set(new PromiseResult(value, undefined), tx);
+			});
+			return value;
+		}, error => {
+			transaction(tx => {
+				/** @description onPromiseRejected */
+				this._value.set(new PromiseResult<T>(undefined, error), tx);
+			});
+			throw error;
+		});
+	}
+}
+
+export class PromiseResult<T> {
+	constructor(
+		/**
+		 * The value of the resolved promise.
+		 * Undefined if the promise rejected.
+		 */
+		public readonly data: T | undefined,
+
+		/**
+		 * The error in case of a rejected promise.
+		 * Undefined if the promise resolved.
+		 */
+		public readonly error: unknown | undefined,
+	) {
+	}
+
+	/**
+	 * Returns the value if the promise resolved, otherwise throws the error.
+	 */
+	public getDataOrThrow(): T {
+		if (this.error) {
+			throw this.error;
+		}
+		return this.data!;
+	}
+}
+
+/**
+ * A lazy promise whose state is observable.
+ */
+export class ObservableLazyPromise<T> {
+	private readonly _lazyValue = new ObservableLazy(() => new ObservablePromise(this._computePromise()));
+
+	/**
+	 * Does not enforce evaluation of the promise compute function.
+	 * Is undefined if the promise has not been computed yet.
+	 */
+	public readonly cachedPromiseResult = derived(this, reader => this._lazyValue.cachedValue.read(reader)?.promiseResult.read(reader));
+
+	constructor(private readonly _computePromise: () => Promise<T>) {
+	}
+
+	public getPromise(): Promise<T> {
+		return this._lazyValue.getValue().promise;
+	}
+}
+
+/**
+ * Resolves the promise when the observables state matches the predicate.
+ */
+export function waitForState<T>(observable: IObservable<T | null | undefined>): Promise<T>;
+export function waitForState<T, TState extends T>(observable: IObservable<T>, predicate: (state: T) => state is TState, isError?: (state: T) => boolean | unknown | undefined, cancellationToken?: CancellationToken): Promise<TState>;
+export function waitForState<T>(observable: IObservable<T>, predicate: (state: T) => boolean, isError?: (state: T) => boolean | unknown | undefined, cancellationToken?: CancellationToken): Promise<T>;
+export function waitForState<T>(observable: IObservable<T>, predicate?: (state: T) => boolean, isError?: (state: T) => boolean | unknown | undefined, cancellationToken?: CancellationToken): Promise<T> {
+	if (!predicate) {
+		predicate = state => state !== null && state !== undefined;
+	}
+	return new Promise((resolve, reject) => {
+		let isImmediateRun = true;
+		let shouldDispose = false;
+		const stateObs = observable.map(state => {
+			/** @description waitForState.state */
+			return {
+				isFinished: predicate(state),
+				error: isError ? isError(state) : false,
+				state
+			};
+		});
+		const d = autorun(reader => {
+			/** @description waitForState */
+			const { isFinished, error, state } = stateObs.read(reader);
+			if (isFinished || error) {
+				if (isImmediateRun) {
+					// The variable `d` is not initialized yet
+					shouldDispose = true;
+				} else {
+					d.dispose();
+				}
+				if (error) {
+					reject(error === true ? state : error);
+				} else {
+					resolve(state);
+				}
+			}
+		});
+		if (cancellationToken) {
+			const dc = cancellationToken.onCancellationRequested(() => {
+				d.dispose();
+				dc.dispose();
+				reject(new CancellationError());
+			});
+			if (cancellationToken.isCancellationRequested) {
+				d.dispose();
+				dc.dispose();
+				reject(new CancellationError());
+				return;
+			}
+		}
+		isImmediateRun = false;
+		if (shouldDispose) {
+			d.dispose();
+		}
+	});
+}
+
+export function derivedWithCancellationToken<T>(computeFn: (reader: IReader, cancellationToken: CancellationToken) => T): IObservable<T>;
+export function derivedWithCancellationToken<T>(owner: object, computeFn: (reader: IReader, cancellationToken: CancellationToken) => T): IObservable<T>;
+export function derivedWithCancellationToken<T>(computeFnOrOwner: ((reader: IReader, cancellationToken: CancellationToken) => T) | object, computeFnOrUndefined?: ((reader: IReader, cancellationToken: CancellationToken) => T)): IObservable<T> {
+	let computeFn: (reader: IReader, store: CancellationToken) => T;
+	let owner: DebugOwner;
+	if (computeFnOrUndefined === undefined) {
+		computeFn = computeFnOrOwner as any;
+		owner = undefined;
+	} else {
+		owner = computeFnOrOwner;
+		computeFn = computeFnOrUndefined as any;
+	}
+
+	let cancellationTokenSource: CancellationTokenSource | undefined = undefined;
+	return new Derived(
+		new DebugNameData(owner, undefined, computeFn),
+		r => {
+			if (cancellationTokenSource) {
+				cancellationTokenSource.dispose(true);
+			}
+			cancellationTokenSource = new CancellationTokenSource();
+			return computeFn(r, cancellationTokenSource.token);
+		}, undefined,
+		undefined,
+		() => cancellationTokenSource?.dispose(),
+		strictEquals,
+	);
+}
diff --git a/src/vs/base/common/observableInternal/utils.ts b/src/vs/base/common/observableInternal/utils.ts
new file mode 100644
index 0000000000..1d012de3d5
--- /dev/null
+++ b/src/vs/base/common/observableInternal/utils.ts
@@ -0,0 +1,610 @@
+/*---------------------------------------------------------------------------------------------
+ *  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 { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
+import { autorun, autorunOpts } from 'vs/base/common/observableInternal/autorun';
+import { BaseObservable, ConvenientObservable, IObservable, IObserver, IReader, ITransaction, _setKeepObserved, _setRecomputeInitiallyAndOnChange, observableValue, subtransaction, transaction } from 'vs/base/common/observableInternal/base';
+import { DebugNameData, IDebugNameData, DebugOwner, getDebugName, } from 'vs/base/common/observableInternal/debugName';
+import { derived, derivedOpts } from 'vs/base/common/observableInternal/derived';
+import { getLogger } from 'vs/base/common/observableInternal/logging';
+import { IValueWithChangeEvent } from '../event';
+import { BugIndicatingError } from 'vs/base/common/errors';
+import { EqualityComparer, strictEquals } from 'vs/base/common/equals';
+
+/**
+ * Represents an efficient observable whose value never changes.
+ */
+export function constObservable<T>(value: T): IObservable<T> {
+	return new ConstObservable(value);
+}
+
+class ConstObservable<T> extends ConvenientObservable<T, void> {
+	constructor(private readonly value: T) {
+		super();
+	}
+
+	public override get debugName(): string {
+		return this.toString();
+	}
+
+	public get(): T {
+		return this.value;
+	}
+	public addObserver(observer: IObserver): void {
+		// NO OP
+	}
+	public removeObserver(observer: IObserver): void {
+		// NO OP
+	}
+
+	override toString(): string {
+		return `Const: ${this.value}`;
+	}
+}
+
+
+export function observableFromPromise<T>(promise: Promise<T>): IObservable<{ value?: T }> {
+	const observable = observableValue<{ value?: T }>('promiseValue', {});
+	promise.then((value) => {
+		observable.set({ value }, undefined);
+	});
+	return observable;
+}
+
+
+export function observableFromEvent<T, TArgs = unknown>(
+	owner: DebugOwner,
+	event: Event<TArgs>,
+	getValue: (args: TArgs | undefined) => T,
+): IObservable<T>;
+export function observableFromEvent<T, TArgs = unknown>(
+	event: Event<TArgs>,
+	getValue: (args: TArgs | undefined) => T,
+): IObservable<T>;
+export function observableFromEvent(...args:
+	[owner: DebugOwner, event: Event<any>, getValue: (args: any | undefined) => any]
+	| [event: Event<any>, getValue: (args: any | undefined) => any]
+): IObservable<any> {
+	let owner;
+	let event;
+	let getValue;
+	if (args.length === 3) {
+		[owner, event, getValue] = args;
+	} else {
+		[event, getValue] = args;
+	}
+	return new FromEventObservable(
+		new DebugNameData(owner, undefined, getValue),
+		event,
+		getValue,
+		() => FromEventObservable.globalTransaction,
+		strictEquals
+	);
+}
+
+export function observableFromEventOpts<T, TArgs = unknown>(
+	options: IDebugNameData & {
+		equalsFn?: EqualityComparer<T>;
+	},
+	event: Event<TArgs>,
+	getValue: (args: TArgs | undefined) => T,
+): IObservable<T> {
+	return new FromEventObservable(
+		new DebugNameData(options.owner, options.debugName, options.debugReferenceFn ?? getValue),
+		event,
+		getValue, () => FromEventObservable.globalTransaction, options.equalsFn ?? strictEquals
+	);
+}
+
+export class FromEventObservable<TArgs, T> extends BaseObservable<T> {
+	public static globalTransaction: ITransaction | undefined;
+
+	private value: T | undefined;
+	private hasValue = false;
+	private subscription: IDisposable | undefined;
+
+	constructor(
+		private readonly _debugNameData: DebugNameData,
+		private readonly event: Event<TArgs>,
+		public readonly _getValue: (args: TArgs | undefined) => T,
+		private readonly _getTransaction: () => ITransaction | undefined,
+		private readonly _equalityComparator: EqualityComparer<T>
+	) {
+		super();
+	}
+
+	private getDebugName(): string | undefined {
+		return this._debugNameData.getDebugName(this);
+	}
+
+	public get debugName(): string {
+		const name = this.getDebugName();
+		return 'From Event' + (name ? `: ${name}` : '');
+	}
+
+	protected override onFirstObserverAdded(): void {
+		this.subscription = this.event(this.handleEvent);
+	}
+
+	private readonly handleEvent = (args: TArgs | undefined) => {
+		const newValue = this._getValue(args);
+		const oldValue = this.value;
+
+		const didChange = !this.hasValue || !(this._equalityComparator(oldValue!, newValue));
+		let didRunTransaction = false;
+
+		if (didChange) {
+			this.value = newValue;
+
+			if (this.hasValue) {
+				didRunTransaction = true;
+				subtransaction(
+					this._getTransaction(),
+					(tx) => {
+						getLogger()?.handleFromEventObservableTriggered(this, { oldValue, newValue, change: undefined, didChange, hadValue: this.hasValue });
+
+						for (const o of this.observers) {
+							tx.updateObserver(o, this);
+							o.handleChange(this, undefined);
+						}
+					},
+					() => {
+						const name = this.getDebugName();
+						return 'Event fired' + (name ? `: ${name}` : '');
+					}
+				);
+			}
+			this.hasValue = true;
+		}
+
+		if (!didRunTransaction) {
+			getLogger()?.handleFromEventObservableTriggered(this, { oldValue, newValue, change: undefined, didChange, hadValue: this.hasValue });
+		}
+	};
+
+	protected override onLastObserverRemoved(): void {
+		this.subscription!.dispose();
+		this.subscription = undefined;
+		this.hasValue = false;
+		this.value = undefined;
+	}
+
+	public get(): T {
+		if (this.subscription) {
+			if (!this.hasValue) {
+				this.handleEvent(undefined);
+			}
+			return this.value!;
+		} else {
+			// no cache, as there are no subscribers to keep it updated
+			const value = this._getValue(undefined);
+			return value;
+		}
+	}
+}
+
+export namespace observableFromEvent {
+	export const Observer = FromEventObservable;
+
+	export function batchEventsGlobally(tx: ITransaction, fn: () => void): void {
+		let didSet = false;
+		if (FromEventObservable.globalTransaction === undefined) {
+			FromEventObservable.globalTransaction = tx;
+			didSet = true;
+		}
+		try {
+			fn();
+		} finally {
+			if (didSet) {
+				FromEventObservable.globalTransaction = undefined;
+			}
+		}
+	}
+}
+
+export function observableSignalFromEvent(
+	debugName: string,
+	event: Event<any>
+): IObservable<void> {
+	return new FromEventObservableSignal(debugName, event);
+}
+
+class FromEventObservableSignal extends BaseObservable<void> {
+	private subscription: IDisposable | undefined;
+
+	constructor(
+		public readonly debugName: string,
+		private readonly event: Event<any>,
+	) {
+		super();
+	}
+
+	protected override onFirstObserverAdded(): void {
+		this.subscription = this.event(this.handleEvent);
+	}
+
+	private readonly handleEvent = () => {
+		transaction(
+			(tx) => {
+				for (const o of this.observers) {
+					tx.updateObserver(o, this);
+					o.handleChange(this, undefined);
+				}
+			},
+			() => this.debugName
+		);
+	};
+
+	protected override onLastObserverRemoved(): void {
+		this.subscription!.dispose();
+		this.subscription = undefined;
+	}
+
+	public override get(): void {
+		// NO OP
+	}
+}
+
+/**
+ * Creates a signal that can be triggered to invalidate observers.
+ * Signals don't have a value - when they are triggered they indicate a change.
+ * However, signals can carry a delta that is passed to observers.
+ */
+export function observableSignal<TDelta = void>(debugName: string): IObservableSignal<TDelta>;
+export function observableSignal<TDelta = void>(owner: object): IObservableSignal<TDelta>;
+export function observableSignal<TDelta = void>(debugNameOrOwner: string | object): IObservableSignal<TDelta> {
+	if (typeof debugNameOrOwner === 'string') {
+		return new ObservableSignal<TDelta>(debugNameOrOwner);
+	} else {
+		return new ObservableSignal<TDelta>(undefined, debugNameOrOwner);
+	}
+}
+
+export interface IObservableSignal<TChange> extends IObservable<void, TChange> {
+	trigger(tx: ITransaction | undefined, change: TChange): void;
+}
+
+class ObservableSignal<TChange> extends BaseObservable<void, TChange> implements IObservableSignal<TChange> {
+	public get debugName() {
+		return new DebugNameData(this._owner, this._debugName, undefined).getDebugName(this) ?? 'Observable Signal';
+	}
+
+	public override toString(): string {
+		return this.debugName;
+	}
+
+	constructor(
+		private readonly _debugName: string | undefined,
+		private readonly _owner?: object,
+	) {
+		super();
+	}
+
+	public trigger(tx: ITransaction | undefined, change: TChange): void {
+		if (!tx) {
+			transaction(tx => {
+				this.trigger(tx, change);
+			}, () => `Trigger signal ${this.debugName}`);
+			return;
+		}
+
+		for (const o of this.observers) {
+			tx.updateObserver(o, this);
+			o.handleChange(this, change);
+		}
+	}
+
+	public override get(): void {
+		// NO OP
+	}
+}
+
+/**
+ * @deprecated Use `debouncedObservable2` instead.
+ */
+export function debouncedObservable<T>(observable: IObservable<T>, debounceMs: number, disposableStore: DisposableStore): IObservable<T | undefined> {
+	const debouncedObservable = observableValue<T | undefined>('debounced', undefined);
+
+	let timeout: any = undefined;
+
+	disposableStore.add(autorun(reader => {
+		/** @description debounce */
+		const value = observable.read(reader);
+
+		if (timeout) {
+			clearTimeout(timeout);
+		}
+		timeout = setTimeout(() => {
+			transaction(tx => {
+				debouncedObservable.set(value, tx);
+			});
+		}, debounceMs);
+
+	}));
+
+	return debouncedObservable;
+}
+
+/**
+ * Creates an observable that debounces the input observable.
+ */
+export function debouncedObservable2<T>(observable: IObservable<T>, debounceMs: number): IObservable<T> {
+	let hasValue = false;
+	let lastValue: T | undefined;
+
+	let timeout: any = undefined;
+
+	return observableFromEvent<T, void>(cb => {
+		const d = autorun(reader => {
+			const value = observable.read(reader);
+
+			if (!hasValue) {
+				hasValue = true;
+				lastValue = value;
+			} else {
+				if (timeout) {
+					clearTimeout(timeout);
+				}
+				timeout = setTimeout(() => {
+					lastValue = value;
+					cb();
+				}, debounceMs);
+			}
+		});
+		return {
+			dispose() {
+				d.dispose();
+				hasValue = false;
+				lastValue = undefined;
+			},
+		};
+	}, () => {
+		if (hasValue) {
+			return lastValue!;
+		} else {
+			return observable.get();
+		}
+	});
+}
+
+export function wasEventTriggeredRecently(event: Event<any>, timeoutMs: number, disposableStore: DisposableStore): IObservable<boolean> {
+	const observable = observableValue('triggeredRecently', false);
+
+	let timeout: any = undefined;
+
+	disposableStore.add(event(() => {
+		observable.set(true, undefined);
+
+		if (timeout) {
+			clearTimeout(timeout);
+		}
+		timeout = setTimeout(() => {
+			observable.set(false, undefined);
+		}, timeoutMs);
+	}));
+
+	return observable;
+}
+
+/**
+ * This makes sure the observable is being observed and keeps its cache alive.
+ */
+export function keepObserved<T>(observable: IObservable<T>): IDisposable {
+	const o = new KeepAliveObserver(false, undefined);
+	observable.addObserver(o);
+	return toDisposable(() => {
+		observable.removeObserver(o);
+	});
+}
+
+_setKeepObserved(keepObserved);
+
+/**
+ * This converts the given observable into an autorun.
+ */
+export function recomputeInitiallyAndOnChange<T>(observable: IObservable<T>, handleValue?: (value: T) => void): IDisposable {
+	const o = new KeepAliveObserver(true, handleValue);
+	observable.addObserver(o);
+	if (handleValue) {
+		handleValue(observable.get());
+	} else {
+		observable.reportChanges();
+	}
+
+	return toDisposable(() => {
+		observable.removeObserver(o);
+	});
+}
+
+_setRecomputeInitiallyAndOnChange(recomputeInitiallyAndOnChange);
+
+export class KeepAliveObserver implements IObserver {
+	private _counter = 0;
+
+	constructor(
+		private readonly _forceRecompute: boolean,
+		private readonly _handleValue: ((value: any) => void) | undefined,
+	) { }
+
+	beginUpdate<T>(observable: IObservable<T, void>): void {
+		this._counter++;
+	}
+
+	endUpdate<T>(observable: IObservable<T, void>): void {
+		this._counter--;
+		if (this._counter === 0 && this._forceRecompute) {
+			if (this._handleValue) {
+				this._handleValue(observable.get());
+			} else {
+				observable.reportChanges();
+			}
+		}
+	}
+
+	handlePossibleChange<T>(observable: IObservable<T, unknown>): void {
+		// NO OP
+	}
+
+	handleChange<T, TChange>(observable: IObservable<T, TChange>, change: TChange): void {
+		// NO OP
+	}
+}
+
+export function derivedObservableWithCache<T>(owner: DebugOwner, computeFn: (reader: IReader, lastValue: T | undefined) => T): IObservable<T> {
+	let lastValue: T | undefined = undefined;
+	const observable = derivedOpts({ owner, debugReferenceFn: computeFn }, reader => {
+		lastValue = computeFn(reader, lastValue);
+		return lastValue;
+	});
+	return observable;
+}
+
+export function derivedObservableWithWritableCache<T>(owner: object, computeFn: (reader: IReader, lastValue: T | undefined) => T): IObservable<T>
+	& { clearCache(transaction: ITransaction): void; setCache(newValue: T | undefined, tx: ITransaction | undefined): void } {
+	let lastValue: T | undefined = undefined;
+	const onChange = observableSignal('derivedObservableWithWritableCache');
+	const observable = derived(owner, reader => {
+		onChange.read(reader);
+		lastValue = computeFn(reader, lastValue);
+		return lastValue;
+	});
+	return Object.assign(observable, {
+		clearCache: (tx: ITransaction) => {
+			lastValue = undefined;
+			onChange.trigger(tx);
+		},
+		setCache: (newValue: T | undefined, tx: ITransaction | undefined) => {
+			lastValue = newValue;
+			onChange.trigger(tx);
+		}
+	});
+}
+
+/**
+ * When the items array changes, referential equal items are not mapped again.
+ */
+export function mapObservableArrayCached<TIn, TOut, TKey = TIn>(owner: DebugOwner, items: IObservable<readonly TIn[]>, map: (input: TIn, store: DisposableStore) => TOut, keySelector?: (input: TIn) => TKey): IObservable<readonly TOut[]> {
+	let m = new ArrayMap(map, keySelector);
+	const self = derivedOpts({
+		debugReferenceFn: map,
+		owner,
+		onLastObserverRemoved: () => {
+			m.dispose();
+			m = new ArrayMap(map);
+		}
+	}, (reader) => {
+		m.setItems(items.read(reader));
+		return m.getItems();
+	});
+	return self;
+}
+
+class ArrayMap<TIn, TOut, TKey> implements IDisposable {
+	private readonly _cache = new Map<TKey, { out: TOut; store: DisposableStore }>();
+	private _items: TOut[] = [];
+	constructor(
+		private readonly _map: (input: TIn, store: DisposableStore) => TOut,
+		private readonly _keySelector?: (input: TIn) => TKey,
+	) {
+	}
+
+	public dispose(): void {
+		this._cache.forEach(entry => entry.store.dispose());
+		this._cache.clear();
+	}
+
+	public setItems(items: readonly TIn[]): void {
+		const newItems: TOut[] = [];
+		const itemsToRemove = new Set(this._cache.keys());
+
+		for (const item of items) {
+			const key = this._keySelector ? this._keySelector(item) : item as unknown as TKey;
+
+			let entry = this._cache.get(key);
+			if (!entry) {
+				const store = new DisposableStore();
+				const out = this._map(item, store);
+				entry = { out, store };
+				this._cache.set(key, entry);
+			} else {
+				itemsToRemove.delete(key);
+			}
+			newItems.push(entry.out);
+		}
+
+		for (const item of itemsToRemove) {
+			const entry = this._cache.get(item)!;
+			entry.store.dispose();
+			this._cache.delete(item);
+		}
+
+		this._items = newItems;
+	}
+
+	public getItems(): TOut[] {
+		return this._items;
+	}
+}
+
+export class ValueWithChangeEventFromObservable<T> implements IValueWithChangeEvent<T> {
+	constructor(public readonly observable: IObservable<T>) {
+	}
+
+	get onDidChange(): Event<void> {
+		return Event.fromObservableLight(this.observable);
+	}
+
+	get value(): T {
+		return this.observable.get();
+	}
+}
+
+export function observableFromValueWithChangeEvent<T>(owner: DebugOwner, value: IValueWithChangeEvent<T>): IObservable<T> {
+	if (value instanceof ValueWithChangeEventFromObservable) {
+		return value.observable;
+	}
+	return observableFromEvent(owner, value.onDidChange, () => value.value);
+}
+
+/**
+ * Creates an observable that has the latest changed value of the given observables.
+ * Initially (and when not observed), it has the value of the last observable.
+ * When observed and any of the observables change, it has the value of the last changed observable.
+ * If multiple observables change in the same transaction, the last observable wins.
+*/
+export function latestChangedValue<T extends IObservable<any>[]>(owner: DebugOwner, observables: T): IObservable<ReturnType<T[number]['get']>> {
+	if (observables.length === 0) {
+		throw new BugIndicatingError();
+	}
+
+	let hasLastChangedValue = false;
+	let lastChangedValue: any = undefined;
+
+	const result = observableFromEvent<any, void>(owner, cb => {
+		const store = new DisposableStore();
+		for (const o of observables) {
+			store.add(autorunOpts({ debugName: () => getDebugName(result, new DebugNameData(owner, undefined, undefined)) + '.updateLastChangedValue' }, reader => {
+				hasLastChangedValue = true;
+				lastChangedValue = o.read(reader);
+				cb();
+			}));
+		}
+		store.add({
+			dispose() {
+				hasLastChangedValue = false;
+				lastChangedValue = undefined;
+			},
+		});
+		return store;
+	}, () => {
+		if (hasLastChangedValue) {
+			return lastChangedValue;
+		} else {
+			return observables[observables.length - 1].get();
+		}
+	});
+	return result;
+}
diff --git a/src/vs/base/common/platform.ts b/src/vs/base/common/platform.ts
new file mode 100644
index 0000000000..d931e64dfa
--- /dev/null
+++ b/src/vs/base/common/platform.ts
@@ -0,0 +1,281 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import * as nls from 'vs/nls';
+
+export const LANGUAGE_DEFAULT = 'en';
+
+let _isWindows = false;
+let _isMacintosh = false;
+let _isLinux = false;
+let _isLinuxSnap = false;
+let _isNative = false;
+let _isWeb = false;
+let _isElectron = false;
+let _isIOS = false;
+let _isCI = false;
+let _isMobile = false;
+let _locale: string | undefined = undefined;
+let _language: string = LANGUAGE_DEFAULT;
+let _platformLocale: string = LANGUAGE_DEFAULT;
+let _translationsConfigFile: string | undefined = undefined;
+let _userAgent: string | undefined = undefined;
+
+export interface IProcessEnvironment {
+	[key: string]: string | undefined;
+}
+
+/**
+ * This interface is intentionally not identical to node.js
+ * process because it also works in sandboxed environments
+ * where the process object is implemented differently. We
+ * define the properties here that we need for `platform`
+ * to work and nothing else.
+ */
+export interface INodeProcess {
+	platform: string;
+	arch: string;
+	env: IProcessEnvironment;
+	versions?: {
+		node?: string;
+		electron?: string;
+		chrome?: string;
+	};
+	type?: string;
+	cwd: () => string;
+}
+
+declare const process: INodeProcess;
+
+const $globalThis: any = globalThis;
+
+let nodeProcess: INodeProcess | undefined = undefined;
+if (typeof $globalThis.vscode !== 'undefined' && typeof $globalThis.vscode.process !== 'undefined') {
+	// Native environment (sandboxed)
+	nodeProcess = $globalThis.vscode.process;
+} else if (typeof process !== 'undefined' && typeof process?.versions?.node === 'string') {
+	// Native environment (non-sandboxed)
+	nodeProcess = process;
+}
+
+const isElectronProcess = typeof nodeProcess?.versions?.electron === 'string';
+const isElectronRenderer = isElectronProcess && nodeProcess?.type === 'renderer';
+
+interface INavigator {
+	userAgent: string;
+	maxTouchPoints?: number;
+	language: string;
+}
+declare const navigator: INavigator;
+
+// Native environment
+if (typeof nodeProcess === 'object') {
+	_isWindows = (nodeProcess.platform === 'win32');
+	_isMacintosh = (nodeProcess.platform === 'darwin');
+	_isLinux = (nodeProcess.platform === 'linux');
+	_isLinuxSnap = _isLinux && !!nodeProcess.env['SNAP'] && !!nodeProcess.env['SNAP_REVISION'];
+	_isElectron = isElectronProcess;
+	_isCI = !!nodeProcess.env['CI'] || !!nodeProcess.env['BUILD_ARTIFACTSTAGINGDIRECTORY'];
+	_locale = LANGUAGE_DEFAULT;
+	_language = LANGUAGE_DEFAULT;
+	const rawNlsConfig = nodeProcess.env['VSCODE_NLS_CONFIG'];
+	if (rawNlsConfig) {
+		try {
+			const nlsConfig: nls.INLSConfiguration = JSON.parse(rawNlsConfig);
+			_locale = nlsConfig.userLocale;
+			_platformLocale = nlsConfig.osLocale;
+			_language = nlsConfig.resolvedLanguage || LANGUAGE_DEFAULT;
+			_translationsConfigFile = nlsConfig.languagePack?.translationsConfigFile;
+		} catch (e) {
+		}
+	}
+	_isNative = true;
+}
+
+// Web environment
+else if (typeof navigator === 'object' && !isElectronRenderer) {
+	_userAgent = navigator.userAgent;
+	_isWindows = _userAgent.indexOf('Windows') >= 0;
+	_isMacintosh = _userAgent.indexOf('Macintosh') >= 0;
+	_isIOS = (_userAgent.indexOf('Macintosh') >= 0 || _userAgent.indexOf('iPad') >= 0 || _userAgent.indexOf('iPhone') >= 0) && !!navigator.maxTouchPoints && navigator.maxTouchPoints > 0;
+	_isLinux = _userAgent.indexOf('Linux') >= 0;
+	_isMobile = _userAgent?.indexOf('Mobi') >= 0;
+	_isWeb = true;
+	// VSCODE_GLOBALS: NLS
+	_language = globalThis._VSCODE_NLS_LANGUAGE || LANGUAGE_DEFAULT;
+	_locale = navigator.language.toLowerCase();
+	_platformLocale = _locale;
+}
+
+// Unknown environment
+else {
+	console.error('Unable to resolve platform.');
+}
+
+export const enum Platform {
+	Web,
+	Mac,
+	Linux,
+	Windows
+}
+export type PlatformName = 'Web' | 'Windows' | 'Mac' | 'Linux';
+
+export function PlatformToString(platform: Platform): PlatformName {
+	switch (platform) {
+		case Platform.Web: return 'Web';
+		case Platform.Mac: return 'Mac';
+		case Platform.Linux: return 'Linux';
+		case Platform.Windows: return 'Windows';
+	}
+}
+
+let _platform: Platform = Platform.Web;
+if (_isMacintosh) {
+	_platform = Platform.Mac;
+} else if (_isWindows) {
+	_platform = Platform.Windows;
+} else if (_isLinux) {
+	_platform = Platform.Linux;
+}
+
+export const isWindows = _isWindows;
+export const isMacintosh = _isMacintosh;
+export const isLinux = _isLinux;
+export const isLinuxSnap = _isLinuxSnap;
+export const isNative = _isNative;
+export const isElectron = _isElectron;
+export const isWeb = _isWeb;
+export const isWebWorker = (_isWeb && typeof $globalThis.importScripts === 'function');
+export const webWorkerOrigin = isWebWorker ? $globalThis.origin : undefined;
+export const isIOS = _isIOS;
+export const isMobile = _isMobile;
+/**
+ * Whether we run inside a CI environment, such as
+ * GH actions or Azure Pipelines.
+ */
+export const isCI = _isCI;
+export const platform = _platform;
+export const userAgent = _userAgent;
+
+/**
+ * The language used for the user interface. The format of
+ * the string is all lower case (e.g. zh-tw for Traditional
+ * Chinese or de for German)
+ */
+export const language = _language;
+
+export namespace Language {
+
+	export function value(): string {
+		return language;
+	}
+
+	export function isDefaultVariant(): boolean {
+		if (language.length === 2) {
+			return language === 'en';
+		} else if (language.length >= 3) {
+			return language[0] === 'e' && language[1] === 'n' && language[2] === '-';
+		} else {
+			return false;
+		}
+	}
+
+	export function isDefault(): boolean {
+		return language === 'en';
+	}
+}
+
+/**
+ * Desktop: The OS locale or the locale specified by --locale or `argv.json`.
+ * Web: matches `platformLocale`.
+ *
+ * The UI is not necessarily shown in the provided locale.
+ */
+export const locale = _locale;
+
+/**
+ * This will always be set to the OS/browser's locale regardless of
+ * what was specified otherwise. The format of the string is all
+ * lower case (e.g. zh-tw for Traditional Chinese). The UI is not
+ * necessarily shown in the provided locale.
+ */
+export const platformLocale = _platformLocale;
+
+/**
+ * The translations that are available through language packs.
+ */
+export const translationsConfigFile = _translationsConfigFile;
+
+export const setTimeout0IsFaster = (typeof $globalThis.postMessage === 'function' && !$globalThis.importScripts);
+
+/**
+ * See https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#:~:text=than%204%2C%20then-,set%20timeout%20to%204,-.
+ *
+ * Works similarly to `setTimeout(0)` but doesn't suffer from the 4ms artificial delay
+ * that browsers set when the nesting level is > 5.
+ */
+export const setTimeout0 = (() => {
+	if (setTimeout0IsFaster) {
+		interface IQueueElement {
+			id: number;
+			callback: () => void;
+		}
+		const pending: IQueueElement[] = [];
+
+		$globalThis.addEventListener('message', (e: any) => {
+			if (e.data && e.data.vscodeScheduleAsyncWork) {
+				for (let i = 0, len = pending.length; i < len; i++) {
+					const candidate = pending[i];
+					if (candidate.id === e.data.vscodeScheduleAsyncWork) {
+						pending.splice(i, 1);
+						candidate.callback();
+						return;
+					}
+				}
+			}
+		});
+		let lastId = 0;
+		return (callback: () => void) => {
+			const myId = ++lastId;
+			pending.push({
+				id: myId,
+				callback: callback
+			});
+			$globalThis.postMessage({ vscodeScheduleAsyncWork: myId }, '*');
+		};
+	}
+	return (callback: () => void) => setTimeout(callback);
+})();
+
+export const enum OperatingSystem {
+	Windows = 1,
+	Macintosh = 2,
+	Linux = 3
+}
+export const OS = (_isMacintosh || _isIOS ? OperatingSystem.Macintosh : (_isWindows ? OperatingSystem.Windows : OperatingSystem.Linux));
+
+let _isLittleEndian = true;
+let _isLittleEndianComputed = false;
+export function isLittleEndian(): boolean {
+	if (!_isLittleEndianComputed) {
+		_isLittleEndianComputed = true;
+		const test = new Uint8Array(2);
+		test[0] = 1;
+		test[1] = 2;
+		const view = new Uint16Array(test.buffer);
+		_isLittleEndian = (view[0] === (2 << 8) + 1);
+	}
+	return _isLittleEndian;
+}
+
+export const isChrome = !!(userAgent && userAgent.indexOf('Chrome') >= 0);
+export const isFirefox = !!(userAgent && userAgent.indexOf('Firefox') >= 0);
+export const isSafari = !!(!isChrome && (userAgent && userAgent.indexOf('Safari') >= 0));
+export const isEdge = !!(userAgent && userAgent.indexOf('Edg/') >= 0);
+export const isAndroid = !!(userAgent && userAgent.indexOf('Android') >= 0);
+
+export function isBigSurOrNewer(osVersion: string): boolean {
+	return parseFloat(osVersion) >= 20;
+}
diff --git a/src/vs/base/common/scrollable.ts b/src/vs/base/common/scrollable.ts
new file mode 100644
index 0000000000..4d1360c586
--- /dev/null
+++ b/src/vs/base/common/scrollable.ts
@@ -0,0 +1,522 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { Emitter, Event } from 'vs/base/common/event';
+import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
+
+export const enum ScrollbarVisibility {
+	Auto = 1,
+	Hidden = 2,
+	Visible = 3
+}
+
+export interface ScrollEvent {
+	inSmoothScrolling: boolean;
+
+	oldWidth: number;
+	oldScrollWidth: number;
+	oldScrollLeft: number;
+
+	width: number;
+	scrollWidth: number;
+	scrollLeft: number;
+
+	oldHeight: number;
+	oldScrollHeight: number;
+	oldScrollTop: number;
+
+	height: number;
+	scrollHeight: number;
+	scrollTop: number;
+
+	widthChanged: boolean;
+	scrollWidthChanged: boolean;
+	scrollLeftChanged: boolean;
+
+	heightChanged: boolean;
+	scrollHeightChanged: boolean;
+	scrollTopChanged: boolean;
+}
+
+export class ScrollState implements IScrollDimensions, IScrollPosition {
+	_scrollStateBrand: void = undefined;
+
+	public readonly rawScrollLeft: number;
+	public readonly rawScrollTop: number;
+
+	public readonly width: number;
+	public readonly scrollWidth: number;
+	public readonly scrollLeft: number;
+	public readonly height: number;
+	public readonly scrollHeight: number;
+	public readonly scrollTop: number;
+
+	constructor(
+		private readonly _forceIntegerValues: boolean,
+		width: number,
+		scrollWidth: number,
+		scrollLeft: number,
+		height: number,
+		scrollHeight: number,
+		scrollTop: number
+	) {
+		if (this._forceIntegerValues) {
+			width = width | 0;
+			scrollWidth = scrollWidth | 0;
+			scrollLeft = scrollLeft | 0;
+			height = height | 0;
+			scrollHeight = scrollHeight | 0;
+			scrollTop = scrollTop | 0;
+		}
+
+		this.rawScrollLeft = scrollLeft; // before validation
+		this.rawScrollTop = scrollTop; // before validation
+
+		if (width < 0) {
+			width = 0;
+		}
+		if (scrollLeft + width > scrollWidth) {
+			scrollLeft = scrollWidth - width;
+		}
+		if (scrollLeft < 0) {
+			scrollLeft = 0;
+		}
+
+		if (height < 0) {
+			height = 0;
+		}
+		if (scrollTop + height > scrollHeight) {
+			scrollTop = scrollHeight - height;
+		}
+		if (scrollTop < 0) {
+			scrollTop = 0;
+		}
+
+		this.width = width;
+		this.scrollWidth = scrollWidth;
+		this.scrollLeft = scrollLeft;
+		this.height = height;
+		this.scrollHeight = scrollHeight;
+		this.scrollTop = scrollTop;
+	}
+
+	public equals(other: ScrollState): boolean {
+		return (
+			this.rawScrollLeft === other.rawScrollLeft
+			&& this.rawScrollTop === other.rawScrollTop
+			&& this.width === other.width
+			&& this.scrollWidth === other.scrollWidth
+			&& this.scrollLeft === other.scrollLeft
+			&& this.height === other.height
+			&& this.scrollHeight === other.scrollHeight
+			&& this.scrollTop === other.scrollTop
+		);
+	}
+
+	public withScrollDimensions(update: INewScrollDimensions, useRawScrollPositions: boolean): ScrollState {
+		return new ScrollState(
+			this._forceIntegerValues,
+			(typeof update.width !== 'undefined' ? update.width : this.width),
+			(typeof update.scrollWidth !== 'undefined' ? update.scrollWidth : this.scrollWidth),
+			useRawScrollPositions ? this.rawScrollLeft : this.scrollLeft,
+			(typeof update.height !== 'undefined' ? update.height : this.height),
+			(typeof update.scrollHeight !== 'undefined' ? update.scrollHeight : this.scrollHeight),
+			useRawScrollPositions ? this.rawScrollTop : this.scrollTop
+		);
+	}
+
+	public withScrollPosition(update: INewScrollPosition): ScrollState {
+		return new ScrollState(
+			this._forceIntegerValues,
+			this.width,
+			this.scrollWidth,
+			(typeof update.scrollLeft !== 'undefined' ? update.scrollLeft : this.rawScrollLeft),
+			this.height,
+			this.scrollHeight,
+			(typeof update.scrollTop !== 'undefined' ? update.scrollTop : this.rawScrollTop)
+		);
+	}
+
+	public createScrollEvent(previous: ScrollState, inSmoothScrolling: boolean): ScrollEvent {
+		const widthChanged = (this.width !== previous.width);
+		const scrollWidthChanged = (this.scrollWidth !== previous.scrollWidth);
+		const scrollLeftChanged = (this.scrollLeft !== previous.scrollLeft);
+
+		const heightChanged = (this.height !== previous.height);
+		const scrollHeightChanged = (this.scrollHeight !== previous.scrollHeight);
+		const scrollTopChanged = (this.scrollTop !== previous.scrollTop);
+
+		return {
+			inSmoothScrolling: inSmoothScrolling,
+			oldWidth: previous.width,
+			oldScrollWidth: previous.scrollWidth,
+			oldScrollLeft: previous.scrollLeft,
+
+			width: this.width,
+			scrollWidth: this.scrollWidth,
+			scrollLeft: this.scrollLeft,
+
+			oldHeight: previous.height,
+			oldScrollHeight: previous.scrollHeight,
+			oldScrollTop: previous.scrollTop,
+
+			height: this.height,
+			scrollHeight: this.scrollHeight,
+			scrollTop: this.scrollTop,
+
+			widthChanged: widthChanged,
+			scrollWidthChanged: scrollWidthChanged,
+			scrollLeftChanged: scrollLeftChanged,
+
+			heightChanged: heightChanged,
+			scrollHeightChanged: scrollHeightChanged,
+			scrollTopChanged: scrollTopChanged,
+		};
+	}
+
+}
+
+export interface IScrollDimensions {
+	readonly width: number;
+	readonly scrollWidth: number;
+	readonly height: number;
+	readonly scrollHeight: number;
+}
+export interface INewScrollDimensions {
+	width?: number;
+	scrollWidth?: number;
+	height?: number;
+	scrollHeight?: number;
+}
+
+export interface IScrollPosition {
+	readonly scrollLeft: number;
+	readonly scrollTop: number;
+}
+export interface ISmoothScrollPosition {
+	readonly scrollLeft: number;
+	readonly scrollTop: number;
+
+	readonly width: number;
+	readonly height: number;
+}
+export interface INewScrollPosition {
+	scrollLeft?: number;
+	scrollTop?: number;
+}
+
+export interface IScrollableOptions {
+	/**
+	 * Define if the scroll values should always be integers.
+	 */
+	forceIntegerValues: boolean;
+	/**
+	 * Set the duration (ms) used for smooth scroll animations.
+	 */
+	smoothScrollDuration: number;
+	/**
+	 * A function to schedule an update at the next frame (used for smooth scroll animations).
+	 */
+	scheduleAtNextAnimationFrame: (callback: () => void) => IDisposable;
+}
+
+export class Scrollable extends Disposable {
+
+	_scrollableBrand: void = undefined;
+
+	private _smoothScrollDuration: number;
+	private readonly _scheduleAtNextAnimationFrame: (callback: () => void) => IDisposable;
+	private _state: ScrollState;
+	private _smoothScrolling: SmoothScrollingOperation | null;
+
+	private _onScroll = this._register(new Emitter<ScrollEvent>());
+	public readonly onScroll: Event<ScrollEvent> = this._onScroll.event;
+
+	constructor(options: IScrollableOptions) {
+		super();
+
+		this._smoothScrollDuration = options.smoothScrollDuration;
+		this._scheduleAtNextAnimationFrame = options.scheduleAtNextAnimationFrame;
+		this._state = new ScrollState(options.forceIntegerValues, 0, 0, 0, 0, 0, 0);
+		this._smoothScrolling = null;
+	}
+
+	public override dispose(): void {
+		if (this._smoothScrolling) {
+			this._smoothScrolling.dispose();
+			this._smoothScrolling = null;
+		}
+		super.dispose();
+	}
+
+	public setSmoothScrollDuration(smoothScrollDuration: number): void {
+		this._smoothScrollDuration = smoothScrollDuration;
+	}
+
+	public validateScrollPosition(scrollPosition: INewScrollPosition): IScrollPosition {
+		return this._state.withScrollPosition(scrollPosition);
+	}
+
+	public getScrollDimensions(): IScrollDimensions {
+		return this._state;
+	}
+
+	public setScrollDimensions(dimensions: INewScrollDimensions, useRawScrollPositions: boolean): void {
+		const newState = this._state.withScrollDimensions(dimensions, useRawScrollPositions);
+		this._setState(newState, Boolean(this._smoothScrolling));
+
+		// Validate outstanding animated scroll position target
+		this._smoothScrolling?.acceptScrollDimensions(this._state);
+	}
+
+	/**
+	 * Returns the final scroll position that the instance will have once the smooth scroll animation concludes.
+	 * If no scroll animation is occurring, it will return the current scroll position instead.
+	 */
+	public getFutureScrollPosition(): IScrollPosition {
+		if (this._smoothScrolling) {
+			return this._smoothScrolling.to;
+		}
+		return this._state;
+	}
+
+	/**
+	 * Returns the current scroll position.
+	 * Note: This result might be an intermediate scroll position, as there might be an ongoing smooth scroll animation.
+	 */
+	public getCurrentScrollPosition(): IScrollPosition {
+		return this._state;
+	}
+
+	public setScrollPositionNow(update: INewScrollPosition): void {
+		// no smooth scrolling requested
+		const newState = this._state.withScrollPosition(update);
+
+		// Terminate any outstanding smooth scrolling
+		if (this._smoothScrolling) {
+			this._smoothScrolling.dispose();
+			this._smoothScrolling = null;
+		}
+
+		this._setState(newState, false);
+	}
+
+	public setScrollPositionSmooth(update: INewScrollPosition, reuseAnimation?: boolean): void {
+		if (this._smoothScrollDuration === 0) {
+			// Smooth scrolling not supported.
+			return this.setScrollPositionNow(update);
+		}
+
+		if (this._smoothScrolling) {
+			// Combine our pending scrollLeft/scrollTop with incoming scrollLeft/scrollTop
+			update = {
+				scrollLeft: (typeof update.scrollLeft === 'undefined' ? this._smoothScrolling.to.scrollLeft : update.scrollLeft),
+				scrollTop: (typeof update.scrollTop === 'undefined' ? this._smoothScrolling.to.scrollTop : update.scrollTop)
+			};
+
+			// Validate `update`
+			const validTarget = this._state.withScrollPosition(update);
+
+			if (this._smoothScrolling.to.scrollLeft === validTarget.scrollLeft && this._smoothScrolling.to.scrollTop === validTarget.scrollTop) {
+				// No need to interrupt or extend the current animation since we're going to the same place
+				return;
+			}
+			let newSmoothScrolling: SmoothScrollingOperation;
+			if (reuseAnimation) {
+				newSmoothScrolling = new SmoothScrollingOperation(this._smoothScrolling.from, validTarget, this._smoothScrolling.startTime, this._smoothScrolling.duration);
+			} else {
+				newSmoothScrolling = this._smoothScrolling.combine(this._state, validTarget, this._smoothScrollDuration);
+			}
+			this._smoothScrolling.dispose();
+			this._smoothScrolling = newSmoothScrolling;
+		} else {
+			// Validate `update`
+			const validTarget = this._state.withScrollPosition(update);
+
+			this._smoothScrolling = SmoothScrollingOperation.start(this._state, validTarget, this._smoothScrollDuration);
+		}
+
+		// Begin smooth scrolling animation
+		this._smoothScrolling.animationFrameDisposable = this._scheduleAtNextAnimationFrame(() => {
+			if (!this._smoothScrolling) {
+				return;
+			}
+			this._smoothScrolling.animationFrameDisposable = null;
+			this._performSmoothScrolling();
+		});
+	}
+
+	public hasPendingScrollAnimation(): boolean {
+		return Boolean(this._smoothScrolling);
+	}
+
+	private _performSmoothScrolling(): void {
+		if (!this._smoothScrolling) {
+			return;
+		}
+		const update = this._smoothScrolling.tick();
+		const newState = this._state.withScrollPosition(update);
+
+		this._setState(newState, true);
+
+		if (!this._smoothScrolling) {
+			// Looks like someone canceled the smooth scrolling
+			// from the scroll event handler
+			return;
+		}
+
+		if (update.isDone) {
+			this._smoothScrolling.dispose();
+			this._smoothScrolling = null;
+			return;
+		}
+
+		// Continue smooth scrolling animation
+		this._smoothScrolling.animationFrameDisposable = this._scheduleAtNextAnimationFrame(() => {
+			if (!this._smoothScrolling) {
+				return;
+			}
+			this._smoothScrolling.animationFrameDisposable = null;
+			this._performSmoothScrolling();
+		});
+	}
+
+	private _setState(newState: ScrollState, inSmoothScrolling: boolean): void {
+		const oldState = this._state;
+		if (oldState.equals(newState)) {
+			// no change
+			return;
+		}
+		this._state = newState;
+		this._onScroll.fire(this._state.createScrollEvent(oldState, inSmoothScrolling));
+	}
+}
+
+export class SmoothScrollingUpdate {
+
+	public readonly scrollLeft: number;
+	public readonly scrollTop: number;
+	public readonly isDone: boolean;
+
+	constructor(scrollLeft: number, scrollTop: number, isDone: boolean) {
+		this.scrollLeft = scrollLeft;
+		this.scrollTop = scrollTop;
+		this.isDone = isDone;
+	}
+
+}
+
+interface IAnimation {
+	(completion: number): number;
+}
+
+function createEaseOutCubic(from: number, to: number): IAnimation {
+	const delta = to - from;
+	return function (completion: number): number {
+		return from + delta * easeOutCubic(completion);
+	};
+}
+
+function createComposed(a: IAnimation, b: IAnimation, cut: number): IAnimation {
+	return function (completion: number): number {
+		if (completion < cut) {
+			return a(completion / cut);
+		}
+		return b((completion - cut) / (1 - cut));
+	};
+}
+
+export class SmoothScrollingOperation {
+
+	public readonly from: ISmoothScrollPosition;
+	public to: ISmoothScrollPosition;
+	public readonly duration: number;
+	public readonly startTime: number;
+	public animationFrameDisposable: IDisposable | null;
+
+	private scrollLeft!: IAnimation;
+	private scrollTop!: IAnimation;
+
+	constructor(from: ISmoothScrollPosition, to: ISmoothScrollPosition, startTime: number, duration: number) {
+		this.from = from;
+		this.to = to;
+		this.duration = duration;
+		this.startTime = startTime;
+
+		this.animationFrameDisposable = null;
+
+		this._initAnimations();
+	}
+
+	private _initAnimations(): void {
+		this.scrollLeft = this._initAnimation(this.from.scrollLeft, this.to.scrollLeft, this.to.width);
+		this.scrollTop = this._initAnimation(this.from.scrollTop, this.to.scrollTop, this.to.height);
+	}
+
+	private _initAnimation(from: number, to: number, viewportSize: number): IAnimation {
+		const delta = Math.abs(from - to);
+		if (delta > 2.5 * viewportSize) {
+			let stop1: number, stop2: number;
+			if (from < to) {
+				// scroll to 75% of the viewportSize
+				stop1 = from + 0.75 * viewportSize;
+				stop2 = to - 0.75 * viewportSize;
+			} else {
+				stop1 = from - 0.75 * viewportSize;
+				stop2 = to + 0.75 * viewportSize;
+			}
+			return createComposed(createEaseOutCubic(from, stop1), createEaseOutCubic(stop2, to), 0.33);
+		}
+		return createEaseOutCubic(from, to);
+	}
+
+	public dispose(): void {
+		if (this.animationFrameDisposable !== null) {
+			this.animationFrameDisposable.dispose();
+			this.animationFrameDisposable = null;
+		}
+	}
+
+	public acceptScrollDimensions(state: ScrollState): void {
+		this.to = state.withScrollPosition(this.to);
+		this._initAnimations();
+	}
+
+	public tick(): SmoothScrollingUpdate {
+		return this._tick(Date.now());
+	}
+
+	protected _tick(now: number): SmoothScrollingUpdate {
+		const completion = (now - this.startTime) / this.duration;
+
+		if (completion < 1) {
+			const newScrollLeft = this.scrollLeft(completion);
+			const newScrollTop = this.scrollTop(completion);
+			return new SmoothScrollingUpdate(newScrollLeft, newScrollTop, false);
+		}
+
+		return new SmoothScrollingUpdate(this.to.scrollLeft, this.to.scrollTop, true);
+	}
+
+	public combine(from: ISmoothScrollPosition, to: ISmoothScrollPosition, duration: number): SmoothScrollingOperation {
+		return SmoothScrollingOperation.start(from, to, duration);
+	}
+
+	public static start(from: ISmoothScrollPosition, to: ISmoothScrollPosition, duration: number): SmoothScrollingOperation {
+		// +10 / -10 : pretend the animation already started for a quicker response to a scroll request
+		duration = duration + 10;
+		const startTime = Date.now() - 10;
+
+		return new SmoothScrollingOperation(from, to, startTime, duration);
+	}
+}
+
+function easeInCubic(t: number) {
+	return Math.pow(t, 3);
+}
+
+function easeOutCubic(t: number) {
+	return 1 - easeInCubic(1 - t);
+}
diff --git a/src/vs/base/common/sequence.ts b/src/vs/base/common/sequence.ts
new file mode 100644
index 0000000000..b68bc7cdc8
--- /dev/null
+++ b/src/vs/base/common/sequence.ts
@@ -0,0 +1,34 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { Emitter, Event } from 'vs/base/common/event';
+
+export interface ISplice<T> {
+	readonly start: number;
+	readonly deleteCount: number;
+	readonly toInsert: readonly T[];
+}
+
+export interface ISpliceable<T> {
+	splice(start: number, deleteCount: number, toInsert: readonly T[]): void;
+}
+
+export interface ISequence<T> {
+	readonly elements: T[];
+	readonly onDidSplice: Event<ISplice<T>>;
+}
+
+export class Sequence<T> implements ISequence<T>, ISpliceable<T> {
+
+	readonly elements: T[] = [];
+
+	private readonly _onDidSplice = new Emitter<ISplice<T>>();
+	readonly onDidSplice: Event<ISplice<T>> = this._onDidSplice.event;
+
+	splice(start: number, deleteCount: number, toInsert: readonly T[] = []): void {
+		this.elements.splice(start, deleteCount, ...toInsert);
+		this._onDidSplice.fire({ start, deleteCount, toInsert });
+	}
+}
diff --git a/src/vs/base/common/stopwatch.ts b/src/vs/base/common/stopwatch.ts
new file mode 100644
index 0000000000..e32c0dd9d9
--- /dev/null
+++ b/src/vs/base/common/stopwatch.ts
@@ -0,0 +1,43 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+// fake definition so that the valid layers check won't trip on this
+declare const globalThis: { performance?: { now(): number } };
+
+const hasPerformanceNow = (globalThis.performance && typeof globalThis.performance.now === 'function');
+
+export class StopWatch {
+
+	private _startTime: number;
+	private _stopTime: number;
+
+	private readonly _now: () => number;
+
+	public static create(highResolution?: boolean): StopWatch {
+		return new StopWatch(highResolution);
+	}
+
+	constructor(highResolution?: boolean) {
+		this._now = hasPerformanceNow && highResolution === false ? Date.now : globalThis.performance!.now.bind(globalThis.performance);
+		this._startTime = this._now();
+		this._stopTime = -1;
+	}
+
+	public stop(): void {
+		this._stopTime = this._now();
+	}
+
+	public reset(): void {
+		this._startTime = this._now();
+		this._stopTime = -1;
+	}
+
+	public elapsed(): number {
+		if (this._stopTime !== -1) {
+			return this._stopTime - this._startTime;
+		}
+		return this._now() - this._startTime;
+	}
+}
diff --git a/src/vs/base/common/strings.ts b/src/vs/base/common/strings.ts
new file mode 100644
index 0000000000..74b4f00cd9
--- /dev/null
+++ b/src/vs/base/common/strings.ts
@@ -0,0 +1,557 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { CharCode } from 'vs/base/common/charCode';
+import { Constants } from 'vs/base/common/uint';
+
+export function isFalsyOrWhitespace(str: string | undefined): boolean {
+	if (!str || typeof str !== 'string') {
+		return true;
+	}
+	return str.trim().length === 0;
+}
+
+const _formatRegexp = /{(\d+)}/g;
+
+/**
+ * Helper to produce a string with a variable number of arguments. Insert variable segments
+ * into the string using the {n} notation where N is the index of the argument following the string.
+ * @param value string to which formatting is applied
+ * @param args replacements for {n}-entries
+ */
+export function format(value: string, ...args: any[]): string {
+	if (args.length === 0) {
+		return value;
+	}
+	return value.replace(_formatRegexp, function (match, group) {
+		const idx = parseInt(group, 10);
+		return isNaN(idx) || idx < 0 || idx >= args.length ?
+			match :
+			args[idx];
+	});
+}
+
+const _format2Regexp = /{([^}]+)}/g;
+
+/**
+ * Helper to create a string from a template and a string record.
+ * Similar to `format` but with objects instead of positional arguments.
+ */
+export function format2(template: string, values: Record<string, unknown>): string {
+	if (Object.keys(values).length === 0) {
+		return template;
+	}
+	return template.replace(_format2Regexp, (match, group) => (values[group] ?? match) as string);
+}
+
+/**
+ * Encodes the given value so that it can be used as literal value in html attributes.
+ *
+ * In other words, computes `$val`, such that `attr` in `<div attr="$val" />` has the runtime value `value`.
+ * This prevents XSS injection.
+ */
+export function htmlAttributeEncodeValue(value: string): string {
+	return value.replace(/[<>"'&]/g, ch => {
+		switch (ch) {
+			case '<': return '&lt;';
+			case '>': return '&gt;';
+			case '"': return '&quot;';
+			case '\'': return '&apos;';
+			case '&': return '&amp;';
+		}
+		return ch;
+	});
+}
+
+/**
+ * Converts HTML characters inside the string to use entities instead. Makes the string safe from
+ * being used e.g. in HTMLElement.innerHTML.
+ */
+export function escape(html: string): string {
+	return html.replace(/[<>&]/g, function (match) {
+		switch (match) {
+			case '<': return '&lt;';
+			case '>': return '&gt;';
+			case '&': return '&amp;';
+			default: return match;
+		}
+	});
+}
+
+/**
+ * Escapes regular expression characters in a given string
+ */
+export function escapeRegExpCharacters(value: string): string {
+	return value.replace(/[\\\{\}\*\+\?\|\^\$\.\[\]\(\)]/g, '\\$&');
+}
+
+/**
+ * Counts how often `substr` occurs inside `value`.
+ */
+export function count(value: string, substr: string): number {
+	let result = 0;
+	let index = value.indexOf(substr);
+	while (index !== -1) {
+		result++;
+		index = value.indexOf(substr, index + substr.length);
+	}
+	return result;
+}
+
+export function truncate(value: string, maxLength: number, suffix = '…'): string {
+	if (value.length <= maxLength) {
+		return value;
+	}
+
+	return `${value.substr(0, maxLength)}${suffix}`;
+}
+
+export function truncateMiddle(value: string, maxLength: number, suffix = '…'): string {
+	if (value.length <= maxLength) {
+		return value;
+	}
+
+	const prefixLength = Math.ceil(maxLength / 2) - suffix.length / 2;
+	const suffixLength = Math.floor(maxLength / 2) - suffix.length / 2;
+
+	return `${value.substr(0, prefixLength)}${suffix}${value.substr(value.length - suffixLength)}`;
+}
+
+/**
+ * Removes all occurrences of needle from the beginning and end of haystack.
+ * @param haystack string to trim
+ * @param needle the thing to trim (default is a blank)
+ */
+export function trim(haystack: string, needle: string = ' '): string {
+	const trimmed = ltrim(haystack, needle);
+	return rtrim(trimmed, needle);
+}
+
+/**
+ * Removes all occurrences of needle from the beginning of haystack.
+ * @param haystack string to trim
+ * @param needle the thing to trim
+ */
+export function ltrim(haystack: string, needle: string): string {
+	if (!haystack || !needle) {
+		return haystack;
+	}
+
+	const needleLen = needle.length;
+	if (needleLen === 0 || haystack.length === 0) {
+		return haystack;
+	}
+
+	let offset = 0;
+
+	while (haystack.indexOf(needle, offset) === offset) {
+		offset = offset + needleLen;
+	}
+	return haystack.substring(offset);
+}
+
+/**
+ * Removes all occurrences of needle from the end of haystack.
+ * @param haystack string to trim
+ * @param needle the thing to trim
+ */
+export function rtrim(haystack: string, needle: string): string {
+	if (!haystack || !needle) {
+		return haystack;
+	}
+
+	const needleLen = needle.length,
+		haystackLen = haystack.length;
+
+	if (needleLen === 0 || haystackLen === 0) {
+		return haystack;
+	}
+
+	let offset = haystackLen,
+		idx = -1;
+
+	while (true) {
+		idx = haystack.lastIndexOf(needle, offset - 1);
+		if (idx === -1 || idx + needleLen !== offset) {
+			break;
+		}
+		if (idx === 0) {
+			return '';
+		}
+		offset = idx;
+	}
+
+	return haystack.substring(0, offset);
+}
+
+export function convertSimple2RegExpPattern(pattern: string): string {
+	return pattern.replace(/[\-\\\{\}\+\?\|\^\$\.\,\[\]\(\)\#\s]/g, '\\$&').replace(/[\*]/g, '.*');
+}
+
+export function stripWildcards(pattern: string): string {
+	return pattern.replace(/\*/g, '');
+}
+
+export interface RegExpOptions {
+	matchCase?: boolean;
+	wholeWord?: boolean;
+	multiline?: boolean;
+	global?: boolean;
+	unicode?: boolean;
+}
+
+export function createRegExp(searchString: string, isRegex: boolean, options: RegExpOptions = {}): RegExp {
+	if (!searchString) {
+		throw new Error('Cannot create regex from empty string');
+	}
+	if (!isRegex) {
+		searchString = escapeRegExpCharacters(searchString);
+	}
+	if (options.wholeWord) {
+		if (!/\B/.test(searchString.charAt(0))) {
+			searchString = '\\b' + searchString;
+		}
+		if (!/\B/.test(searchString.charAt(searchString.length - 1))) {
+			searchString = searchString + '\\b';
+		}
+	}
+	let modifiers = '';
+	if (options.global) {
+		modifiers += 'g';
+	}
+	if (!options.matchCase) {
+		modifiers += 'i';
+	}
+	if (options.multiline) {
+		modifiers += 'm';
+	}
+	if (options.unicode) {
+		modifiers += 'u';
+	}
+
+	return new RegExp(searchString, modifiers);
+}
+
+export function regExpLeadsToEndlessLoop(regexp: RegExp): boolean {
+	// Exit early if it's one of these special cases which are meant to match
+	// against an empty string
+	if (regexp.source === '^' || regexp.source === '^$' || regexp.source === '$' || regexp.source === '^\\s*$') {
+		return false;
+	}
+
+	// We check against an empty string. If the regular expression doesn't advance
+	// (e.g. ends in an endless loop) it will match an empty string.
+	const match = regexp.exec('');
+	return !!(match && regexp.lastIndex === 0);
+}
+
+export function splitLines(str: string): string[] {
+	return str.split(/\r\n|\r|\n/);
+}
+
+export function splitLinesIncludeSeparators(str: string): string[] {
+	const linesWithSeparators: string[] = [];
+	const splitLinesAndSeparators = str.split(/(\r\n|\r|\n)/);
+	for (let i = 0; i < Math.ceil(splitLinesAndSeparators.length / 2); i++) {
+		linesWithSeparators.push(splitLinesAndSeparators[2 * i] + (splitLinesAndSeparators[2 * i + 1] ?? ''));
+	}
+	return linesWithSeparators;
+}
+
+/**
+ * Returns first index of the string that is not whitespace.
+ * If string is empty or contains only whitespaces, returns -1
+ */
+export function firstNonWhitespaceIndex(str: string): number {
+	for (let i = 0, len = str.length; i < len; i++) {
+		const chCode = str.charCodeAt(i);
+		if (chCode !== CharCode.Space && chCode !== CharCode.Tab) {
+			return i;
+		}
+	}
+	return -1;
+}
+
+/**
+ * Returns the leading whitespace of the string.
+ * If the string contains only whitespaces, returns entire string
+ */
+export function getLeadingWhitespace(str: string, start: number = 0, end: number = str.length): string {
+	for (let i = start; i < end; i++) {
+		const chCode = str.charCodeAt(i);
+		if (chCode !== CharCode.Space && chCode !== CharCode.Tab) {
+			return str.substring(start, i);
+		}
+	}
+	return str.substring(start, end);
+}
+
+/**
+ * Returns last index of the string that is not whitespace.
+ * If string is empty or contains only whitespaces, returns -1
+ */
+export function lastNonWhitespaceIndex(str: string, startIndex: number = str.length - 1): number {
+	for (let i = startIndex; i >= 0; i--) {
+		const chCode = str.charCodeAt(i);
+		if (chCode !== CharCode.Space && chCode !== CharCode.Tab) {
+			return i;
+		}
+	}
+	return -1;
+}
+
+/**
+ * Function that works identically to String.prototype.replace, except, the
+ * replace function is allowed to be async and return a Promise.
+ */
+export function replaceAsync(str: string, search: RegExp, replacer: (match: string, ...args: any[]) => Promise<string>): Promise<string> {
+	const parts: (string | Promise<string>)[] = [];
+
+	let last = 0;
+	for (const match of str.matchAll(search)) {
+		parts.push(str.slice(last, match.index));
+		if (match.index === undefined) {
+			throw new Error('match.index should be defined');
+		}
+
+		last = match.index + match[0].length;
+		parts.push(replacer(match[0], ...match.slice(1), match.index, str, match.groups));
+	}
+
+	parts.push(str.slice(last));
+
+	return Promise.all(parts).then(p => p.join(''));
+}
+
+export function compare(a: string, b: string): number {
+	if (a < b) {
+		return -1;
+	} else if (a > b) {
+		return 1;
+	} else {
+		return 0;
+	}
+}
+
+export function compareSubstring(a: string, b: string, aStart: number = 0, aEnd: number = a.length, bStart: number = 0, bEnd: number = b.length): number {
+	for (; aStart < aEnd && bStart < bEnd; aStart++, bStart++) {
+		const codeA = a.charCodeAt(aStart);
+		const codeB = b.charCodeAt(bStart);
+		if (codeA < codeB) {
+			return -1;
+		} else if (codeA > codeB) {
+			return 1;
+		}
+	}
+	const aLen = aEnd - aStart;
+	const bLen = bEnd - bStart;
+	if (aLen < bLen) {
+		return -1;
+	} else if (aLen > bLen) {
+		return 1;
+	}
+	return 0;
+}
+
+export function compareIgnoreCase(a: string, b: string): number {
+	return compareSubstringIgnoreCase(a, b, 0, a.length, 0, b.length);
+}
+
+export function compareSubstringIgnoreCase(a: string, b: string, aStart: number = 0, aEnd: number = a.length, bStart: number = 0, bEnd: number = b.length): number {
+
+	for (; aStart < aEnd && bStart < bEnd; aStart++, bStart++) {
+
+		let codeA = a.charCodeAt(aStart);
+		let codeB = b.charCodeAt(bStart);
+
+		if (codeA === codeB) {
+			// equal
+			continue;
+		}
+
+		if (codeA >= 128 || codeB >= 128) {
+			// not ASCII letters -> fallback to lower-casing strings
+			return compareSubstring(a.toLowerCase(), b.toLowerCase(), aStart, aEnd, bStart, bEnd);
+		}
+
+		// mapper lower-case ascii letter onto upper-case varinats
+		// [97-122] (lower ascii) --> [65-90] (upper ascii)
+		if (isLowerAsciiLetter(codeA)) {
+			codeA -= 32;
+		}
+		if (isLowerAsciiLetter(codeB)) {
+			codeB -= 32;
+		}
+
+		// compare both code points
+		const diff = codeA - codeB;
+		if (diff === 0) {
+			continue;
+		}
+
+		return diff;
+	}
+
+	const aLen = aEnd - aStart;
+	const bLen = bEnd - bStart;
+
+	if (aLen < bLen) {
+		return -1;
+	} else if (aLen > bLen) {
+		return 1;
+	}
+
+	return 0;
+}
+
+export function isAsciiDigit(code: number): boolean {
+	return code >= CharCode.Digit0 && code <= CharCode.Digit9;
+}
+
+export function isLowerAsciiLetter(code: number): boolean {
+	return code >= CharCode.a && code <= CharCode.z;
+}
+
+export function isUpperAsciiLetter(code: number): boolean {
+	return code >= CharCode.A && code <= CharCode.Z;
+}
+
+export function equalsIgnoreCase(a: string, b: string): boolean {
+	return a.length === b.length && compareSubstringIgnoreCase(a, b) === 0;
+}
+
+export function startsWithIgnoreCase(str: string, candidate: string): boolean {
+	const candidateLength = candidate.length;
+	if (candidate.length > str.length) {
+		return false;
+	}
+
+	return compareSubstringIgnoreCase(str, candidate, 0, candidateLength) === 0;
+}
+
+/**
+ * @returns the length of the common prefix of the two strings.
+ */
+export function commonPrefixLength(a: string, b: string): number {
+
+	const len = Math.min(a.length, b.length);
+	let i: number;
+
+	for (i = 0; i < len; i++) {
+		if (a.charCodeAt(i) !== b.charCodeAt(i)) {
+			return i;
+		}
+	}
+
+	return len;
+}
+
+/**
+ * @returns the length of the common suffix of the two strings.
+ */
+export function commonSuffixLength(a: string, b: string): number {
+
+	const len = Math.min(a.length, b.length);
+	let i: number;
+
+	const aLastIndex = a.length - 1;
+	const bLastIndex = b.length - 1;
+
+	for (i = 0; i < len; i++) {
+		if (a.charCodeAt(aLastIndex - i) !== b.charCodeAt(bLastIndex - i)) {
+			return i;
+		}
+	}
+
+	return len;
+}
+
+/**
+ * See http://en.wikipedia.org/wiki/Surrogate_pair
+ */
+export function isHighSurrogate(charCode: number): boolean {
+	return (0xD800 <= charCode && charCode <= 0xDBFF);
+}
+
+/**
+ * See http://en.wikipedia.org/wiki/Surrogate_pair
+ */
+export function isLowSurrogate(charCode: number): boolean {
+	return (0xDC00 <= charCode && charCode <= 0xDFFF);
+}
+
+/**
+ * See http://en.wikipedia.org/wiki/Surrogate_pair
+ */
+export function computeCodePoint(highSurrogate: number, lowSurrogate: number): number {
+	return ((highSurrogate - 0xD800) << 10) + (lowSurrogate - 0xDC00) + 0x10000;
+}
+
+/**
+ * get the code point that begins at offset `offset`
+ */
+export function getNextCodePoint(str: string, len: number, offset: number): number {
+	const charCode = str.charCodeAt(offset);
+	if (isHighSurrogate(charCode) && offset + 1 < len) {
+		const nextCharCode = str.charCodeAt(offset + 1);
+		if (isLowSurrogate(nextCharCode)) {
+			return computeCodePoint(charCode, nextCharCode);
+		}
+	}
+	return charCode;
+}
+
+/**
+ * get the code point that ends right before offset `offset`
+ */
+function getPrevCodePoint(str: string, offset: number): number {
+	const charCode = str.charCodeAt(offset - 1);
+	if (isLowSurrogate(charCode) && offset > 1) {
+		const prevCharCode = str.charCodeAt(offset - 2);
+		if (isHighSurrogate(prevCharCode)) {
+			return computeCodePoint(prevCharCode, charCode);
+		}
+	}
+	return charCode;
+}
+
+export class CodePointIterator {
+
+	private readonly _str: string;
+	private readonly _len: number;
+	private _offset: number;
+
+	public get offset(): number {
+		return this._offset;
+	}
+
+	constructor(str: string, offset: number = 0) {
+		this._str = str;
+		this._len = str.length;
+		this._offset = offset;
+	}
+
+	public setOffset(offset: number): void {
+		this._offset = offset;
+	}
+
+	public prevCodePoint(): number {
+		const codePoint = getPrevCodePoint(this._str, this._offset);
+		this._offset -= (codePoint >= Constants.UNICODE_SUPPLEMENTARY_PLANE_BEGIN ? 2 : 1);
+		return codePoint;
+	}
+
+	public nextCodePoint(): number {
+		const codePoint = getNextCodePoint(this._str, this._len, this._offset);
+		this._offset += (codePoint >= Constants.UNICODE_SUPPLEMENTARY_PLANE_BEGIN ? 2 : 1);
+		return codePoint;
+	}
+
+	public eol(): boolean {
+		return (this._offset >= this._len);
+	}
+}
+
+export const noBreakWhitespace = '\xa0';
diff --git a/src/vs/base/common/symbols.ts b/src/vs/base/common/symbols.ts
new file mode 100644
index 0000000000..9aa8e5bb04
--- /dev/null
+++ b/src/vs/base/common/symbols.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.
+ *--------------------------------------------------------------------------------------------*/
+
+/**
+ * Can be passed into the Delayed to defer using a microtask
+ * */
+export const MicrotaskDelay = Symbol('MicrotaskDelay');
diff --git a/src/vs/base/common/uint.ts b/src/vs/base/common/uint.ts
new file mode 100644
index 0000000000..347af57eec
--- /dev/null
+++ b/src/vs/base/common/uint.ts
@@ -0,0 +1,59 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+export const enum Constants {
+	/**
+	 * MAX SMI (SMall Integer) as defined in v8.
+	 * one bit is lost for boxing/unboxing flag.
+	 * one bit is lost for sign flag.
+	 * See https://thibaultlaurens.github.io/javascript/2013/04/29/how-the-v8-engine-works/#tagged-values
+	 */
+	MAX_SAFE_SMALL_INTEGER = 1 << 30,
+
+	/**
+	 * MIN SMI (SMall Integer) as defined in v8.
+	 * one bit is lost for boxing/unboxing flag.
+	 * one bit is lost for sign flag.
+	 * See https://thibaultlaurens.github.io/javascript/2013/04/29/how-the-v8-engine-works/#tagged-values
+	 */
+	MIN_SAFE_SMALL_INTEGER = -(1 << 30),
+
+	/**
+	 * Max unsigned integer that fits on 8 bits.
+	 */
+	MAX_UINT_8 = 255, // 2^8 - 1
+
+	/**
+	 * Max unsigned integer that fits on 16 bits.
+	 */
+	MAX_UINT_16 = 65535, // 2^16 - 1
+
+	/**
+	 * Max unsigned integer that fits on 32 bits.
+	 */
+	MAX_UINT_32 = 4294967295, // 2^32 - 1
+
+	UNICODE_SUPPLEMENTARY_PLANE_BEGIN = 0x010000
+}
+
+export function toUint8(v: number): number {
+	if (v < 0) {
+		return 0;
+	}
+	if (v > Constants.MAX_UINT_8) {
+		return Constants.MAX_UINT_8;
+	}
+	return v | 0;
+}
+
+export function toUint32(v: number): number {
+	if (v < 0) {
+		return 0;
+	}
+	if (v > Constants.MAX_UINT_32) {
+		return Constants.MAX_UINT_32;
+	}
+	return v | 0;
+}
diff --git a/src/vs/patches/nls.ts b/src/vs/patches/nls.ts
new file mode 100644
index 0000000000..1661dfb04b
--- /dev/null
+++ b/src/vs/patches/nls.ts
@@ -0,0 +1,90 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+export interface ILocalizeInfo {
+  key: string;
+  comment: string[];
+}
+
+export function localize(info: ILocalizeInfo | string, message: string, ...args: (string | number | boolean | undefined | null)[]): string {
+  return message;
+}
+
+export interface INLSLanguagePackConfiguration {
+
+  /**
+   * The path to the translations config file that contains pointers to
+   * all message bundles for `main` and extensions.
+   */
+  readonly translationsConfigFile: string;
+
+  /**
+   * The path to the file containing the translations for this language
+   * pack as flat string array.
+   */
+  readonly messagesFile: string;
+
+  /**
+   * The path to the file that can be used to signal a corrupt language
+   * pack, for example when reading the `messagesFile` fails. This will
+   * instruct the application to re-create the cache on next startup.
+   */
+  readonly corruptMarkerFile: string;
+}
+
+export interface INLSConfiguration {
+
+  /**
+   * Locale as defined in `argv.json` or `app.getLocale()`.
+   */
+  readonly userLocale: string;
+
+  /**
+   * Locale as defined by the OS (e.g. `app.getPreferredSystemLanguages()`).
+   */
+  readonly osLocale: string;
+
+  /**
+   * The actual language of the UI that ends up being used considering `userLocale`
+   * and `osLocale`.
+   */
+  readonly resolvedLanguage: string;
+
+  /**
+   * Defined if a language pack is used that is not the
+   * default english language pack. This requires a language
+   * pack to be installed as extension.
+   */
+  readonly languagePack?: INLSLanguagePackConfiguration;
+
+  /**
+   * The path to the file containing the default english messages
+   * as flat string array. The file is only present in built
+   * versions of the application.
+   */
+  readonly defaultMessagesFile: string;
+
+  /**
+   * Below properties are deprecated and only there to continue support
+   * for `vscode-nls` module that depends on them.
+   * Refs https://github.com/microsoft/vscode-nls/blob/main/src/node/main.ts#L36-L46
+   */
+  /** @deprecated */
+  readonly locale: string;
+  /** @deprecated */
+  readonly availableLanguages: Record<string, string>;
+  /** @deprecated */
+  readonly _languagePackSupport?: boolean;
+  /** @deprecated */
+  readonly _languagePackId?: string;
+  /** @deprecated */
+  readonly _translationsConfigFile?: string;
+  /** @deprecated */
+  readonly _cacheRoot?: string;
+  /** @deprecated */
+  readonly _resolvedLanguagePackCoreLocation?: string;
+  /** @deprecated */
+  readonly _corruptedFile?: string;
+}
diff --git a/src/vs/tsconfig.json b/src/vs/tsconfig.json
new file mode 100644
index 0000000000..c45bc40ccf
--- /dev/null
+++ b/src/vs/tsconfig.json
@@ -0,0 +1,69 @@
+{
+  "extends": "../tsconfig-library-base",
+	"compilerOptions": {
+		"experimentalDecorators": true,
+		"noImplicitReturns": true,
+		"noImplicitOverride": true,
+		"noUnusedLocals": true,
+		"allowUnreachableCode": false,
+		"strict": true,
+		"exactOptionalPropertyTypes": false,
+		"useUnknownInCatchVariables": false,
+		"forceConsistentCasingInFileNames": true,
+		"paths": {
+			"vs/*": [
+				"./vs/*"
+			],
+			"vs/nls": [
+        "./vs/patches/nls"
+      ]
+		},
+		"target": "es2022",
+		"useDefineForClassFields": false,
+		"lib": [
+			"ES2022",
+			"DOM",
+			"DOM.Iterable",
+			"WebWorker.ImportScripts"
+		],
+		"esModuleInterop": true,
+		"removeComments": false,
+		"preserveConstEnums": true,
+		"sourceMap": false,
+		"allowJs": true,
+		"resolveJsonModule": true,
+		"isolatedModules": true,
+		"types": [
+			"trusted-types",
+		],
+    "composite": true,
+    "outDir": "../../out",
+    "baseUrl": "../",
+	},
+	"include": [
+    "./base/**/*",
+    "./patches/**/*",
+    "./typings/**/*",
+  ],
+  "exclude": [
+    // Update repo folder
+    "temp/",
+
+    // Unwanted layers/modules
+    "**/electron-main",
+    "**/electron-sandbox",
+    "**/node",
+    "**/parts",
+    "**/test",
+    "**/sandbox",
+    "**/worker",
+
+    // Problematic files
+    "base/common/amd.ts",
+    "base/browser/defaultWorkerFactory.ts",
+    "base/common/jsonc.js",
+    "base/common/performance.js",
+    "base/common/marked/marked.js",
+    "base/common/network.ts"
+  ]
+}
diff --git a/src/vs/typings/base-common.d.ts b/src/vs/typings/base-common.d.ts
new file mode 100644
index 0000000000..4fc7b59856
--- /dev/null
+++ b/src/vs/typings/base-common.d.ts
@@ -0,0 +1,20 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+// Declare types that we probe for to implement util and/or polyfill functions
+
+declare global {
+
+	interface IdleDeadline {
+		readonly didTimeout: boolean;
+		timeRemaining(): number;
+	}
+
+	function requestIdleCallback(callback: (args: IdleDeadline) => void, options?: { timeout: number }): number;
+	function cancelIdleCallback(handle: number): void;
+
+}
+
+export { }
diff --git a/src/vs/typings/require.d.ts b/src/vs/typings/require.d.ts
new file mode 100644
index 0000000000..7934279012
--- /dev/null
+++ b/src/vs/typings/require.d.ts
@@ -0,0 +1,42 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+declare class LoaderEvent {
+	readonly type: number;
+	readonly timestamp: number;
+	readonly detail: string;
+}
+
+declare const define: {
+	(moduleName: string, dependencies: string[], callback: (...args: any[]) => any): any;
+	(moduleName: string, dependencies: string[], definition: any): any;
+	(moduleName: string, callback: (...args: any[]) => any): any;
+	(moduleName: string, definition: any): any;
+	(dependencies: string[], callback: (...args: any[]) => any): any;
+	(dependencies: string[], definition: any): any;
+};
+
+interface NodeRequire {
+	/**
+	 * @deprecated use `FileAccess.asFileUri()` for node.js contexts or `FileAccess.asBrowserUri` for browser contexts.
+	 */
+	toUrl(path: string): string;
+
+	/**
+	 * @deprecated MUST not be used anymore
+	 *
+	 * With the move from AMD to ESM we cannot use this anymore. There will be NO MORE node require like this.
+	 */
+	__$__nodeRequire<T>(moduleName: string): T;
+
+	(dependencies: string[], callback: (...args: any[]) => any, errorback?: (err: any) => void): any;
+	config(data: any): any;
+	onError: Function;
+	getStats?(): ReadonlyArray<LoaderEvent>;
+	hasDependencyCycle?(): boolean;
+	define(amdModuleId: string, dependencies: string[], callback: (...args: any[]) => any): any;
+}
+
+declare var require: NodeRequire;
diff --git a/src/vs/typings/thenable.d.ts b/src/vs/typings/thenable.d.ts
new file mode 100644
index 0000000000..73373eadba
--- /dev/null
+++ b/src/vs/typings/thenable.d.ts
@@ -0,0 +1,12 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+/**
+ * Thenable is a common denominator between ES6 promises, Q, jquery.Deferred, WinJS.Promise,
+ * and others. This API makes no assumption about what promise library is being used which
+ * enables reusing existing code without migrating to a specific promise implementation. Still,
+ * we recommend the use of native promises which are available in VS Code.
+ */
+interface Thenable<T> extends PromiseLike<T> { }
diff --git a/src/vs/typings/vscode-globals-nls.d.ts b/src/vs/typings/vscode-globals-nls.d.ts
new file mode 100644
index 0000000000..bc48076762
--- /dev/null
+++ b/src/vs/typings/vscode-globals-nls.d.ts
@@ -0,0 +1,36 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+// AMD2ESM mirgation relevant
+
+/**
+ * NLS Globals: these need to be defined in all contexts that make
+ * use of our `nls.localize` and `nls.localize2` functions. This includes:
+ * - Electron main process
+ * - Electron window (renderer) process
+ * - Utility Process
+ * - Node.js
+ * - Browser
+ * - Web worker
+ *
+ * That is because during build time we strip out all english strings from
+ * the resulting JS code and replace it with a <number> that is then looked
+ * up from the `_VSCODE_NLS_MESSAGES` array.
+ */
+declare global {
+	/**
+	 * All NLS messages produced by `localize` and `localize2` calls
+	 * under `src/vs` translated to the language as indicated by
+	 * `_VSCODE_NLS_LANGUAGE`.
+	 */
+	var _VSCODE_NLS_MESSAGES: string[];
+	/**
+	 * The actual language of the NLS messages (e.g. 'en', de' or 'pt-br').
+	 */
+	var _VSCODE_NLS_LANGUAGE: string | undefined;
+}
+
+// fake export to make global work
+export { }
diff --git a/src/vs/typings/vscode-globals-product.d.ts b/src/vs/typings/vscode-globals-product.d.ts
new file mode 100644
index 0000000000..f6aa62bfe9
--- /dev/null
+++ b/src/vs/typings/vscode-globals-product.d.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.
+ *--------------------------------------------------------------------------------------------*/
+
+// AMD2ESM mirgation relevant
+
+declare global {
+
+	/**
+	 * Holds the file root for resources.
+	 */
+	var _VSCODE_FILE_ROOT: string;
+
+	/**
+	 * CSS loader that's available during development time.
+	 * DO NOT call directly, instead just import css modules, like `import 'some.css'`
+	 */
+	var _VSCODE_CSS_LOAD: (module: string) => void;
+
+	/**
+	 * @deprecated You MUST use `IProductService` whenever possible.
+	 */
+	var _VSCODE_PRODUCT_JSON: Record<string, any>;
+	/**
+	 * @deprecated You MUST use `IProductService` whenever possible.
+	 */
+	var _VSCODE_PACKAGE_JSON: Record<string, any>;
+
+}
+
+// fake export to make global work
+export { }
diff --git a/typings/xterm-headless.d.ts b/typings/xterm-headless.d.ts
index 2d3329edf0..3cbde44b2b 100644
--- a/typings/xterm-headless.d.ts
+++ b/typings/xterm-headless.d.ts
@@ -83,6 +83,8 @@ declare module '@xterm/headless' {
 
     /**
      * The modifier key hold to multiply scroll speed.
+     * @deprecated This option is no longer available and will always use alt.
+     * Setting this will be ignored.
      */
     fastScrollModifier?: 'none' | 'alt' | 'ctrl' | 'shift';
 
diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts
index b64702757c..3aa4b1910d 100644
--- a/typings/xterm.d.ts
+++ b/typings/xterm.d.ts
@@ -109,11 +109,13 @@ declare module '@xterm/xterm' {
 
     /**
      * The modifier key hold to multiply scroll speed.
+     * @deprecated This option is no longer available and will always use alt.
+     * Setting this will be ignored.
      */
     fastScrollModifier?: 'none' | 'alt' | 'ctrl' | 'shift';
 
     /**
-     * The scroll speed multiplier used for fast scrolling.
+     * The scroll speed multiplier used for fast scrolling when `Alt` is held.
      */
     fastScrollSensitivity?: number;
 
@@ -368,6 +370,28 @@ declare module '@xterm/xterm' {
      * be transparent)
      */
     selectionInactiveBackground?: string;
+    /**
+     * The scrollbar slider background color. Defaults to
+     * {@link ITerminalOptions.foreground foreground} with 20% opacity.
+     */
+    scrollbarSliderBackground?: string;
+    /**
+     * The scrollbar slider background color when hovered. Defaults to
+     * {@link ITerminalOptions.foreground foreground} with 40% opacity.
+     */
+    scrollbarSliderHoverBackground?: string;
+    /**
+     * The scrollbar slider background color when clicked. Defaults to
+     * {@link ITerminalOptions.foreground foreground} with 50% opacity.
+     */
+    scrollbarSliderActiveBackground?: string;
+    /**
+     * The border color of the overview ruler. This visually separates the
+     * terminal from the scroll bar when
+     * {@link ITerminalOptions.overviewRulerWidth overviewRulerWidth} is set.
+     * When this is not set it defaults to black (`#000000`).
+     */
+    overviewRulerBorder?: string;
     /** ANSI black (eg. `\x1b[30m`) */
     black?: string;
     /** ANSI red (eg. `\x1b[31m`) */
diff --git a/webpack.config.js b/webpack.config.js
index 3769bfb0fb..123e31dfbb 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -33,7 +33,8 @@ const config = {
     extensions: [ '.js' ],
     alias: {
       common: path.resolve('./out/common'),
-      browser: path.resolve('./out/browser')
+      browser: path.resolve('./out/browser'),
+      vs: path.resolve('./out/vs'),
     }
   },
   output: {
diff --git a/yarn.lock b/yarn.lock
index 1422c7db3e..5925b86dae 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -830,6 +830,11 @@
   resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.2.tgz#6286b4c7228d58ab7866d19716f3696e03a09397"
   integrity sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==
 
+"@types/trusted-types@^1.0.6":
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-1.0.6.tgz#569b8a08121d3203398290d602d84d73c8dcf5da"
+  integrity sha512-230RC8sFeHoT6sSUlRO6a8cAnclO06eeiq1QDfiv2FGCLWFvvERWgwIQD4FWqD9A69BN7Lzee4OXwoMVnnsWDw==
+
 "@types/utf8@^3.0.0":
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/@types/utf8/-/utf8-3.0.1.tgz#bf081663d4fff05ee63b41f377a35f8b189f7e5b"