diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 00000000..39babb71
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,20 @@
+# Stolen from https://github.com/HaaLeo/publish-vscode-extension#readme
+name: Deploy Extension
+
+on:
+  workflow_dispatch:
+
+jobs:
+  deploy:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-node@v4
+        with:
+          node-version: 20
+      - run: npm ci
+      - name: Publish to Open VSX Registry
+        uses: HaaLeo/publish-vscode-extension@v1.6.2
+        id: publishToOpenVSX
+        with:
+          pat: ${{ secrets.OPEN_VSX_TOKEN }}
diff --git a/.github/workflows/standard_push.yml b/.github/workflows/standard_push.yml
new file mode 100644
index 00000000..939571ac
--- /dev/null
+++ b/.github/workflows/standard_push.yml
@@ -0,0 +1,26 @@
+name: CI
+
+on:
+  push:
+    branches: [ main ]
+  pull_request:
+    branches: [ main ]
+
+jobs:
+  test:
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Install Node Dependencies
+        run: npm ci
+
+      - name: Setup Biome CLI
+        uses: biomejs/setup-biome@v2
+
+      - name: Code quality
+        run: biome ci ./src/
+
+      - name: Run tests
+        run: xvfb-run -a npm test
diff --git a/.gitignore b/.gitignore
index 8e5962ee..55537343 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,5 @@
 out
-node_modules
\ No newline at end of file
+dist
+*.vsix
+node_modules
+.vscode-test/
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 00000000..9d19b7ca
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,5 @@
+{
+    "recommendations": [
+        "biomejs.biome"
+    ]
+}
\ No newline at end of file
diff --git a/.vscode/launch.json b/.vscode/launch.json
index d60c89de..b7938601 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -1,28 +1,36 @@
-// A launch configuration that compiles the extension and then opens it inside a new window
 {
-	"version": "0.1.0",
+	"version": "0.2.0",
 	"configurations": [
 		{
-			"name": "Launch Extension",
+			"name": "Extension",
 			"type": "extensionHost",
 			"request": "launch",
 			"runtimeExecutable": "${execPath}",
-			"args": ["--extensionDevelopmentPath=${workspaceRoot}" ],
-			"stopOnEntry": false,
+			"args": [
+				"--disable-extensions",
+				"--extensionDevelopmentPath=${workspaceRoot}"
+			],
 			"sourceMaps": true,
-			"outDir": "${workspaceRoot}/out/src",
-			"preLaunchTask": "npm"
+			"outFiles": [
+				"${workspaceRoot}/out/src/**/*.js"
+			],
+			"preLaunchTask": "npm: pretest"
 		},
 		{
-			"name": "Launch Tests",
+			"name": "Test Extension",
 			"type": "extensionHost",
 			"request": "launch",
 			"runtimeExecutable": "${execPath}",
-			"args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/out/test" ],
-			"stopOnEntry": false,
+			"args": [
+				"--disable-extensions",
+				"--extensionDevelopmentPath=${workspaceRoot}",
+				"--extensionTestsPath=${workspaceRoot}/out/test/suite"
+			],
 			"sourceMaps": true,
-			"outDir": "${workspaceRoot}/out/test",
-			"preLaunchTask": "npm"
+			"outFiles": [
+				"${workspaceRoot}/out/src/**/*.js"
+			],
+			"preLaunchTask": "npm: pretest"
 		}
 	]
 }
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 3f5aa9cf..3d1f39f1 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,10 +1,16 @@
 // Place your settings in this file to overwrite default and user settings.
 {
-	"files.exclude": {
-		"out": false // set this to true to hide the "out" folder with the compiled JS files
-	},
 	"search.exclude": {
-		"out": true // set this to false to include "out" folder in search results
+		"out": true,
+		"dist": true,
+		"nod_modules": true
 	},
-	"typescript.tsdk": "./node_modules/typescript/lib" // we want to use the TS server from our node_modules folder to control its version
+	"typescript.tsdk": "./node_modules/typescript/lib",
+	"editor.insertSpaces": false,
+	"editor.formatOnSave": true,
+	"javascript.validate.enable": true,
+	"editor.showUnused": true,
+	"[typescript]": {
+		"editor.defaultFormatter": "biomejs.biome"
+	}
 }
\ No newline at end of file
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
deleted file mode 100644
index 1992757d..00000000
--- a/.vscode/tasks.json
+++ /dev/null
@@ -1,30 +0,0 @@
-// Available variables which can be used inside of strings.
-// ${workspaceRoot}: the root folder of the team
-// ${file}: the current opened file
-// ${fileBasename}: the current opened file's basename
-// ${fileDirname}: the current opened file's dirname
-// ${fileExtname}: the current opened file's extension
-// ${cwd}: the current working directory of the spawned process
-
-// A task runner that calls a custom npm script that compiles the extension.
-{
-	"version": "0.1.0",
-
-	// we want to run npm
-	"command": "npm",
-
-	// the command is a shell script
-	"isShellCommand": true,
-
-	// show the output window only if unrecognized errors occur.
-	"showOutput": "silent",
-
-	// we run the custom script "compile" as defined in package.json
-	"args": ["run", "compile", "--loglevel", "silent"],
-
-	// The tsc compiler is started in watching mode
-	"isWatching": true,
-
-	// use the standard tsc in watch mode problem matcher to find compile problems in the output.
-	"problemMatcher": "$tsc-watch"
-}
\ No newline at end of file
diff --git a/.vscodeignore b/.vscodeignore
index 795e7143..6f0ee9fc 100644
--- a/.vscodeignore
+++ b/.vscodeignore
@@ -1,9 +1,19 @@
+.github/**
 .vscode/**
-typings/**
-out/test/**
 test/**
 src/**
+types/**
+out/**
+!out/src/index.js
 **/*.map
 .gitignore
+.eslintrc
+.eslintignore
+CHANGELOG OLD.md
+esbuild.mjs
 tsconfig.json
-vsc-extension-quickstart.md
+tsconfig.test.json
+package-lock.json
+node_modules
+rollup.config.js
+**/preview.png
\ No newline at end of file
diff --git a/CHANGELOG OLD.md b/CHANGELOG OLD.md
new file mode 100644
index 00000000..3e806368
--- /dev/null
+++ b/CHANGELOG OLD.md	
@@ -0,0 +1,397 @@
+# Older changelog
+
+## 9.0.1 (August 02, 2022)
+* Fix: CHANGELOG.md headers are of the wrong level and other fixes
+* Fix: Updating dependencies
+
+## 9.0.0 (July 29, 2022)
+* Breaking change: Removed `gitblame.isWebPathPlural`. Please use `gitblame.pluralWebPathSubstrings`.
+* Breaking change: Removed `gitblame.statusBarMessageDisplayRight`. Status bar position is now always to the right.
+* Feature: Apply `-C` option to git blame for more precise blaming in reorganized files [#125](https://github.com/Sertion/vscode-gitblame/issues/125) (Thanks to [Andy Li](https://github.com/Friendly-Robot))
+* Feature: `gitblame.commitUrl` has a real value as default and no longer gives special meaning to an empty value
+* Feature: Add new tokens to `gitblame.commitUrl`:
+  * `${file.path.result}` - path to the final file
+  * `${file.path.source}` - path to the original file
+  * `${file.line.result}` - the line number of the line in the final file
+  * `${file.line.source}` - the line number of the line in the original file
+  * `${tool.protocol}` - `http:` or `https:`
+  * `${tool.commitpath}` - `/commit/` or `/commits`
+* Feature: New preview image
+* Bug: 'View' button leads to dead link when repo is fully local [#128](https://github.com/Sertion/vscode-gitblame/issues/128) (Thanks to [Milli Beckers](https://github.com/emilyjbeckers))
+* Fix: Updating the readme
+* Fix: The `${commit.hash}` token now takes a parameter just like `${commit.hash_short,length}`
+* Fix: Updating dependencies
+
+## 8.2.3 (June 17, 2022)
+* Bug: Reduce the amount of `git` calls
+* Feature: Move to new build system with fewer dependencies
+* Fix: Updating dependencies
+
+## 8.2.2 (June 04, 2022)
+* Bug: Errors get logged to console when directory is not using git [#124](https://github.com/Sertion/vscode-gitblame/issues/124) (Thanks to [Daniel Abromeit aka Abro](https://github.com/Abromeit))
+
+## 8.2.1 (May 25, 2022)
+* Fix: Updating dependencies
+* Fix: CHANGELOG.md now has correct release date for 8.2.0
+* Fix: CHANGELOG.md link to contributor
+
+## 8.2.0 (May 13, 2022)
+* Feature: Blame information as soon as it is available. This will allow some lines to show blame information while others still are waiting for `git blame`.
+* Feature: Allow blaming symlinked files [#121](https://github.com/Sertion/vscode-gitblame/pull/121) (Thanks to [Angel Fraga Parodi](https://github.com/angelfraga))
+* Bug: Blame info persistent over file change and file close [#115](https://github.com/Sertion/vscode-gitblame/issues/115) (Thanks to [Wenfang Du](https://github.com/wenfangdu) and [Viktor Stenqvist](https://github.com/Yottster))
+* Bug: commit.summary before commit.hash_short causes display bug [#119](https://github.com/Sertion/vscode-gitblame/issues/119) (Thanks to [Cathryne Linenweaver](https://github.com/Cathryne))
+* Bug: Remove right-to-left override characters from parsed text [#122](https://github.com/Sertion/vscode-gitblame/pull/122) (Thanks to [Chris Atkin](https://github.com/atkinchris))
+
+## 8.1.0 (July 16, 2021)
+* Fix: Avoid reloading for settings change [#112](https://github.com/Sertion/vscode-gitblame/pull/112) (Thanks to [João Moreno](https://github.com/joaomoreno))
+
+## 8.0.1 (July 16, 2021)
+* Feature: We now use vscodes language for Intl.RelativeTimeFormat locale [#111](https://github.com/Sertion/vscode-gitblame/issues/111) (Thanks to [AmosChenYQ](https://github.com/AmosChenYQ))
+* Fix: CHANGELOG.md now has correct release date for 8.0.0
+
+## 8.0.0 (July 15, 2021)
+* Breaking change: Removed `${time.c_from}` and `${time.from}`, please use `${time.c_ago}` and `${time.ago}` instead. They have been aliases since version 3.0.0
+* Breaking change: Dangling tokens (non-closed) now resolve correctly to their literal text instead of breaking
+* Breaking change: Modifiers on tokens with parameters now work as expected
+* Breaking change: Using Intl.RelativeTimeFormat to get relative time messages. This will change some things in the output:
+  * Before `1 hour ago`, after `60 minutes ago`
+  * Before `right now`, after `4 minutes ago`
+  * etc.
+* Breaking change: Output channel renamed from "gitblame" to "Git Blame"
+* Feature: New setting (`gitblame.statusBarMessageDisplayRight`) will show status bar message to the right (Thanks to [João Moreno](https://github.com/joaomoreno))
+* Feature: New setting (`gitblame.statusBarMessageClickAction`) will allow you to make clicks on the status bar directly open the tool URL (Thanks to [João Moreno](https://github.com/joaomoreno))
+* Feature: Some info messages have changed.
+* Feature: Now compiling for Node 14 and vscode ^1.57.0
+
+## 7.0.6 (May 13, 2021)
+* Bug: Issue with large blames caused some commits to be overwritten by empty versions of themselves (Thanks to [smcdef](https://github.com/smcdef) for reporting this)
+* Bug: Last version was incorrectly marked as 7.0.4 in CHANGELOG.md
+
+## 7.0.5 (May 13, 2021)
+* Bug: Some lines are not blameable (Thanks to [smcdef](https://github.com/smcdef))
+
+## 7.0.4 (May 09, 2021)
+* Bug: Fix issue in token length function
+
+## 7.0.3 (May 09, 2021)
+* Bug: Git blame status bar info disappears on 7.0.2 [#102](https://github.com/Sertion/vscode-gitblame/issues/102) (Thanks to [smcdef](https://github.com/smcdef) and [Gene](https://github.com/geneaiello))
+* Bug: Quick link not working [#103](https://github.com/Sertion/vscode-gitblame/issues/103) (Thanks to [Ricardo Faria](https://github.com/RicardoFariaSilva))
+
+## 7.0.2 (May 08, 2021)
+* Fix: Adding the fix from the previous release
+
+## 7.0.1 (May 08, 2021)
+* Bug: Git blame status bar info disappears on 7.0.0 [#101](https://github.com/Sertion/vscode-gitblame/issues/101) (Thanks to [Ben Reinhart](https://github.com/benjreinhart))
+
+## 7.0.0 (May 07, 2021)
+* Breaking change: Removing setting `gitblame.inferCommitUrl`. We will now always make an attempt at the URL.
+* Feature: Added new token (`${gitorigin.path,n}`) for `gitblame.commitUrl`.
+* Feature: Adding a tag to show we don't support the new virtual workspaces
+* Fix: Updating dependencies
+
+## 6.0.2 (December 20, 2020)
+* Fix: Spelling error in CHANGELOG.md
+* Bug: Stuck Waiting for git blame response (6.0.0) [#95](https://github.com/Sertion/vscode-gitblame/issues/95) (Thanks to [Vadzim Dambrouski](https://github.com/pftbest))
+
+## 6.0.1 (December 19, 2020)
+* Bug: Stuck Waiting for git blame response (6.0.0) [#95](https://github.com/Sertion/vscode-gitblame/issues/95) (Thanks to [Vadzim Dambrouski](https://github.com/pftbest))
+
+## 6.0.0 (October 14, 2020)
+* Breaking change: Removing setting `gitblame.logNonCritical` as we no longer produce any critical errors
+* Breaking change: The token `${|mod}` now expands to `|mod`, previously `${|mod}`
+* Breaking change: Renamed output channel from "Extension: gitblame" to "gitblame"
+* Bug: "Waiting for git blame response" (5.0.1) [#92](https://github.com/Sertion/vscode-gitblame/issues/92) (Thanks to [Dominik Zogg](https://github.com/dominikzogg) and [Alex Shelmire](https://github.com/shelmire))
+
+## 5.0.1 (October 10, 2020)
+* Feature: Faster load times
+* Feature: Deactivation function for easier and faster uninstalls
+* Fix: Reducing the number of dependencies
+* Fix: Updating dependencies
+
+## 5.0.0 (September 10, 2020)
+* Breaking change: Previously we stripped the port from git origin. We now keep the port if the protocol is http or https.
+* Feature Support HTTP git origins with port [#89](https://github.com/Sertion/vscode-gitblame/issues/89) (Thanks to [xgdgsc](https://github.com/xgdgsc))
+* Fix: Updating dependencies
+
+## 4.2.0 (July 24, 2020)
+* Feature: Relative Path Token [#87](https://github.com/Sertion/vscode-gitblame/pull/87) (Thanks to [Ben](https://github.com/bwathen))
+
+## 4.1.0 (July 09, 2020)
+* Feature: Improve time ago estimate [#83](https://github.com/Sertion/vscode-gitblame/pull/83) (Thanks to [Ben Langlois](https://github.com/BenLanglois))
+* Feature: Unable to open '': File is a directory. [#84](https://github.com/Sertion/vscode-gitblame/issues/84) (Thanks to [Matt Fletcher](https://github.com/MaffooBristol))
+* Bug: Re-fixing [#3](https://github.com/Sertion/vscode-gitblame/issues/3)
+* Fix: Updating dependencies
+* Fix: Updated linters means pushing around code
+
+## 4.0.1 (May 13, 2020)
+* Bug: Undocumented change to [StatusBarItem api](https://code.visualstudio.com/api/references/vscode-api#StatusBarItem) [#82](https://github.com/Sertion/vscode-gitblame/issues/82) (Thanks to [Mike MacCana](https://github.com/mikemaccana))
+
+## 4.0.0 (May 01, 2020)
+* Deprecation: Removing all deprecated message token
+* Feature: It is now able to limit the length of the summary [#81](https://github.com/Sertion/vscode-gitblame/issues/81) (Thanks to [Diab Neiroukh](https://github.com/lazerl0rd))
+
+## 3.2.0 (March 09, 2020)
+* Bug: No longer show a critical error when trying to blame a removed file [#78](https://github.com/Sertion/vscode-gitblame/issues/78) (Thanks to [Marius van Witzenburg](https://github.com/mariusvw))
+* Feature: Move to new build system with fewer dependencies
+* Feature: No longer bundle the feature gif into the install package
+
+## 3.1.0 (February 23, 2020)
+* Bug: Extension host crashing when renaming file [#59](https://github.com/Sertion/vscode-gitblame/issues/59) (Thanks to [pierznj](https://github.com/pierznj))
+* Bug: Git command not found [#67](https://github.com/Sertion/vscode-gitblame/issues/67) (Thanks to [MarcMenghin](https://github.com/MarcMenghin))
+* Bug: Documentation or text token is wrong [#70](https://github.com/Sertion/vscode-gitblame/issues/70) (Thanks to [MisLink](https://github.com/MisLink))
+* Feature: Two new tokens for `gitblame.commitUrl` [#61](https://github.com/Sertion/vscode-gitblame/issues/61) (Thanks to [ajoga](https://github.com/ajoga) and [nitzel](https://github.com/nitzel))
+* Fix: Adding [acknowledgements for the logo](https://twitter.com/jasonlong) [#63](https://github.com/Sertion/vscode-gitblame/issues/63) (Thanks to [Eonfge](https://github.com/Eonfge))
+* Fix: Updating readme
+* Fix: Updating dependencies
+
+## 3.0.1 (August 02, 2019)
+* Bug: Removing deprecated token from default value for `gitblame.infoMessageFormat` [#57](https://github.com/Sertion/vscode-gitblame/issues/57) (Thanks to [Kyngo](https://github.com/Kyngo))
+
+## 3.0.0 (July 27, 2019)
+* Bug: Fixing rare bug where swapping between different file views didn't update the view
+* Deprecation: Removing the `.custom` tokens from status bar message
+* Deprecation: The `.from` token now is identical to `.ago`
+* Deprecation: Removing the `commit.filename` tokens from status bar message
+* Deprecation: Removing custom spinner support (`gitblame.progressSpinner`)
+* Deprecation: Removing log levels (`gitblame.logLevel`). It is replaced by `gitblame.logNonCritical` (defaults to `true`)
+* Feature: Blame cache is now tied to the open document. Closing the document will clear if from the cache
+* Feature: Move to the new `@types/vscode` and `vscode-test` packages
+* Feature: Move to new build system
+* Fix: Removing or updating dependencies
+
+The removal of the custom-token allows us to remove our dependency on moment. This lowers the extension bundle size from 850 kb to about 100 kb.
+
+## 2.8.1 (June 21, 2019)
+* Bug: Blaming a removed file crashes the extension [#54](https://github.com/Sertion/vscode-gitblame/pull/54) (Thanks to [dmitriismitnov](https://github.com/dmitriismitnov))
+* Fix: Updating dependencies
+
+## 2.8.0 (June 09, 2019)
+* Feature: Auto detect Atlassian's BitBucket [#52](https://github.com/Sertion/vscode-gitblame/pull/52)
+* Feature: Now display _X years ago_ instead of massive amounts of months [#53](https://github.com/Sertion/vscode-gitblame/pull/53) (Thanks to [radar](https://github.com/radar))
+* Fix: Updating dependencies
+* Fix: New linting rules
+
+### BitBucket Detection
+
+There is a new setting called `gitblame.pluralWebPathSubstrings`. It is an array of strings that, if present in the git origin url, will add an extra _S_ to the online tool url.
+
+## 2.7.0 (March 27, 2019)
+
+* Feature: Copy tool URL or hash to your clipboard with `gitblame.addToolUrlToClipboard` and `gitblame.addCommitHashToClipboard`! (Thanks to [tombusby](https://github.com/tombusby) for the suggestion)
+* Fix: Updating dependencies
+
+## 2.6.3 (November 30, 2018)
+
+* Feature: Add support for using remote URL in blame link expansion [#50](https://github.com/Sertion/vscode-gitblame/pull/50) (Thanks to [allight](https://github.com/allight))
+
+## 2.6.2 (November 27, 2018)
+
+* Fix: Updating dependencies
+
+## 2.6.1 (November 15, 2018)
+
+* Bug: Dot in username generates wrong repository URL [#48](https://github.com/Sertion/vscode-gitblame/pull/48) (Thanks to [bolduz](https://github.com/bolduz))
+
+## 2.6.0 (November 05, 2018)
+
+* Feature: We can now parse a wider variety of git origin formats [#46](https://github.com/Sertion/vscode-gitblame/pull/46)
+* Feature: You can now use your project name in `vscode.commitUrl` [#46](https://github.com/Sertion/vscode-gitblame/pull/46)
+* Fix: Updating dependencies
+
+## 2.5.1 (October 28, 2018)
+
+* Bug: Remote URLs without `.git` are now handled correctly [#44](https://github.com/Sertion/vscode-gitblame/pull/44) (Thanks to [dewe](https://github.com/dewe))
+
+## 2.5.0 (October 21, 2018)
+
+* Bug: Sometimes gitblame blamed the same file multiple time in parallel. This is no longer the case.
+* Feature: We now use `vscode.git` to find your git binary.
+* Fix: Change the default info message format to be prefixed by the date instead of the commit hash. It is in ISO 8601 with dashes for separator.
+* Fix: Updating Readme to better guide Bitbucket users to the `isWebPathPlural` setting.
+* Fix: Removed `internalHashLength` setting. We now always use the whole hash.
+* Fix: Moved back to using [`fs.watch`](https://nodejs.org/api/fs.html#fs_fs_watch_filename_options_listener) from [`workspace.FileSystemWatcher`](https://code.visualstudio.com/docs/extensionAPI/vscode-api#workspace.createFileSystemWatcher) after getting reports that file watching is having issues.
+* Fix: Better handling of ENOENT in git command runner. [#41](https://github.com/Sertion/vscode-gitblame/pull/41) (Thanks to [Yottster](https://github.com/Yottster))
+* Fix: Moved to strict TypeScript.
+* Fix: Updating dependencies
+
+## 2.4.4 (August 01, 2018)
+
+* Fix: Updating dependencies
+
+## 2.4.3 (August 01, 2018)
+
+* Bug: Showing unedited gitblame info template when running *Show quick Info* on un-blameable line (Thanks to [BerndErnst](https://github.com/BerndErnst))
+* Bug: Using a map instead of an object for storing git blame file cache. Now we can blame files named `__proto__` etc.
+
+## 2.4.2 (April 05, 2018)
+
+* Bug: Blame uses committer not author [#29](https://github.com/Sertion/vscode-gitblame/issues/29), [#32](https://github.com/Sertion/vscode-gitblame/issues/32), and [#33](https://github.com/Sertion/vscode-gitblame/issues/33) (Thanks to [HCoban](https://github.com/HCoban), [richardscarrott](https://github.com/richardscarrott), and [KenCoder](https://github.com/KenCoder))
+
+## 2.4.1 (March 30, 2018)
+
+* Bug: defaultWebPath handles plural not correctly [#30](https://github.com/Sertion/vscode-gitblame/issues/30) (Thanks to [HCoban](https://github.com/HCoban))
+
+## 2.4.0 (March 26, 2018)
+
+* Feature: Added `gitblame.isWebPathPlural`. Setting for GitBucket users to help the new auto detect feature. [PR#28](https://github.com/Sertion/vscode-gitblame/pull/28) (Thanks to [dimitarnestorov](https://github.com/dimitarnestorov))
+
+## 2.3.1 (March 24, 2018)
+
+* Fix: Updating Readme
+
+## 2.3.0 (March 24, 2018)
+
+* Feature: Attempting to auto detect if you use a known git web interface [#15](https://github.com/Sertion/vscode-gitblame/issues/15) (Thanks to [@Fidge123](https://github.com/Fidge123), [@sabrehagen](https://github.com/sabrehagen), [@henvic](https://github.com/henvic), and an extra thanks to [@neerolyte](https://github.com/neerolyte))
+* Feature: Added `gitblame.statusBarPositionPriority` for moving the status bar view [#25](https://github.com/Sertion/vscode-gitblame/issues/25) (Thanks to [@jvoigt](https://github.com/jvoigt))
+* Fix: Merging `GitBlame` and `GitBlameController` to `GitBlame`
+* Fix: Renaming `GitBlameFile*` to `GitFile*`
+* Fix: Rewrote all the tests
+* Fix: Updating dependencies
+* Fix: Updating preview video/image
+* Enhancement: Prettifying with [Prettier](https://prettier.io/)
+* Enhancement: Tslintifying with [TSLint](https://palantir.github.io/tslint/)
+
+## 2.2.0 (September 07, 2017)
+
+* Feature: Multiple workspace support [#23](https://github.com/Sertion/vscode-gitblame/issues/23) (Thanks to [@IgorNovozhilov](https://github.com/IgorNovozhilov))
+* Fix: Updating dependencies
+
+## 2.1.0 (August 12, 2017)
+
+* Bug: Keep current line blame info when opening `gitblame.quickInfo`
+* Bug: No longer tells you that your custom git path is incorrect
+* Bug: Supports git paths with spaces in them
+* Feature: Allow for shorter internal git hash storage (`gitblame.internalHashLength`)
+
+## 2.0.2 (July 24, 2017)
+
+* Bug: Spinner will spin forever when there is no repo to be found
+
+## 2.0.1 (July 20, 2017)
+
+* Fix: Moving `git.path` message from `critical` to `error`
+
+## 2.0.0 (July 20, 2017)
+
+This will be updating the major version as we are changing what the exposed command is called.
+
+* Fix: Change name of the command to `gitblame.quickInfo` (was `extension.blame`)
+* Fix: Updating the _Known issues_ link to the new issue tracker as all old issues are resolved
+* Fix: Moved to TypeScript 2.4.1
+* Fix: Cleaning imports
+* Fix: Remove Q&A-section from vscode marketplace
+* Fix: No more `null`
+* Fix: Renamed all interfaces (removed the `I`-prefix)
+* Bug: Only try to blame files in our `workspace.rootPath`
+* Bug: Adding missing _the_ in tooltip
+* Bug: Adding better `dispose` handling
+* Feature: Adding command (`gitblame.blameLink`) for online blame
+* Feature: Adding a fancy _loading spinner_ when waiting for blaming information
+* Feature: Clear the cache of closed files from time to time
+* Feature: Replacing [git-blame](https://github.com/alessioalex/git-blame) with our own `--incremental` based solution
+* Feature: Killing the `git blame` process when requesting a re-blame
+* Feature: Logging when we run commands and what command it was
+* Feature: More informative logging
+* Feature: Time stamps in the log
+* Feature: Adding setting to limit what log levels gets logged
+
+## 1.11.3 (June 15, 2017)
+
+* Bug: Blaming the wrong line [#20](https://github.com/Sertion/vscode-gitblame/issues/20) (Thanks to [@gucong3000](https://github.com/gucong3000))
+
+## 1.11.2 (June 06, 2017)
+
+* Bug: Updating issue link in change log [#19](https://github.com/Sertion/vscode-gitblame/issues/19) (Thanks to [@adambowles](https://github.com/adambowles))
+* Fix: Updating dependencies
+
+## 1.11.1 (June 05, 2017)
+
+* Bug: Singular for single number minutes, hours, and days [#18](https://github.com/Sertion/vscode-gitblame/issues/18) (Thanks to [@adambowles](https://github.com/adambowles))
+
+## 1.11.0 (June 05, 2017)
+
+* Bug: Singular for single number months [#16](https://github.com/Sertion/vscode-gitblame/issues/16) (Thanks to [@adambowles](https://github.com/adambowles))
+* Fix: Adding additional tests for checking `toDateText`
+* Fix: Watching only blamed files
+
+## 1.10.0 (May 21, 2017)
+
+* Feature: Adding support for git submodules [#12](https://github.com/Sertion/vscode-gitblame/issues/12)
+
+## 1.9.0 (May 15, 2017)
+
+* Bug: Fix link in CHANGELOG.md
+* Fix: Moved to TypeScript 2.1.5
+* Bug: Allow for underscore in tokens
+* Fix: Using `async`/`await` where appropriate
+* Bug: Allow token functions do declare default values
+* Fix: Moving editor and document validation to its on file
+* Fix: Moving git repository finding process to its own file
+* Feature: Adding a better tool for handling informative errors to the user
+* Feature: Listening to file changes in the repository and generates new git blame info if an external tool changes a file
+
+## 1.8.2 (May 14, 2017)
+
+* Bug: Fix incorrect version number in CHANGELOG.md [#13](https://github.com/Sertion/vscode-gitblame/pull/13) (Thanks to [@zackschuster](https://github.com/zackschuster))
+* Fix: Removing `typings` directory
+* Feature: Now respects `git.path` (Thanks to [@alessioalex](https://github.com/alessioalex)) [#4](https://github.com/Sertion/vscode-gitblame/issues/4)
+* Feature: Adding short hash token to `infoMessageFormat` and `statusBarMessageFormat` [#10](https://github.com/Sertion/vscode-gitblame/issues/10)
+
+## 1.8.1 (May 01, 2017)
+
+* Bug: Fix incorrect file name in imports [#9](https://github.com/Sertion/vscode-gitblame/issues/9) (Thanks to [@pftbest](https://github.com/pftbest))
+
+## 1.8.0 (May 01, 2017)
+
+* Feature: Customizable status bar message format [#5](https://github.com/Sertion/vscode-gitblame/issues/5)
+* Feature: Customizable `infoMessage` format
+* Enhancement: Updating installation instructions
+
+## 1.7.1 (April 30, 2017)
+
+* Enhancement: Use the same cache for `showMessage` and `view.refresh`
+
+## 1.7.0 (April 30, 2017)
+
+* Feature: Adding setting to ignore whitespace changes (`gitblame.ignoreWhitespace`) [#1](https://github.com/Sertion/vscode-gitblame/issues/1)
+* Feature: Adding setting to open commit info in online tool (`gitblame.commitUrl`) [#6](https://github.com/Sertion/vscode-gitblame/issues/6)
+* Enhancement: Status bar message no longer clickable when there is no commit associated with the current line
+* Enhancement: Adding info about configuration in `README.md`
+* Bug: Spawn fewer git processes when opening a file [#3](https://github.com/Sertion/vscode-gitblame/issues/3)
+
+## 1.6.2 (April 29, 2017)
+
+* Updating example animation
+* Removing backlog from `README.md`, it is now the [`Planned` label in the issue tracker](https://github.com/Sertion/vscode-gitblame/labels/Planned)
+
+## 1.6.1 (April 29, 2017)
+
+* Split change log into its own file as per [suggestion from @daniel-white](https://github.com/waderyan/vscode-gitblame/issues/30)
+
+## 1.6.0 (April 17, 2017)
+
+* More granular time info
+* Adding a re-check of blame info on save
+
+## 1.5.0 (April 17, 2017)
+
+* Spring cleaning
+
+## 1.4.0 (April 16, 2017)
+
+* Now respects changes made in the git working tree when blaming
+* Updating dependencies
+* Updating to new repository
+
+## 1.3.0 (July 21, 2016)
+
+* Merged in [PR](https://github.com/waderyan/vscode-gitblame/pull/12) to make the status bar message interactive (credit to [@j-em](https://github.com/j-em));
+
+## 1.2.0 (July 20, 2016)
+
+* Merged in [PR](https://github.com/waderyan/vscode-gitblame/pull/10) replacing 'Hello World' message with hash and commit message (credit to [@carloscz](https://github.com/carloscz)).
+
+## 1.1.0 (May 20, 2016)
+
+* Reduced text size which was causing the blame info not to show.
+* Merged in [PR](https://github.com/waderyan/vscode-gitblame/pull/5) (credit to [@fogzot](https://github.com/fogzot)) that searches for .git in parent dirs.
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 00000000..73219c58
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,123 @@
+# Change Log
+
+## 11.1.4 (June 25, 2025)
+* Bug: Extension causes high cpu load [#191](https://github.com/Sertion/vscode-gitblame/pull/191). Thanks to [Martin-vH](https://github.com/Martin-vH) and [NaterGator](https://github.com/NaterGator)!
+* Fix: Updating dependencies
+
+## 11.1.3 (May 29, 2025)
+* Bug:  Update remote URL parsing to support non-`git` ssh user [#189](https://github.com/Sertion/vscode-gitblame/pull/189). Thanks to [Nadock](https://github.com/Nadock)!
+* Fix: Updating dependencies
+
+## 11.1.2 (February 14, 2025)
+* Bug: Fix gitorigin.port error for http(s) [#188](https://github.com/Sertion/vscode-gitblame/pull/188). Thanks to [yuanliuu](https://github.com/yuanliuu)!
+* Fix: Updating dependencies
+* Fix: Update CHANGELOG.md format
+
+## 11.1.1 (October 10, 2024)
+* Fix: Fix for Default value of gitblame.commitUrl [#182](https://github.com/Sertion/vscode-gitblame/pull/182). Thanks to [Kovbas](https://github.com/Kovbas)!
+* Fix: Split the changelog into two files. Old changes live in [CHANGELOG OLD.md](./CHANGELOG%20OLD.md)
+* FIX: Update release date for 11.1.0.
+
+## 11.1.0 (October 03, 2024)
+* Feature: Adding a new setting that modifies the `user.name` and `committer.name` token: `gitblame.currentUserAlias`. [#181](https://github.com/Sertion/vscode-gitblame/issues/181). Thanks to [Antecer](https://github.com/Antecer)!
+ * When set its value will be printed instead of the name when the commit author or committer email matches the current git config `user.email`.
+* Fix: Updating dependencies
+
+## 11.0.2 (July 17, 2024)
+* Feature: Adding logging behind the `debug` and `trace` levels.
+* Fix: Updating dependencies
+
+## 11.0.1 (May 29, 2024)
+* Bug: Environment parameters are no longer sent to git commands [#174](https://github.com/Sertion/vscode-gitblame/issues/174). Thanks to [Martijn Hols](https://github.com/MartijnHols), [amc6](https://github.com/amc6), and especially [Alex Neo](https://github.com/alexneo2003)!
+
+## 11.0.0 (May 21, 2024)
+* Feature: Will no longer blame files with more lines than `gitblame.maxLineCount` (default `16384`) [#172](https://github.com/Sertion/vscode-gitblame/issues/172). Thanks to [webextensions](https://github.com/webextensions)!
+  * Fun fact: This number was selected as it is the last power of 2 lower than 20000. It has no other significance.
+
+## 10.11.0 (May 09, 2024)
+* Feature: *Copy commit hash on info message click [#171](https://github.com/Sertion/vscode-gitblame/issues/171)*. Thanks to [Harsh](https://github.com/harshbhatt)!
+* Fix: Updating dependencies
+
+## 10.10.1 (April 30, 2024)
+* Bug: *Status Bar block disappears after a change in extension settings [#155](https://github.com/Sertion/vscode-gitblame/issues/155)*. Thanks to [ADTC](https://github.com/ADTC)!
+* Fix: Updating dependencies
+
+## 10.10.0 (March 12, 2024)
+* Feature: Update extension to support [the future move to SHA-256 hashes for object names](https://github.com/git/git/blob/70661d28/Documentation/technical/hash-function-transition.txt)
+* Fix: Change from `rome` to `@biomejs/biome`
+* Fix: Updating dependencies
+
+## 10.9.0 (February 06, 2024)
+* Feature: Moving "Waiting for git blame response" to the tool tip and replacing the spinning icon with a non-spinning version while waiting for blame information for a line. [#167](https://github.com/Sertion/vscode-gitblame/issues/163) Thanks to [Benjamin Pasero](https://github.com/bpasero) and [Jasper Trooster](https://github.com/Japsert)!
+
+## 10.8.0 (February 02, 2024)
+* Feature: *Define a custom theme color for the inline message [#168](https://github.com/Sertion/vscode-gitblame/issues/168)*. Thanks to [Johannes Rieken ](https://github.com/jrieken)!
+  * You can [customise the inline blame message by overriding `gitblame.inlineMessage`](https://code.visualstudio.com/docs/getstarted/themes#_customizing-a-color-theme)
+
+## 10.7.1 (January 24, 2024)
+* Bug: *Incorrect issues URL in changelog [#166](https://github.com/Sertion/vscode-gitblame/issues/166)*. Thanks to [Minobi](https://github.com/Minobi)!
+
+## 10.7.0 (January 20, 2024)
+* Feature: New setting `gitblame.revsFile` [#165](https://github.com/Sertion/vscode-gitblame/issues/165) Thanks to [mpawlowski-eyeo](https://github.com/mpawlowski-eyeo)!
+* Fix: Updating dependencies
+
+## 10.6.0 (December 11, 2023)
+* Bug/Breaking: *`gitblame.delayBlame` triggers for each character typed [#160](https://github.com/Sertion/vscode-gitblame/issues/160)*. Thanks to [redactedscribe](https://github.com/redactedscribe)!
+* Fix: Updating dependencies
+
+## 10.5.1 (September 24, 2023)
+* Bug: *TypeError: Cannot read properties of undefined (reading 'toLowerCase') [#155](https://github.com/Sertion/vscode-gitblame/issues/155)*. Thanks to [Andre Figueiredo](https://github.com/andretf)!
+* Fix: Builds now target ES2022
+* Fix: Updating dependencies
+
+## 10.5.0 (August 25, 2023)
+* Known issue: `gitblame.gitShow` does not work in some shells
+* Fix: Use absolute path to determine when a git repository HEAD changes
+* Fix: Respect the `gitblame.parallelBlames` settings after a vscode relaunch
+* Fix: Updating dependencies
+* Fix: Updated README.md
+
+## 10.4.0 (July 08, 2023)
+* Feature: Open `git show` for the commit on the last selected line in the terminal from the info message or from the `gitblame.gitShow` command
+  * It is also possible to change the status bar button default behavor to run git show by changing `gitblame.statusBarMessageClickAction` to `"Open git show"`
+* Bug: Attempting to fix *Extension causes high cpu load ([#145](https://github.com/Sertion/vscode-gitblame/issues/145))*. Thanks to [joshrbarcodefactory](https://github.com/joshrbarcodefactory) for uploading the CPU snapshot!
+* Fix: Updating dependencies
+* Fix: Updated README.md
+
+## 10.3.0 (June 23, 2023)
+* Bug: Spawns many Git processes and uses up CPU ([#144](https://github.com/Sertion/vscode-gitblame/issues/144)). Thanks to [Theo Crandall](https://github.com/thrandale)!
+  * New setting `gitblame.parallelBlames` controls how many git blame processes that will run in parallel. Defaults to `2`.
+* Bug: Blame completing can sometimes update the status bar to info from the wrong file
+* Fix: Fewer dev dependencies
+
+## 10.2.1 (June 21, 2023)
+* Bug: Inline message drawn on top of code when backspacing from EOL ([#142](https://github.com/Sertion/vscode-gitblame/issues/142)). Thanks to [Kim Alford](https://github.com/kgalford1)!
+* Fix: Hide blame decorator saving a file
+* Fix: Updated unclear error message
+* Fix: Added `${project.defaultbranch}` to the readme
+* Fix: Updating dependencies
+
+## 10.2.0 (April 8, 2023)
+* Feature: Add a delay before blame is shown ([#139](https://github.com/Sertion/vscode-gitblame/issues/139)) [#141](https://github.com/Sertion/vscode-gitblame/pull/141). Thanks to [Ajith Aravind](https://github.com/aaravind100) for the PR and request!
+  * Set `gitblame.delayBlame` to add a timeout from when navigating to a line to when the blame information is shown
+* Feature:  Master or Main [#134](https://github.com/Sertion/vscode-gitblame/issues/134). Thanks to [Pierre Hersant](https://github.com/elcortez)!
+  * New token for `gitblame.commitUrl`: `${project.defaultbranch}` - The current projects default branch
+* Fix: Updating dependencies
+
+## 10.1.0 (December 19, 2022)
+* Feature: Now uses the new [LogOutputChannel](https://code.visualstudio.com/api/references/vscode-api#LogOutputChannel)
+* Fix: Removing unused setting from README.md. Thanks to [RAZINJ](https://github.com/razinj) for the [PR](https://github.com/Sertion/vscode-gitblame/pull/136)!
+* Fix: Updating dependencies
+
+## 10.0.0 (November 23, 2022)
+* Feature: Inline view of gitblame [#94](https://github.com/Sertion/vscode-gitblame/issues/94). Thanks to [f4n0](https://github.com/f4n0) for the PR and [kramarz89](https://github.com/kramarz89) for the request!
+  * `gitblame.inlineMessageEnabled` to enable the feature
+  * `gitblame.inlineMessageFormat` to configure the message format
+  * `gitblame.inlineMessageNoCommit` to set the _no commit_ message
+  * `gitblame.inlineMessageMargin` to configure the distance between the end of the line and the blame info
+  * Use the same token as for `gitblame.statusBarMessageFormat`
+* Fix: Updating dependencies
+
+# Older Changes
+
+For older changes please see [CHANGELOG OLD.md](./CHANGELOG%20OLD.md).
diff --git a/README.md b/README.md
index 27232c55..eb69fbb5 100644
--- a/README.md
+++ b/README.md
@@ -1,37 +1,172 @@
 # Git Blame
 
-See Git Blame information in the status bar for the currently selected line. 
+Features:
+* See Git Blame information in the status bar for the currently selected line.
+* See Git Blame information on the last selected line in your editor.
+* Quick link to open the latest commit on the current line in the most popular online git tools.
+* Open `git show` for the latest commit on the current line in an vscode terminal.
 
-![Feature Usage](https://github.com/wadeanderson7/vscode-gitblame/raw/master/images/GitBlamePreview.gif)
+## How to use
 
-# Install
+![Feature Usage](https://raw.githubusercontent.com/Sertion/vscode-gitblame/master/images/preview.png)
 
-Open up VS Code.
+Git blame adds git blame information to your vscode compatible view. See information about what commit last changed a line and how long ago it was. Click the message to see more information about the commit. It is possible to edit all of these information messages in the settings. There are multiple tokens available. These are described below.
 
-1. Type `F1`
-2. Type `ext` in command palette.
-3. Select "install" and hit enter.
-4. Type "blame"
-5. Select "Git Blame" extension and hit enter. 
+Git Blame works very well with WSL but does not work with the web browser based vscode compatible editors.
 
-# Backlog
+## Configuration
 
-* Click on the status bar to see more blame info, [including commit SHA](https://github.com/waderyan/vscode-gitblame/issues/3)
-* [Show blame line ranges](https://github.com/waderyan/vscode-gitblame/issues/1)
+### `gitblame.commitUrl`
+> Type: `string`
 
-# [Known Issues](https://github.com/waderyan/vscode-gitblame/issues)
+> Default value: `"${tool.protocol}//${gitorigin.hostname}${gitorigin.port}${gitorigin.path}${tool.commitpath}${hash}"`
 
-# Update Log
+Url where you can see the commit by hash
 
-Version 1.1
+If set to an empty value it will try to guess the URL based on your remote origin. Can only support servers that don't require auth.
 
-* Reduced text size which was causing the blame info not to show. 
-* Merged in [PR](https://github.com/waderyan/vscode-gitblame/pull/5) (credit to [@fogzot](https://github.com/fogzot)) that searches for .git in parent dirs.
+Available tokens:
+* `${hash}` - the commit hash
+* `${file.path}` - path to the final file
+* `${file.path.result}` - path to the final file
+* `${file.path.source}` - path to the original file
+* `${file.line}` - the line number of the line in the final file
+* `${file.line.result}` - the line number of the line in the final file
+* `${file.line.source}` - the line number of the line in the original file
+* `${project.defaultbranch}` - The current projects default branch
+* `${project.name}` - your project name (e.g. `project_name` in `https://github.com/user/project_name.git`)
+* `${project.remote}` - the current default remote's URL with the protocol, port-specifiers, and trailing `.git` stripped. (e.g. `github.com/user/project_name` in `https://github.com/user/project_name.git`)
+* `${gitorigin.hostname}` - the git origin domain (e.g. `github.com` in `https://github.com/ckb-next/ckb-next.git`)
+* `${gitorigin.hostname,n}` - the nth part of the git origin domain (e.g. if the git origin is `https://github.com/ckb-next/ckb-next.git` `${gitorigin.hostname,1}` will return `com`)
+* `${gitorigin.path}` - the git origin path (e.g. `/ckb-next/ckb-next.git` in `https://github.com/ckb-next/ckb-next.git`)
+* `${gitorigin.path,n}` - the nth part of the git origin path (e.g. if the git origin is `https://github.com/ckb-next/ckb-next.git` `${gitorigin.path,1}` will return `ckb-next.git`)
+* `${gitorigin.port}` - the git origin port (if it uses http/https) including prefixed `:`
+* `${tool.protocol}` - `http:` or `https:`
+* `${tool.commitpath}` - `/commit/` or `/commits`
 
-Version 1.2
+### `gitblame.pluralWebPathSubstrings`
+> Type: `string[]`
 
-* Merged in [PR](https://github.com/waderyan/vscode-gitblame/pull/10) replacing 'Hello World' message with hash and commit message (credit to [@carloscz](https://github.com/carloscz)). 
+> Default value: `["bitbucket", "atlassian"]`
 
-Version 1.3
+An array of substrings that, when present in the git origin URL, replaces _commit_ with _commits_ in the `gitblame.commitUrl` token `tool.commitpath`. Set the value to something that matches anything to recreate the old `gitblame.isWebPathPlural`-setting.
 
-* Merged in [PR](https://github.com/waderyan/vscode-gitblame/pull/12) to make the status bar message interactive (credit to [@j-em](https://github.com/j-em));
+### `gitblame.ignoreWhitespace`
+> Type: `boolean`
+
+> Default value: `false`
+
+Use the git blame `-w` flag.
+
+### `gitblame.infoMessageFormat`
+> Type: `string`
+
+> Default value: `"${commit.hash} ${commit.summary}"`
+
+Message that appears when the `gitblame.quickInfo` command executes (when you click the status bar message).
+
+### `gitblame.statusBarMessageFormat`
+> Type: `string`
+
+> Default value: `"Blame ${author.name} (${time.ago})"`
+
+Message in the status bar about the current line's git blame commit. (Available tokens)[#message-tokens].
+
+### `gitblame.statusBarMessageNoCommit`
+> Type: `string`
+
+> Default value: `"Not Committed Yet"`
+
+Message in the status bar about the current line when no commit can be found. _No available tokens_.
+
+### `gitblame.statusBarPositionPriority`
+> Type: `number`
+
+> Default value: `500`
+
+Priority where the status bar view should be placed. Higher value should be placed further to the left.
+
+### `gitblame.inlineMessageFormat`
+> Type: `string`
+
+> Default value: `"Blame ${author.name} (${time.ago})"`
+
+Message on the current line in the editor about the line's git blame commit. (Available tokens)[#message-tokens].
+
+### `gitblame.inlineMessageNoCommit`
+> Type: `string`
+
+> Default value: `"Not Committed Yet"`
+
+Message on the current line when no commit can be found. _No available tokens_.
+
+### `gitblame.inlineMessageEnabled`
+> Type: `boolean`
+
+> Default value: `false`
+
+To enable the inline git blame view. Shows blame information at the end of the current line if available.
+
+### `gitblame.inlineMessageMargin`
+> Type: `number`
+
+> Default value: `2`
+
+The amount of margin between line and inline blame view
+
+### `gitblame.currentUserAlias`
+> Type: `string` or `null`
+
+> Default value: `null`
+
+Replaces `${author.name}` and `${committer.name}` when the git config `user.email` matches the author's or committer's email address.
+
+### `gitblame.delayBlame`
+> Type: `number`
+
+> Default value: `0`
+
+This setting adds a delay (in milliseconds) before the blame is displayed
+
+### `gitblame.parallelBlames`
+> Type: `number`
+
+> Default value: `2`
+
+Limit how many git blame processes the extension can run in parallel. This can help with high CPU usage.
+
+### `gitblame.revsFile`
+> Type: `string[]`
+
+> Default value: `[]`
+
+List of refs-file names to look for relative to the closest `.git`-folder. The first file in the list that is [accessible](https://nodejs.org/docs/latest-v18.x/api/fs.html#fspromisesaccesspath-mode) will be used with the [`-S` argument](https://git-scm.com/docs/git-blame#Documentation/git-blame.txt--Sltrevs-filegt) in `git blame`.
+
+### Message Tokens
+
+| Token                         | Function | Parameter | Default Value | Description |
+|-------------------------------|----------|-----------|---------------|-------------|
+| `${commit.hash,length}`       | Yes      | `length`  |            64 | the first `length` characters of the 40-bit (or 64-bit) hash unique to the commit |
+| `${commit.hash_short,length}` | Yes      | `length`  |             7 | the first `length` characters of the 40-bit (or 64-bit) hash unique to the commit |
+| `${commit.summary}`           | Yes      | `length`  |         65536 | the first `length` characters of the first line of the commit message |
+| `${author.name}`              | No       | -         | -             | the commit author's name |
+| `${author.mail}`              | No       | -         | -             | the commit author's e-mail |
+| `${author.timestamp}`         | No       | -         | -             | timestamp for the commit author's commit |
+| `${author.tz}`                | No       | -         | -             | the commit author's time zone |
+| `${author.date}`              | No       | -         | -             | the commit author's date (ex: 1990-09-16) |
+| `${committer.name}`           | No       | -         | -             | the committer's name |
+| `${committer.mail}`           | No       | -         | -             | the committer's e-mail |
+| `${committer.timestamp}`      | No       | -         | -             | timestamp for the committer's commit |
+| `${committer.tz}`             | No       | -         | -             | the committer's time zone |
+| `${committer.date}`           | No       | -         | -             | the committer's date (ex: 1990-09-16) |
+| `${time.ago}`                 | No       | -         | -             | displays an estimation of how long ago the author committed (e.g. `10 hours ago`, `20 days ago`, `4 months ago`) |
+| `${time.c_ago}`               | No       | -         | -             | displays an estimation of how long ago the committer committed (e.g. `10 hours ago`, `20 days ago`, `4 months ago`) |
+
+## Known issues
+### The `gitblame.gitShow` command does not work with _my shell_
+
+If your default terminal profile is set to `cmd.exe` `gitblame.gitShow` will not function correctly. Fix this by using a unix compatible shell.
+
+## Acknowledgements
+
+* Logo by [Jason Long](https://twitter.com/jasonlong).
diff --git a/biome.json b/biome.json
new file mode 100644
index 00000000..776fc86f
--- /dev/null
+++ b/biome.json
@@ -0,0 +1,28 @@
+{
+	"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
+	"files": {
+		"includes": ["src/**/*", "test/**/*"]
+	},
+	"assist": { "actions": { "source": { "organizeImports": "on" } } },
+	"linter": {
+		"enabled": true,
+		"rules": {
+			"recommended": true,
+			"style": {
+				"noParameterAssign": "error",
+				"useAsConstAssertion": "error",
+				"useDefaultParameterLast": "error",
+				"useEnumInitializers": "error",
+				"useSelfClosingElements": "error",
+				"useSingleVarDeclarator": "error",
+				"noUnusedTemplateLiteral": "error",
+				"useNumberNamespace": "error",
+				"noInferrableTypes": "error",
+				"noUselessElse": "error"
+			},
+			"suspicious": {
+				"noTemplateCurlyInString": "off"
+			}
+		}
+	}
+}
diff --git a/esbuild.mjs b/esbuild.mjs
new file mode 100644
index 00000000..3a3da2b5
--- /dev/null
+++ b/esbuild.mjs
@@ -0,0 +1,19 @@
+import { build } from "esbuild";
+
+await build({
+	entryPoints: ["./src/index.ts"],
+	bundle: true,
+	format: "cjs",
+	minify: true,
+	target: "node20.9",
+	outfile: "./out/src/index.js",
+	external: [
+		"vscode",
+		"node:child_process",
+		"node:fs",
+		"node:fs/promises",
+		"node:path",
+		"node:url",
+		"node:util",
+	],
+});
diff --git a/gitblame-1.0.0.vsix b/gitblame-1.0.0.vsix
deleted file mode 100644
index ff45796d..00000000
Binary files a/gitblame-1.0.0.vsix and /dev/null differ
diff --git a/images/GitBlamePreview.gif b/images/GitBlamePreview.gif
deleted file mode 100644
index 76b8642b..00000000
Binary files a/images/GitBlamePreview.gif and /dev/null differ
diff --git a/images/git_icon.png b/images/git_icon.png
index 482bfa2e..85345a67 100644
Binary files a/images/git_icon.png and b/images/git_icon.png differ
diff --git a/images/preview.png b/images/preview.png
new file mode 100644
index 00000000..c8b2879f
Binary files /dev/null and b/images/preview.png differ
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 00000000..7cf606fe
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,2445 @@
+{
+  "name": "gitblame",
+  "version": "11.1.4",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "gitblame",
+      "version": "11.1.4",
+      "license": "MIT",
+      "devDependencies": {
+        "@biomejs/biome": "2.0.5",
+        "@types/mocha": "10.0.10",
+        "@types/node": "20.14.6",
+        "@types/sinon": "17.0.4",
+        "@types/sinonjs__fake-timers": "8.1.5",
+        "@types/vscode": "1.91.0",
+        "@vscode/test-electron": "2.5.2",
+        "esbuild": "0.25.5",
+        "mocha": "11.7.1.0",
+        "sinon": "21.0.0",
+        "typescript": "5.8.3"
+      },
+      "engines": {
+        "node": ">=20.9.0",
+        "vscode": ">=1.91.0"
+      }
+    },
+    "node_modules/@biomejs/biome": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.0.5.tgz",
+      "integrity": "sha512-MztFGhE6cVjf3QmomWu83GpTFyWY8KIcskgRf2AqVEMSH4qI4rNdBLdpAQ11TNK9pUfLGz3IIOC1ZYwgBePtig==",
+      "dev": true,
+      "license": "MIT OR Apache-2.0",
+      "bin": {
+        "biome": "bin/biome"
+      },
+      "engines": {
+        "node": ">=14.21.3"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/biome"
+      },
+      "optionalDependencies": {
+        "@biomejs/cli-darwin-arm64": "2.0.5",
+        "@biomejs/cli-darwin-x64": "2.0.5",
+        "@biomejs/cli-linux-arm64": "2.0.5",
+        "@biomejs/cli-linux-arm64-musl": "2.0.5",
+        "@biomejs/cli-linux-x64": "2.0.5",
+        "@biomejs/cli-linux-x64-musl": "2.0.5",
+        "@biomejs/cli-win32-arm64": "2.0.5",
+        "@biomejs/cli-win32-x64": "2.0.5"
+      }
+    },
+    "node_modules/@biomejs/cli-darwin-arm64": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.0.5.tgz",
+      "integrity": "sha512-VIIWQv9Rcj9XresjCf3isBFfWjFStsdGZvm8SmwJzKs/22YQj167ge7DkxuaaZbNf2kmYif0AcjAKvtNedEoEw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT OR Apache-2.0",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=14.21.3"
+      }
+    },
+    "node_modules/@biomejs/cli-darwin-x64": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.0.5.tgz",
+      "integrity": "sha512-DRpGxBgf5Z7HUFcNUB6n66UiD4VlBlMpngNf32wPraxX8vYU6N9cb3xQWOXIQVBBQ64QfsSLJnjNu79i/LNmSg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT OR Apache-2.0",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=14.21.3"
+      }
+    },
+    "node_modules/@biomejs/cli-linux-arm64": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.0.5.tgz",
+      "integrity": "sha512-FQTfDNMXOknf8+g9Eede2daaduRjTC2SNbfWPNFMadN9K3UKjeZ62jwiYxztPaz9zQQsZU8VbddQIaeQY5CmIA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT OR Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=14.21.3"
+      }
+    },
+    "node_modules/@biomejs/cli-linux-arm64-musl": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.0.5.tgz",
+      "integrity": "sha512-OpflTCOw/ElEs7QZqN/HFaSViPHjAsAPxFJ22LhWUWvuJgcy/Z8+hRV0/3mk/ZRWy5A6fCDKHZqAxU+xB6W4mA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT OR Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=14.21.3"
+      }
+    },
+    "node_modules/@biomejs/cli-linux-x64": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.0.5.tgz",
+      "integrity": "sha512-znpfydUDPuDkyBTulnODrQVK2FaG/4hIOPcQSsF2GeauQOYrBAOplj0etGB0NUrr0dFsvaQ15nzDXYb60ACoiw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT OR Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=14.21.3"
+      }
+    },
+    "node_modules/@biomejs/cli-linux-x64-musl": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.0.5.tgz",
+      "integrity": "sha512-9lmjCnajAzpZXbav2P6D87ugkhnaDpJtDvOH5uQbY2RXeW6Rq18uOUltxgacGBP+d8GusTr+s3IFOu7SN0Ok8g==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT OR Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=14.21.3"
+      }
+    },
+    "node_modules/@biomejs/cli-win32-arm64": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.0.5.tgz",
+      "integrity": "sha512-CP2wKQB+gh8HdJTFKYRFETqReAjxlcN9AlYDEoye8v2eQp+L9v+PUeDql/wsbaUhSsLR0sjj3PtbBtt+02AN3A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT OR Apache-2.0",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=14.21.3"
+      }
+    },
+    "node_modules/@biomejs/cli-win32-x64": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.0.5.tgz",
+      "integrity": "sha512-Sw3rz2m6bBADeQpr3+MD7Ch4E1l15DTt/+dfqKnwkm3cn4BrYwnArmvKeZdVsFRDjMyjlKIP88bw1r7o+9aqzw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT OR Apache-2.0",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=14.21.3"
+      }
+    },
+    "node_modules/@esbuild/aix-ppc64": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
+      "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz",
+      "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz",
+      "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz",
+      "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz",
+      "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz",
+      "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz",
+      "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz",
+      "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz",
+      "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz",
+      "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz",
+      "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz",
+      "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz",
+      "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz",
+      "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz",
+      "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz",
+      "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz",
+      "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-arm64": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz",
+      "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz",
+      "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-arm64": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz",
+      "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz",
+      "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz",
+      "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz",
+      "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz",
+      "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz",
+      "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@isaacs/cliui": {
+      "version": "8.0.2",
+      "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+      "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "string-width": "^5.1.2",
+        "string-width-cjs": "npm:string-width@^4.2.0",
+        "strip-ansi": "^7.0.1",
+        "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+        "wrap-ansi": "^8.1.0",
+        "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@pkgjs/parseargs": {
+      "version": "0.11.0",
+      "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+      "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/@sinonjs/commons": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz",
+      "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "type-detect": "4.0.8"
+      }
+    },
+    "node_modules/@sinonjs/fake-timers": {
+      "version": "13.0.5",
+      "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz",
+      "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@sinonjs/commons": "^3.0.1"
+      }
+    },
+    "node_modules/@sinonjs/samsam": {
+      "version": "8.0.2",
+      "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.2.tgz",
+      "integrity": "sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@sinonjs/commons": "^3.0.1",
+        "lodash.get": "^4.4.2",
+        "type-detect": "^4.1.0"
+      }
+    },
+    "node_modules/@sinonjs/samsam/node_modules/type-detect": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz",
+      "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/@types/mocha": {
+      "version": "10.0.10",
+      "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz",
+      "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/node": {
+      "version": "20.14.6",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.6.tgz",
+      "integrity": "sha512-JbA0XIJPL1IiNnU7PFxDXyfAwcwVVrOoqyzzyQTyMeVhBzkJVMSkC1LlVsRQ2lpqiY4n6Bb9oCS6lzDKVQxbZw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~5.26.4"
+      }
+    },
+    "node_modules/@types/sinon": {
+      "version": "17.0.4",
+      "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.4.tgz",
+      "integrity": "sha512-RHnIrhfPO3+tJT0s7cFaXGZvsL4bbR3/k7z3P312qMS4JaS2Tk+KiwiLx1S0rQ56ERj00u1/BtdyVd0FY+Pdew==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/sinonjs__fake-timers": "*"
+      }
+    },
+    "node_modules/@types/sinonjs__fake-timers": {
+      "version": "8.1.5",
+      "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz",
+      "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/vscode": {
+      "version": "1.91.0",
+      "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.91.0.tgz",
+      "integrity": "sha512-PgPr+bUODjG3y+ozWUCyzttqR9EHny9sPAfJagddQjDwdtf66y2sDKJMnFZRuzBA2YtBGASqJGPil8VDUPvO6A==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@vscode/test-electron": {
+      "version": "2.5.2",
+      "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.5.2.tgz",
+      "integrity": "sha512-8ukpxv4wYe0iWMRQU18jhzJOHkeGKbnw7xWRX3Zw1WJA4cEKbHcmmLPdPrPtL6rhDcrlCZN+xKRpv09n4gRHYg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "http-proxy-agent": "^7.0.2",
+        "https-proxy-agent": "^7.0.5",
+        "jszip": "^3.10.1",
+        "ora": "^8.1.0",
+        "semver": "^7.6.2"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/ansi-regex": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
+      "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+      }
+    },
+    "node_modules/ansi-styles": {
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+      "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/brace-expansion": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+      "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/browser-stdout": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
+      "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/chalk": {
+      "version": "5.4.1",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
+      "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^12.17.0 || ^14.13 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
+      }
+    },
+    "node_modules/chokidar": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
+      "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "readdirp": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 14.16.0"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/cli-cursor": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
+      "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "restore-cursor": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/cli-spinners": {
+      "version": "2.9.2",
+      "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
+      "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/cliui": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+      "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "string-width": "^4.2.0",
+        "strip-ansi": "^6.0.1",
+        "wrap-ansi": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/cliui/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/cliui/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/cliui/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/cliui/node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/cliui/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/cliui/node_modules/wrap-ansi": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/cross-spawn": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+      "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/debug": {
+      "version": "4.3.5",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",
+      "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ms": "2.1.2"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/debug/node_modules/ms": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/diff": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz",
+      "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.3.1"
+      }
+    },
+    "node_modules/eastasianwidth": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+      "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/emoji-regex": {
+      "version": "9.2.2",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+      "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/esbuild": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz",
+      "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.25.5",
+        "@esbuild/android-arm": "0.25.5",
+        "@esbuild/android-arm64": "0.25.5",
+        "@esbuild/android-x64": "0.25.5",
+        "@esbuild/darwin-arm64": "0.25.5",
+        "@esbuild/darwin-x64": "0.25.5",
+        "@esbuild/freebsd-arm64": "0.25.5",
+        "@esbuild/freebsd-x64": "0.25.5",
+        "@esbuild/linux-arm": "0.25.5",
+        "@esbuild/linux-arm64": "0.25.5",
+        "@esbuild/linux-ia32": "0.25.5",
+        "@esbuild/linux-loong64": "0.25.5",
+        "@esbuild/linux-mips64el": "0.25.5",
+        "@esbuild/linux-ppc64": "0.25.5",
+        "@esbuild/linux-riscv64": "0.25.5",
+        "@esbuild/linux-s390x": "0.25.5",
+        "@esbuild/linux-x64": "0.25.5",
+        "@esbuild/netbsd-arm64": "0.25.5",
+        "@esbuild/netbsd-x64": "0.25.5",
+        "@esbuild/openbsd-arm64": "0.25.5",
+        "@esbuild/openbsd-x64": "0.25.5",
+        "@esbuild/sunos-x64": "0.25.5",
+        "@esbuild/win32-arm64": "0.25.5",
+        "@esbuild/win32-ia32": "0.25.5",
+        "@esbuild/win32-x64": "0.25.5"
+      }
+    },
+    "node_modules/escalade": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+      "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/escape-string-regexp": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+      "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/find-up": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+      "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "locate-path": "^6.0.0",
+        "path-exists": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/find-up/node_modules/locate-path": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+      "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-locate": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/find-up/node_modules/p-limit": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+      "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "yocto-queue": "^0.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/find-up/node_modules/p-locate": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+      "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-limit": "^3.0.2"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/find-up/node_modules/path-exists": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+      "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/find-up/node_modules/yocto-queue": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+      "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/foreground-child": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+      "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "cross-spawn": "^7.0.6",
+        "signal-exit": "^4.0.1"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/get-caller-file": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+      "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": "6.* || 8.* || >= 10.*"
+      }
+    },
+    "node_modules/get-east-asian-width": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz",
+      "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/glob": {
+      "version": "10.4.5",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
+      "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "foreground-child": "^3.1.0",
+        "jackspeak": "^3.1.2",
+        "minimatch": "^9.0.4",
+        "minipass": "^7.1.2",
+        "package-json-from-dist": "^1.0.0",
+        "path-scurry": "^1.11.1"
+      },
+      "bin": {
+        "glob": "dist/esm/bin.mjs"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/he": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+      "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "he": "bin/he"
+      }
+    },
+    "node_modules/http-proxy-agent": {
+      "version": "7.0.2",
+      "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+      "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "^7.1.0",
+        "debug": "^4.3.4"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/http-proxy-agent/node_modules/agent-base": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz",
+      "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.3.4"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/https-proxy-agent": {
+      "version": "7.0.5",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz",
+      "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "^7.0.2",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/https-proxy-agent/node_modules/agent-base": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz",
+      "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.3.4"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-interactive": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz",
+      "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/is-unicode-supported": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
+      "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/jackspeak": {
+      "version": "3.4.3",
+      "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+      "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "@isaacs/cliui": "^8.0.2"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      },
+      "optionalDependencies": {
+        "@pkgjs/parseargs": "^0.11.0"
+      }
+    },
+    "node_modules/js-yaml": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+      "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "argparse": "^2.0.1"
+      },
+      "bin": {
+        "js-yaml": "bin/js-yaml.js"
+      }
+    },
+    "node_modules/js-yaml/node_modules/argparse": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+      "dev": true,
+      "license": "Python-2.0"
+    },
+    "node_modules/jszip": {
+      "version": "3.10.1",
+      "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
+      "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
+      "dev": true,
+      "license": "(MIT OR GPL-3.0-or-later)",
+      "dependencies": {
+        "lie": "~3.3.0",
+        "pako": "~1.0.2",
+        "readable-stream": "~2.3.6",
+        "setimmediate": "^1.0.5"
+      }
+    },
+    "node_modules/jszip/node_modules/core-util-is": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+      "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/jszip/node_modules/immediate": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+      "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/jszip/node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/jszip/node_modules/isarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+      "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/jszip/node_modules/lie": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
+      "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "immediate": "~3.0.5"
+      }
+    },
+    "node_modules/jszip/node_modules/pako": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+      "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
+      "dev": true,
+      "license": "(MIT AND Zlib)"
+    },
+    "node_modules/jszip/node_modules/process-nextick-args": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+      "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/jszip/node_modules/readable-stream": {
+      "version": "2.3.8",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+      "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "core-util-is": "~1.0.0",
+        "inherits": "~2.0.3",
+        "isarray": "~1.0.0",
+        "process-nextick-args": "~2.0.0",
+        "safe-buffer": "~5.1.1",
+        "string_decoder": "~1.1.1",
+        "util-deprecate": "~1.0.1"
+      }
+    },
+    "node_modules/jszip/node_modules/safe-buffer": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/jszip/node_modules/setimmediate": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
+      "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/jszip/node_modules/string_decoder": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+      "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "safe-buffer": "~5.1.0"
+      }
+    },
+    "node_modules/jszip/node_modules/util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/lodash.get": {
+      "version": "4.4.2",
+      "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
+      "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/log-symbols": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
+      "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "chalk": "^4.1.0",
+        "is-unicode-supported": "^0.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/log-symbols/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/log-symbols/node_modules/chalk": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
+      }
+    },
+    "node_modules/log-symbols/node_modules/has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/log-symbols/node_modules/is-unicode-supported": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
+      "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/log-symbols/node_modules/supports-color": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/lru-cache": {
+      "version": "10.4.3",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+      "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/mimic-function": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
+      "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/minimatch": {
+      "version": "9.0.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+      "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/minipass": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+      "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
+    },
+    "node_modules/mocha": {
+      "version": "11.5.0",
+      "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.5.0.tgz",
+      "integrity": "sha512-VKDjhy6LMTKm0WgNEdlY77YVsD49LZnPSXJAaPNL9NRYQADxvORsyG1DIQY6v53BKTnlNbEE2MbVCDbnxr4K3w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "browser-stdout": "^1.3.1",
+        "chokidar": "^4.0.1",
+        "debug": "^4.3.5",
+        "diff": "^7.0.0",
+        "escape-string-regexp": "^4.0.0",
+        "find-up": "^5.0.0",
+        "glob": "^10.4.5",
+        "he": "^1.2.0",
+        "js-yaml": "^4.1.0",
+        "log-symbols": "^4.1.0",
+        "minimatch": "^9.0.5",
+        "ms": "^2.1.3",
+        "picocolors": "^1.1.1",
+        "serialize-javascript": "^6.0.2",
+        "strip-json-comments": "^3.1.1",
+        "supports-color": "^8.1.1",
+        "workerpool": "^6.5.1",
+        "yargs": "^17.7.2",
+        "yargs-parser": "^21.1.1",
+        "yargs-unparser": "^2.0.0"
+      },
+      "bin": {
+        "_mocha": "bin/_mocha",
+        "mocha": "bin/mocha.js"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
+    "node_modules/mocha/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/onetime": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
+      "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "mimic-function": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/ora": {
+      "version": "8.2.0",
+      "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz",
+      "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "chalk": "^5.3.0",
+        "cli-cursor": "^5.0.0",
+        "cli-spinners": "^2.9.2",
+        "is-interactive": "^2.0.0",
+        "is-unicode-supported": "^2.0.0",
+        "log-symbols": "^6.0.0",
+        "stdin-discarder": "^0.2.2",
+        "string-width": "^7.2.0",
+        "strip-ansi": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/ora/node_modules/emoji-regex": {
+      "version": "10.4.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
+      "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/ora/node_modules/log-symbols": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz",
+      "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "chalk": "^5.3.0",
+        "is-unicode-supported": "^1.3.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/ora/node_modules/log-symbols/node_modules/is-unicode-supported": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz",
+      "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/ora/node_modules/string-width": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+      "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^10.3.0",
+        "get-east-asian-width": "^1.0.0",
+        "strip-ansi": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/package-json-from-dist": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+      "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+      "dev": true,
+      "license": "BlueOak-1.0.0"
+    },
+    "node_modules/path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-scurry": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+      "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "lru-cache": "^10.2.0",
+        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/readdirp": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
+      "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14.18.0"
+      },
+      "funding": {
+        "type": "individual",
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/require-directory": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+      "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/restore-cursor": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
+      "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "onetime": "^7.0.0",
+        "signal-exit": "^4.1.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/semver": {
+      "version": "7.6.2",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
+      "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/serialize-javascript": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
+      "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "randombytes": "^2.1.0"
+      }
+    },
+    "node_modules/serialize-javascript/node_modules/randombytes": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+      "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "safe-buffer": "^5.1.0"
+      }
+    },
+    "node_modules/serialize-javascript/node_modules/safe-buffer": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "shebang-regex": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/signal-exit": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+      "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/sinon": {
+      "version": "21.0.0",
+      "resolved": "https://registry.npmjs.org/sinon/-/sinon-21.0.0.tgz",
+      "integrity": "sha512-TOgRcwFPbfGtpqvZw+hyqJDvqfapr1qUlOizROIk4bBLjlsjlB00Pg6wMFXNtJRpu+eCZuVOaLatG7M8105kAw==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@sinonjs/commons": "^3.0.1",
+        "@sinonjs/fake-timers": "^13.0.5",
+        "@sinonjs/samsam": "^8.0.1",
+        "diff": "^7.0.0",
+        "supports-color": "^7.2.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/sinon"
+      }
+    },
+    "node_modules/sinon/node_modules/has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/sinon/node_modules/supports-color": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/stdin-discarder": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz",
+      "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/string-width": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+      "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "eastasianwidth": "^0.2.0",
+        "emoji-regex": "^9.2.2",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/string-width-cjs": {
+      "name": "string-width",
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/string-width-cjs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/string-width-cjs/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/string-width-cjs/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+      "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+      }
+    },
+    "node_modules/strip-ansi-cjs": {
+      "name": "strip-ansi",
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-json-comments": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+      "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/supports-color": {
+      "version": "8.1.1",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+      "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/supports-color?sponsor=1"
+      }
+    },
+    "node_modules/supports-color/node_modules/has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/type-detect": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
+      "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/typescript": {
+      "version": "5.8.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
+      "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/undici-types": {
+      "version": "5.26.5",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
+      "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "node-which": "bin/node-which"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/workerpool": {
+      "version": "6.5.1",
+      "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz",
+      "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==",
+      "dev": true,
+      "license": "Apache-2.0"
+    },
+    "node_modules/wrap-ansi": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+      "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^6.1.0",
+        "string-width": "^5.0.1",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs": {
+      "name": "wrap-ansi",
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/y18n": {
+      "version": "5.0.8",
+      "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+      "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/yargs": {
+      "version": "17.7.2",
+      "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+      "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "cliui": "^8.0.1",
+        "escalade": "^3.1.1",
+        "get-caller-file": "^2.0.5",
+        "require-directory": "^2.1.1",
+        "string-width": "^4.2.3",
+        "y18n": "^5.0.5",
+        "yargs-parser": "^21.1.1"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/yargs-parser": {
+      "version": "21.1.1",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+      "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/yargs-unparser": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz",
+      "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "camelcase": "^6.0.0",
+        "decamelize": "^4.0.0",
+        "flat": "^5.0.2",
+        "is-plain-obj": "^2.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/yargs-unparser/node_modules/camelcase": {
+      "version": "6.3.0",
+      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+      "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/yargs-unparser/node_modules/decamelize": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz",
+      "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/yargs-unparser/node_modules/flat": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
+      "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "bin": {
+        "flat": "cli.js"
+      }
+    },
+    "node_modules/yargs-unparser/node_modules/is-plain-obj": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
+      "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/yargs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/yargs/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/yargs/node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/yargs/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    }
+  }
+}
diff --git a/package.json b/package.json
index b0962113..683d5ffb 100644
--- a/package.json
+++ b/package.json
@@ -2,50 +2,221 @@
   "name": "gitblame",
   "displayName": "Git Blame",
   "description": "See git blame information in the status bar.",
-  "version": "1.3.0",
+  "version": "11.1.4",
   "publisher": "waderyan",
   "engines": {
-    "vscode": "^1.0.0"
+    "vscode": ">=1.91.0",
+    "node": ">=20.9.0"
   },
   "categories": [
     "Other"
   ],
   "galleryBanner": {
-      "color": "#f0efe7",
-      "theme": "light"
+    "color": "#f0efe7",
+    "theme": "light"
   },
   "icon": "images/git_icon.png",
   "activationEvents": [
-    "*"
+    "onStartupFinished"
   ],
   "keywords": [
-    "git", "gitblame", "blame"  
+    "git",
+    "gitblame",
+    "blame",
+    "github",
+    "gitlab",
+    "bitbucket"
   ],
-  "main": "./out/src/extension",
+  "qna": false,
+  "main": "./out/src/index",
   "scripts": {
-    "vscode:prepublish": "node ./node_modules/vscode/bin/compile",
-    "compile": "node ./node_modules/vscode/bin/compile -watch -p ./"
+    "vscode:prepublish": "npm run build",
+    "build": "node esbuild.mjs",
+    "package": "npx @vscode/vsce package",
+    "lint": "biome ci ./src",
+    "pretest": "tsc --project tsconfig.test.json",
+    "test": "npm run pretest && node ./out/test/run-test.js"
   },
-  "dependencies": {
-    "git-blame": "^0.1.1",
-    "moment": "^2.10.6",
-    "path": "^0.12.7",
-    "typescript": "^1.6.2",
-    "vscode": "0.10.x"
+  "devDependencies": {
+    "@biomejs/biome": "2.0.5",
+    "@types/mocha": "10.0.10",
+    "@types/node": "20.14.6",
+    "@types/sinon": "17.0.4",
+    "@types/sinonjs__fake-timers": "8.1.5",
+    "@types/vscode": "1.91.0",
+    "@vscode/test-electron": "2.5.2",
+    "esbuild": "0.25.5",
+    "mocha": "11.7.1.0",
+    "sinon": "21.0.0",
+    "typescript": "5.8.3"
   },
-  "homepage": "https://github.com/wadeanderson7/vscode-gitblame/blob/master/README.md",
+  "homepage": "https://github.com/Sertion/vscode-gitblame/blob/main/README.md",
   "bugs": {
-      "url": "https://github.com/wadeanderson7/vscode-gitblame/issues"
+    "url": "https://github.com/Sertion/vscode-gitblame/issues"
   },
-  "license": "SEE LICENSE IN LICENSE.md",
+  "license": "MIT",
   "repository": {
     "type": "git",
-    "url": "https://github.com/wadeanderson7/vscode-gitblame"
+    "url": "https://github.com/Sertion/vscode-gitblame"
+  },
+  "extensionDependencies": [
+    "vscode.git"
+  ],
+  "capabilities": {
+    "virtualWorkspaces": false,
+    "untrustedWorkspaces": {
+      "supported": false,
+      "description": "Git Blame (extension) executes command line tools in the workspace"
+    }
   },
   "contributes": {
-    "commands": [{
-        "command": "extension.blame",
-        "title": "Git Blame"
-    }]
+    "commands": [
+      {
+        "command": "gitblame.quickInfo",
+        "title": "Git Blame: Show quick info"
+      },
+      {
+        "command": "gitblame.online",
+        "title": "Git Blame: View last change online"
+      },
+      {
+        "command": "gitblame.addCommitHashToClipboard",
+        "title": "Git Blame: Copy hash to clipboard"
+      },
+      {
+        "command": "gitblame.addToolUrlToClipboard",
+        "title": "Git Blame: Copy tool URL to clipboard"
+      },
+      {
+        "command": "gitblame.gitShow",
+        "title": "Git Blame: Git show for current line hash"
+      }
+    ],
+    "colors": [
+      {
+        "id": "gitblame.inlineMessage",
+        "description": "Inline git blame message. Defaults to editorCodeLens.foreground",
+        "defaults": {
+          "dark": "editorCodeLens.foreground",
+          "light": "editorCodeLens.foreground",
+          "highContrast": "editorCodeLens.foreground",
+          "highContrastLight": "editorCodeLens.foreground"
+        }
+      }
+    ],
+    "configuration": {
+      "type": "object",
+      "title": "Git Blame",
+      "properties": {
+        "gitblame.infoMessageFormat": {
+          "type": "string",
+          "default": "${commit.summary}",
+          "description": "Customize the info message"
+        },
+        "gitblame.statusBarMessageClickAction": {
+          "type": "string",
+          "default": "Show info message",
+          "enum": [
+            "Show info message",
+            "Open tool URL",
+            "Open git show",
+            "Copy hash to clipboard"
+          ],
+          "enumDescriptions": [
+            "Show a info message with a short summary of the commit",
+            "Attempt to directly open the tool URL",
+            "Run git show in a vscode terminal view",
+            "Copies the hash of the current line's commit to the clipboard"
+          ]
+        },
+        "gitblame.statusBarMessageFormat": {
+          "type": "string",
+          "default": "Blame ${author.name} (${time.ago})",
+          "description": "Customize the status bar message"
+        },
+        "gitblame.statusBarMessageNoCommit": {
+          "type": "string",
+          "default": "Not Committed Yet",
+          "description": "Customize the status bar message"
+        },
+        "gitblame.statusBarPositionPriority": {
+          "type": "number",
+          "default": 500,
+          "description": "Priority where the status bar view should be placed"
+        },
+        "gitblame.inlineMessageEnabled": {
+          "type": "boolean",
+          "default": false,
+          "description": "Show blame information inline (next to the code)"
+        },
+        "gitblame.inlineMessageFormat": {
+          "type": "string",
+          "default": "Blame ${author.name} (${time.ago})",
+          "description": "Customize the inline message"
+        },
+        "gitblame.inlineMessageNoCommit": {
+          "type": "string",
+          "default": "Not Committed Yet",
+          "description": "Customize the inline message"
+        },
+        "gitblame.inlineMessageMargin": {
+          "type": "number",
+          "default": 2,
+          "description": "The amount of margin between line and inline blame view"
+        },
+        "gitblame.currentUserAlias": {
+          "type": "string",
+          "default": "",
+          "markdownDescription": "Replaces `${author.name}` and `${committer.name}` when the git config `user.email` matches the author's or committer's email address."
+        },
+        "gitblame.remoteName": {
+          "type": "string",
+          "default": "origin",
+          "description": "The name of the git remote used to build the URL."
+        },
+        "gitblame.commitUrl": {
+          "type": "string",
+          "default": "${tool.protocol}//${gitorigin.hostname}${gitorigin.port}${gitorigin.path}${tool.commitpath}${hash}",
+          "markdownDescription": "The link to an online tool to view a commit (use `${hash}` for the commit hash)."
+        },
+        "gitblame.ignoreWhitespace": {
+          "type": "boolean",
+          "default": false,
+          "markdownDescription": "Ignore whitespace changes when blaming (`-w` flag)"
+        },
+        "gitblame.isWebPathPlural": {
+          "deprecationMessage": "This setting has been replaced by gitblame.pluralWebPathSubstrings"
+        },
+        "gitblame.pluralWebPathSubstrings": {
+          "type": "array",
+          "default": [
+            "bitbucket",
+            "atlassian"
+          ],
+          "markdownDescription": "An array of substrings that, when present in the git origin URL, adds an extra _s_ to the url after _commit_ in `gitblame.commitUrl`'s default behavior"
+        },
+        "gitblame.delayBlame": {
+          "type": "number",
+          "default": 0,
+          "description": "This setting adds a delay (in milliseconds) before the blame is displayed"
+        },
+        "gitblame.parallelBlames": {
+          "type": "number",
+          "minimum": 1,
+          "default": 2,
+          "description": "Limit allowed parallel git blame process count"
+        },
+        "gitblame.maxLineCount": {
+          "type": "number",
+          "default": 16384,
+          "markdownDescription": "Will skip blaming files with **more** lines than this value"
+        },
+        "gitblame.revsFile": {
+          "type": "array",
+          "default": [],
+          "markdownDescription": "List of refs-file names to look for relative to the closest `.git`-folder. The first file in the list that is accessible will be used with the `-S`-argument in `git blame`."
+        }
+      }
+    }
   }
-}
+}
\ No newline at end of file
diff --git a/src/controller.ts b/src/controller.ts
deleted file mode 100644
index 4bcf6b32..00000000
--- a/src/controller.ts
+++ /dev/null
@@ -1,101 +0,0 @@
-
-import {Disposable, window, TextEditor, TextEditorSelectionChangeEvent} from 'vscode';
-import {GitBlame} from './gitblame';
-import * as path from 'path';
-import * as moment from 'moment';
-
-export class GitBlameController {
-
-    private _disposable: Disposable;
-    private _textDecorator: TextDecorator
-
-    constructor(private gitBlame: GitBlame, private gitRoot: string, private view) {
-        const self = this;
-
-        const disposables: Disposable[] = [];
-
-        window.onDidChangeActiveTextEditor(self.onTextEditorChange, self, disposables);
-        window.onDidChangeTextEditorSelection(self.onTextEditorSelectionChange, self, disposables);
-
-        this.onTextEditorChange(window.activeTextEditor);
-
-        this._disposable = Disposable.from(...disposables);
-        this._textDecorator = new TextDecorator();
-    }
-
-    onTextEditorChange(editor: TextEditor) : void {
-        this.clear();
-
-        if (!editor) return;
-
-        const doc = editor.document;
-
-        if (!doc) return;
-        if (doc.isUntitled) return; // Document hasn't been saved and is not in git.
-
-        const lineNumber = editor.selection.active.line + 1; // line is zero based
-        const file = path.relative(this.gitRoot, editor.document.fileName);
-
-        this.gitBlame.getBlameInfo(file).then((info) => {
-            this.show(info, lineNumber);
-        }, () => {
-            // Do nothing.
-        });
-    }
-
-    onTextEditorSelectionChange(textEditorSelectionChangeEvent: TextEditorSelectionChangeEvent) : void {
-        this.onTextEditorChange(textEditorSelectionChangeEvent.textEditor);
-    }
-
-    clear() {
-        this.view.refresh('');
-    }
-
-    show(blameInfo: Object, lineNumber: number) : void {
-
-        if (lineNumber in blameInfo['lines']) {
-            const hash = blameInfo['lines'][lineNumber]['hash'];
-            const commitInfo = blameInfo['commits'][hash];
-
-            this.view.refresh(this._textDecorator.toTextView(new Date(), commitInfo));
-        } else {
-            // No line info.
-        }
-    }
-
-    dispose() {
-        this._disposable.dispose();
-    }
-}
-
-
-export class TextDecorator {
-
-    toTextView(dateNow: Date, commit: Object) : string {
-        const author = commit['author'];
-        const dateText = this.toDateText(dateNow, new Date(author['timestamp'] * 1000));
-
-        return 'Blame ' + author['name'] + ' ( ' + dateText + ' )';
-    }
-
-    toDateText(dateNow: Date, dateThen: Date) : string {
-
-        const momentNow = moment(dateNow);
-        const momentThen = moment(dateThen);
-
-        const months = momentNow.diff(momentThen, 'months');
-        const days = momentNow.diff(momentThen, 'days');
-
-        if (months <= 1) {
-            if (days == 0) {
-                return 'today';
-            } else if (days == 1) {
-                return 'yesterday';
-            } else {
-                return days + ' days ago';
-            }
-        } else {
-            return months + ' months ago';
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/extension.ts b/src/extension.ts
deleted file mode 100644
index e5e5f617..00000000
--- a/src/extension.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-import {GitBlame} from './gitblame';
-import {StatusBarView} from './view';
-import {GitBlameController} from './controller';
-import {window, ExtensionContext, Disposable, StatusBarAlignment,
-    workspace, TextEditor, TextEditorSelectionChangeEvent, commands} from 'vscode';
-import * as fs from 'fs';
-import * as path from 'path';
-
-const gitBlameShell= require('git-blame');
-
-export function activate(context: ExtensionContext) {
-
-    // Workspace not using a folder. No access to git repo.
-    if (!workspace.rootPath) {
-        return;
-    }
-
-    const workspaceRoot = workspace.rootPath;
-    commands.registerCommand('extension.blame', () => {
-        showMessage(context, workspaceRoot);
-    });
-
-    // Try to find the repo first in the workspace, then in parent directories
-    // because sometimes one opens a subdirectory but still wants information
-    // about the full repo.
-    lookupRepo(context, workspaceRoot);
-}
-
-function lookupRepo(context: ExtensionContext, repoDir: string) {
-    const repoPath = path.join(repoDir, '.git');
-
-    fs.access(repoPath, (err) => {
-        if (err) {
-            // No access to git repo or no repo, try to go up.
-            const parentDir = path.dirname(repoDir);
-            if (parentDir != repoDir) {
-                lookupRepo(context, parentDir);
-            }
-        }
-        else {
-            const statusBar = window.createStatusBarItem(StatusBarAlignment.Left);
-            const gitBlame = new GitBlame(repoPath, gitBlameShell);
-            const controller = new GitBlameController(gitBlame, repoDir, new StatusBarView(statusBar));
-
-            context.subscriptions.push(controller);
-            context.subscriptions.push(gitBlame);
-        }
-    });
-}
-
-function showMessage(context: ExtensionContext, repoDir: string) {
-    const repoPath = path.join(repoDir, '.git');
-
-    fs.access(repoPath, (err) => {
-        if (err) {
-            // No access to git repo or no repo, try to go up.
-            const parentDir = path.dirname(repoDir);
-            if (parentDir != repoDir) {
-                showMessage(context, parentDir);
-            }
-        }
-        else {
-            const editor = window.activeTextEditor;
-            
-            if (!editor) return;
-
-            const doc = editor.document;
-
-            if (!doc) return;
-            if (doc.isUntitled) return; // Document hasn't been saved and is not in git.
-
-            const gitBlame = new GitBlame(repoPath, gitBlameShell);
-            const lineNumber = editor.selection.active.line + 1; // line is zero based
-            const file = path.relative(repoDir, editor.document.fileName);
-
-            gitBlame.getBlameInfo(file).then((info) => {
-
-                if (lineNumber in info['lines']) {
-                
-                    const hash = info['lines'][lineNumber]['hash'];
-                    const commitInfo = info['commits'][hash];
-
-                    window.showInformationMessage(hash + ' ' + commitInfo['summary']);
-                }
-            });
-        }
-    });
-}
-
-
-
diff --git a/src/git/blame.ts b/src/git/blame.ts
new file mode 100644
index 00000000..8d4a9c5e
--- /dev/null
+++ b/src/git/blame.ts
@@ -0,0 +1,145 @@
+import { type FSWatcher, promises, watch } from "node:fs";
+import { type Disposable, workspace } from "vscode";
+import { Logger } from "../util/logger.js";
+import { getProperty } from "../util/property.js";
+import { type Blame, File } from "./file.js";
+import { Queue } from "./queue.js";
+import { getGitFolder } from "./util/git-command.js";
+import type { LineAttachedCommit } from "./util/stream-parsing.js";
+
+export class Blamer {
+	private readonly metadata = new WeakMap<
+		Promise<Blame | undefined>,
+		| {
+				file: File;
+				gitRoot: string;
+		  }
+		| undefined
+	>();
+	private readonly files = new Map<string, Promise<Blame | undefined>>();
+	private readonly fsWatchers = new Map<string, FSWatcher>();
+	private readonly blameQueue = new Queue<Blame | undefined>(
+		getProperty("parallelBlames"),
+	);
+	private readonly configChange: Disposable;
+
+	public constructor() {
+		this.configChange = workspace.onDidChangeConfiguration((e) => {
+			if (e.affectsConfiguration("gitblame")) {
+				this.blameQueue.updateParallel(getProperty("parallelBlames"));
+			}
+		});
+	}
+
+	public async prepareFile(fileName: string): Promise<void> {
+		if (this.files.has(fileName)) {
+			return;
+		}
+
+		let resolve: (blame: Promise<Blame | undefined> | undefined) => void =
+			() => {};
+		this.files.set(
+			fileName,
+			new Promise<Blame | undefined>((res) => {
+				resolve = res;
+			}),
+		);
+
+		const { file, gitRoot } = await this.create(fileName);
+
+		if (file === undefined) {
+			resolve(undefined);
+			return;
+		}
+
+		Logger.debug("Setting up file watcher for '%s'", file.fileName);
+
+		this.fsWatchers.set(
+			file.fileName,
+			watch(
+				file.fileName,
+				{
+					persistent: false,
+				},
+				() => {
+					Logger.debug(
+						"File watcher callback for '%s' executed",
+						file.fileName,
+					);
+					this.remove(file.fileName);
+				},
+			),
+		);
+
+		const blame = this.blameQueue.add(() => file.getBlame());
+		this.metadata.set(blame, { file, gitRoot });
+		resolve(blame);
+	}
+
+	public async getLine(
+		fileName: string,
+		lineNumber: number,
+	): Promise<LineAttachedCommit | undefined> {
+		await this.prepareFile(fileName);
+
+		const commitLineNumber = lineNumber + 1;
+		const blameInfo = await this.files.get(fileName);
+
+		return blameInfo?.get(commitLineNumber);
+	}
+
+	public removeFromRepository(gitRepositoryPath: string): void {
+		for (const [fileName, file] of this.files) {
+			const metadata = this.metadata.get(file);
+			if (metadata?.gitRoot === gitRepositoryPath) {
+				this.remove(fileName);
+			}
+		}
+	}
+
+	public remove(fileName: string): void {
+		const blame = this.files.get(fileName);
+		if (blame) {
+			this.metadata.get(blame)?.file?.dispose();
+		}
+
+		this.files.delete(fileName);
+		this.fsWatchers.get(fileName)?.close();
+		this.fsWatchers.delete(fileName);
+		Logger.debug("Cache for '%s' cleared. File watcher closed.", fileName);
+	}
+
+	public dispose(): void {
+		for (const fileName of this.files.keys()) {
+			this.remove(fileName);
+		}
+		this.configChange.dispose();
+	}
+
+	private async create(
+		fileName: string,
+	): Promise<
+		{ gitRoot: string; file: File } | { gitRoot: undefined; file: undefined }
+	> {
+		try {
+			await promises.access(fileName);
+
+			const gitRoot = await getGitFolder(fileName);
+			if (gitRoot) {
+				return {
+					gitRoot: gitRoot,
+					file: new File(fileName),
+				};
+			}
+		} catch {
+			// NOOP
+		}
+
+		Logger.info(`Will not blame '${fileName}'. Not in a git repository.`);
+
+		return {
+			gitRoot: undefined,
+			file: undefined,
+		};
+	}
+}
diff --git a/src/git/extension.ts b/src/git/extension.ts
new file mode 100644
index 00000000..1d876fcd
--- /dev/null
+++ b/src/git/extension.ts
@@ -0,0 +1,267 @@
+import { dirname } from "node:path";
+import {
+	commands,
+	Disposable,
+	env,
+	type MessageItem,
+	type TextEditor,
+	ThemeIcon,
+	window,
+	workspace,
+} from "vscode";
+import {
+	getActiveTextEditor,
+	getFilePosition,
+	NO_FILE_OR_PLACE,
+} from "../util/get-active.js";
+import { errorMessage, infoMessage } from "../util/message.js";
+import { getProperty } from "../util/property.js";
+import {
+	normalizeCommitInfoTokens,
+	parseTokens,
+} from "../util/text-decorator.js";
+import { type Document, validEditor } from "../util/valid-editor.js";
+import { StatusBarView } from "../view.js";
+import { Blamer } from "./blame.js";
+import { HeadWatch } from "./head-watch.js";
+import { getToolUrl } from "./util/get-tool-url.js";
+import { isHash, isUncommitted } from "./util/is-hash.js";
+import type { LineAttachedCommit } from "./util/stream-parsing.js";
+
+type ActionableMessageItem = MessageItem & {
+	action: () => void;
+};
+
+export class Extension {
+	private readonly disposable: Disposable;
+	private readonly blame: Blamer;
+	private readonly view: StatusBarView;
+	private readonly headWatcher: HeadWatch;
+
+	constructor() {
+		this.blame = new Blamer();
+		this.view = new StatusBarView();
+		this.headWatcher = new HeadWatch();
+
+		this.disposable = this.setupListeners();
+	}
+
+	public async blameLink(): Promise<void> {
+		const lineAware = await this.commit(true);
+		if (lineAware === undefined) {
+			return;
+		}
+		const toolUrl = await getToolUrl(lineAware);
+
+		if (toolUrl) {
+			commands.executeCommand("vscode.open", toolUrl);
+		} else {
+			errorMessage("Empty gitblame.commitUrl");
+		}
+	}
+
+	public async showMessage(): Promise<void> {
+		const lineAware = await this.commit(false);
+
+		if (!lineAware || isUncommitted(lineAware.commit)) {
+			this.view.clear();
+			return;
+		}
+
+		const message = parseTokens(
+			getProperty("infoMessageFormat"),
+			normalizeCommitInfoTokens(lineAware.commit),
+		);
+		const toolUrl = await getToolUrl(lineAware);
+		const action: ActionableMessageItem[] = [];
+
+		if (toolUrl) {
+			action.push({
+				title: "Online",
+				action() {
+					commands.executeCommand("vscode.open", toolUrl);
+				},
+			});
+		}
+
+		action.push({
+			title: "Terminal",
+			action: () => this.runGitShow(),
+		});
+
+		this.view.set(lineAware.commit, getActiveTextEditor());
+
+		(await infoMessage(message, action))?.action();
+	}
+
+	public async copyHash(): Promise<void> {
+		const lineAware = await this.commit(true);
+
+		if (lineAware && !isUncommitted(lineAware.commit)) {
+			await env.clipboard.writeText(lineAware.commit.hash);
+			infoMessage("Copied hash");
+		}
+	}
+
+	public async copyToolUrl(): Promise<void> {
+		const lineAware = await this.commit(true);
+		if (lineAware === undefined) {
+			return;
+		}
+		const toolUrl = await getToolUrl(lineAware);
+
+		if (toolUrl) {
+			await env.clipboard.writeText(toolUrl.toString());
+			infoMessage("Copied tool URL");
+		} else {
+			errorMessage("gitblame.commitUrl config empty");
+		}
+	}
+
+	public async runGitShow(): Promise<void> {
+		const editor = getActiveTextEditor();
+
+		if (!validEditor(editor)) {
+			return;
+		}
+
+		const currentLine = await this.commit(true);
+		if (currentLine === undefined) {
+			return;
+		}
+		const { hash } = currentLine.commit;
+
+		// Only ever allow HEAD or a git hash
+		if (!isHash(hash, true)) {
+			return;
+		}
+
+		const ignoreWhitespace = getProperty("ignoreWhitespace") ? "-w " : "";
+		const terminal = window.createTerminal({
+			name: `Git Blame: git show ${hash}`,
+			iconPath: new ThemeIcon("git-commit"),
+			isTransient: true,
+			cwd: dirname(editor.document.fileName),
+		});
+		terminal.sendText(`git show ${ignoreWhitespace}${hash}; exit 0`, true);
+		terminal.show();
+	}
+
+	public async updateView(
+		textEditor = getActiveTextEditor(),
+		useDelay = true,
+	): Promise<void> {
+		if (!this.view.preUpdate(textEditor)) {
+			return;
+		}
+
+		if (textEditor.document.lineCount > getProperty("maxLineCount")) {
+			this.view.fileToLong();
+			return;
+		}
+
+		this.headWatcher.addFile(textEditor.document.fileName);
+
+		const before = getFilePosition(textEditor);
+		const lineAware = await this.blame.getLine(
+			textEditor.document.fileName,
+			textEditor.selection.active.line,
+		);
+
+		const textEditorAfter = getActiveTextEditor();
+		if (!validEditor(textEditorAfter)) {
+			return;
+		}
+		const after = getFilePosition(textEditorAfter);
+
+		// Only update if we haven't moved since we started blaming
+		// or if we no longer have focus on any file
+		if (before === after || after === NO_FILE_OR_PLACE) {
+			this.view.set(lineAware?.commit, textEditor, useDelay);
+		}
+	}
+
+	public dispose(): void {
+		this.view.dispose();
+		this.disposable.dispose();
+		this.blame.dispose();
+		this.headWatcher.dispose();
+	}
+
+	private setupListeners(): Disposable {
+		const changeTextEditorSelection = (textEditor: TextEditor): void => {
+			const { scheme } = textEditor.document.uri;
+			if (scheme === "file" || scheme === "untitled") {
+				this.updateView(textEditor);
+			}
+		};
+
+		this.headWatcher.onChange(({ repositoryRoot }) =>
+			this.blame.removeFromRepository(repositoryRoot),
+		);
+
+		return Disposable.from(
+			window.onDidChangeActiveTextEditor((textEditor): void => {
+				if (validEditor(textEditor)) {
+					this.view.activity();
+					this.blame.prepareFile(textEditor.document.fileName);
+					changeTextEditorSelection(textEditor);
+				} else {
+					this.view.clear();
+				}
+			}),
+			window.onDidChangeTextEditorSelection(({ textEditor }) => {
+				changeTextEditorSelection(textEditor);
+			}),
+			workspace.onDidSaveTextDocument((): void => {
+				this.updateView();
+			}),
+			workspace.onDidCloseTextDocument((document: Document): void => {
+				this.blame.remove(document.fileName);
+			}),
+			workspace.onDidChangeTextDocument(({ document }) => {
+				const textEditor = getActiveTextEditor();
+				if (textEditor?.document === document) {
+					this.updateView(textEditor, false);
+				}
+			}),
+		);
+	}
+
+	private async commit(
+		noVisibleActivity: boolean,
+	): Promise<LineAttachedCommit | undefined> {
+		const editor = getActiveTextEditor();
+
+		if (!validEditor(editor)) {
+			errorMessage(
+				"Unable to blame current line. Active view is not a file on disk.",
+			);
+			return;
+		}
+
+		if (editor.document.lineCount > getProperty("maxLineCount")) {
+			errorMessage("Git Blame is disabled for the current file");
+			this.view.fileToLong();
+			return;
+		}
+
+		if (!noVisibleActivity) {
+			this.view.activity();
+		}
+
+		this.headWatcher.addFile(editor.document.fileName);
+		const line = await this.blame.getLine(
+			editor.document.fileName,
+			editor.selection.active.line,
+		);
+
+		if (!line) {
+			errorMessage(
+				"Unable to blame current line. Unable to get blame information for line.",
+			);
+		}
+
+		return line;
+	}
+}
diff --git a/src/git/file.ts b/src/git/file.ts
new file mode 100644
index 00000000..2ba3bb9a
--- /dev/null
+++ b/src/git/file.ts
@@ -0,0 +1,93 @@
+import type { ChildProcess } from "node:child_process";
+import { realpath } from "node:fs/promises";
+import { relative } from "node:path";
+
+import { Logger } from "../util/logger.js";
+import { blameProcess, getGitEmail, getRevsFile } from "./util/git-command.js";
+import {
+	type CommitRegistry,
+	type LineAttachedCommit,
+	processChunk,
+} from "./util/stream-parsing.js";
+
+export type Blame = Map<number, LineAttachedCommit | undefined>;
+
+export class File {
+	private store?: Promise<Blame | undefined>;
+	private process?: ChildProcess;
+	private killed = false;
+
+	public constructor(public readonly fileName: string) {}
+
+	public getBlame(): Promise<Blame | undefined> {
+		this.store ??= this.blame();
+
+		return this.store;
+	}
+
+	public dispose(): void {
+		this.process?.kill();
+		this.killed = true;
+	}
+
+	private async *run(file: string): AsyncGenerator<LineAttachedCommit> {
+		const [refs, email] = await Promise.all([
+			getRevsFile(file),
+			getGitEmail(file),
+		]);
+		this.process = blameProcess(file, refs);
+
+		Logger.debug(
+			"Email address for currentUser for file '%s' is '%s'",
+			file,
+			email ?? "VALUE_NOT_SET_IN_GIT_CONFIG",
+		);
+
+		const commitRegistry: CommitRegistry = new Map();
+		for await (const chunk of this.process?.stdout ?? []) {
+			Logger.debug(
+				"Got chunk from '%s' git blame process. Size: %d",
+				file,
+				chunk.length,
+			);
+			yield* processChunk(chunk, email, commitRegistry);
+		}
+		for await (const error of this.process?.stderr ?? []) {
+			if (typeof error === "string") {
+				throw new Error(error);
+			}
+		}
+	}
+
+	private async blame(): Promise<Blame | undefined> {
+		const blameInfo: Blame = new Map();
+		const realpathFileName = await realpath(this.fileName);
+
+		try {
+			for await (const lineAttachedCommit of this.run(realpathFileName)) {
+				Logger.trace(
+					"Found blame information for %s:%d: hash:%s",
+					realpathFileName,
+					lineAttachedCommit.line.result,
+					lineAttachedCommit.commit.hash,
+				);
+				blameInfo.set(lineAttachedCommit.line.result, lineAttachedCommit);
+			}
+		} catch (err) {
+			Logger.error(err);
+			this.dispose();
+		}
+
+		// Don't return partial git blame info when terminating a blame
+		if (!this.killed) {
+			if (relative(this.fileName, realpathFileName)) {
+				Logger.info(
+					`Blamed "${realpathFileName}" (resolved via symlink from "${this.fileName}")`,
+				);
+			} else {
+				Logger.info(`Blamed "${realpathFileName}"`);
+			}
+			return blameInfo;
+		}
+	}
+}
diff --git a/src/git/head-watch.ts b/src/git/head-watch.ts
new file mode 100644
index 00000000..06da5320
--- /dev/null
+++ b/src/git/head-watch.ts
@@ -0,0 +1,77 @@
+import { type FSWatcher, watch } from "node:fs";
+import { join, resolve } from "node:path";
+import { Logger } from "../util/logger.js";
+import { getGitFolder } from "./util/git-command.js";
+
+export type HeadChangeEvent = {
+	gitRoot: string;
+	repositoryRoot: string;
+};
+
+type HeadChangeEventCallbackFunction = (event: HeadChangeEvent) => void;
+
+export class HeadWatch {
+	private readonly heads: Map<string, FSWatcher> = new Map();
+	private readonly filesWithFoundHeads: Set<string> = new Set();
+	private callback: HeadChangeEventCallbackFunction = () => undefined;
+
+	public onChange(callback: HeadChangeEventCallbackFunction): void {
+		this.callback = callback;
+	}
+
+	public async addFile(filePath: string): Promise<void> {
+		if (this.filesWithFoundHeads.has(filePath)) {
+			return;
+		}
+
+		this.filesWithFoundHeads.add(filePath);
+
+		const gitRepositoryPath = await getGitFolder(filePath);
+		const gitRoot = this.normalizeWindowsDriveLetter(gitRepositoryPath);
+		const watched = this.heads.has(gitRoot);
+
+		if (watched === true || gitRepositoryPath === "") {
+			return;
+		}
+
+		const repositoryRoot = resolve(gitRoot, "..");
+		const HEADFile = join(gitRoot, "HEAD");
+
+		this.heads.set(
+			gitRoot,
+			watch(
+				HEADFile,
+				{
+					persistent: false,
+				},
+				() => {
+					Logger.debug("File watcher callback for '%s' called.", HEADFile);
+					this.callback({ gitRoot, repositoryRoot });
+				},
+			),
+		);
+
+		Logger.debug("File watcher for '%s' created.", HEADFile);
+	}
+
+	public dispose(): void {
+		for (const [gitRoot, headWatcher] of this.heads) {
+			headWatcher.close();
+			Logger.debug(
+				"File watcher for HEAD file in git root '%s' closed.",
+				gitRoot,
+			);
+		}
+		this.heads.clear();
+		this.filesWithFoundHeads.clear();
+		this.callback = () => undefined;
+	}
+
+	private normalizeWindowsDriveLetter(path: string): string {
+		if (path.length === 0) {
+			return "";
+		}
+
+		return path[0].toLowerCase() + path.substr(1);
+	}
+}
diff --git a/src/git/queue.ts b/src/git/queue.ts
new file mode 100644
index 00000000..8cea39fa
--- /dev/null
+++ b/src/git/queue.ts
@@ -0,0 +1,71 @@
+import { Logger } from "../util/logger";
+
+export class Queue<
+	ReturnValue = void,
+	QueueFunction extends () => Promise<ReturnValue> = () => Promise<ReturnValue>,
+> {
+	private readonly list: QueueFunction[] = [];
+	private readonly storage = new Map<QueueFunction, (r: ReturnValue) => void>();
+	private readonly processing = new Set<QueueFunction>();
+	private _maxParallel?: number;
+
+	constructor(maxParallel = 2) {
+		this.maxParallel = maxParallel;
+	}
+
+	public add(toQueue: QueueFunction): Promise<ReturnValue> {
+		return new Promise<ReturnValue>((resolve) => {
+			this.storage.set(toQueue, resolve);
+			if (this.processing.size < this.maxParallel) {
+				this.startFunction(toQueue);
+			} else {
+				Logger.debug(
+					"Already running %s in parallel. Adding execution to queue.",
+					this.maxParallel,
+				);
+				this.list.push(toQueue);
+			}
+		});
+	}
+
+	public updateParallel(maxParallel: number): void {
+		const oldMax = this.maxParallel;
+		this.maxParallel = maxParallel;
+		const moreQueueSpace = Math.max(0, this.maxParallel - oldMax);
+
+		for (let i = 0; i < moreQueueSpace; i++) {
+			this.runNext();
+		}
+	}
+
+	private set maxParallel(value: number) {
+		this._maxParallel = value;
+	}
+
+	private get maxParallel(): number {
+		return Math.max(1, Number(this._maxParallel));
+	}
+
+	private startFunction(func: QueueFunction): void {
+		this.processing.add(func);
+		const resolve = this.storage.get(func);
+		this.storage.delete(func);
+		if (resolve) {
+			func()
+				.then(resolve)
+				.finally(() => {
+					this.processing.delete(func);
+					this.runNext();
+				});
+		}
+	}
+
+	private runNext() {
+		if (this.processing.size < this.maxParallel) {
+			const next = this.list.shift();
+			if (next) {
+				this.startFunction(next);
+			}
+		}
+	}
+}
diff --git a/src/git/util/get-tool-url.ts b/src/git/util/get-tool-url.ts
new file mode 100644
index 00000000..c426f95f
--- /dev/null
+++ b/src/git/util/get-tool-url.ts
@@ -0,0 +1,141 @@
+import { URL } from "node:url";
+import { Uri } from "vscode";
+import { isUrl } from "../../util/is-url.js";
+import { errorMessage } from "../../util/message.js";
+import { getProperty } from "../../util/property.js";
+import { split } from "../../util/split.js";
+import { type InfoTokens, parseTokens } from "../../util/text-decorator.js";
+import {
+	getActiveFileOrigin,
+	getDefaultBranch,
+	getRelativePathOfActiveFile,
+	getRemoteUrl,
+} from "./git-command.js";
+import { isUncommitted } from "./is-hash.js";
+import { originUrlToToolUrl } from "./origin-url-to-tool-url.js";
+import { projectNameFromOrigin } from "./project-name-from-origin.js";
+import type { LineAttachedCommit } from "./stream-parsing.js";
+import { stripGitRemoteUrl, stripGitSuffix } from "./strip-git-remote-url.js";
+
+export type ToolUrlTokens = {
+	hash: string;
+	"project.name": string;
+	"project.remote": string;
+	"gitorigin.hostname": string | ((index?: string) => string | undefined);
+	"gitorigin.path": string | ((index?: string) => string | undefined);
+	"file.path": string;
+	"file.path.result": string;
+	"file.path.source": string;
+	"file.line": string;
+	"file.line.result": string;
+	"file.line.source": string;
+} & InfoTokens;
+
+const getPathIndex = (path: string, index?: string, splitOn = "/"): string => {
+	const parts = path.split(splitOn).filter((a) => !!a);
+	return parts[Number(index)] || "invalid-index";
+};
+
+const gitOriginHostname = ({
+	hostname,
+}: URL): string | ((index?: string) => string) => {
+	return (index?: string): string => {
+		if (index === "") {
+			return hostname;
+		}
+
+		return getPathIndex(hostname, index, ".");
+	};
+};
+
+export const gitRemotePath = (
+	remote: string,
+): string | ((index?: string) => string) => {
+	if (/^[a-z0-9-]+?@/.test(remote)) {
+		const [, path] = split(remote, ":");
+		return (index = ""): string => {
+			if (index === "") {
+				return `/${path}`;
+			}
+
+			return getPathIndex(path, index);
+		};
+	}
+	try {
+		const { pathname } = new URL(remote);
+		return (index = ""): string => {
+			if (index === "") {
+				return pathname;
+			}
+
+			return getPathIndex(pathname, index);
+		};
+	} catch {
+		return () => "no-remote-url";
+	}
+};
+
+const isToolUrlPlural = (origin: string): boolean =>
+	(getProperty("pluralWebPathSubstrings") ?? []).some((substring) =>
+		origin.includes(substring),
+	);
+
+export const generateUrlTokens = async (
+	lineAware: LineAttachedCommit,
+): Promise<ToolUrlTokens | undefined> => {
+	const remoteName = getProperty("remoteName");
+
+	const origin = await getActiveFileOrigin(remoteName);
+
+	if (origin === remoteName) {
+		return;
+	}
+
+	const remoteUrl = await getRemoteUrl(remoteName);
+	const tool = originUrlToToolUrl(remoteUrl);
+	const filePath = await getRelativePathOfActiveFile();
+	const defaultBranch = await getDefaultBranch(remoteName);
+
+	return {
+		hash: lineAware.commit.hash,
+		"tool.protocol": tool?.protocol ?? "https:",
+		"tool.commitpath": `/commit${isToolUrlPlural(remoteUrl) ? "s" : ""}/`,
+		"project.name": projectNameFromOrigin(origin),
+		"project.remote": stripGitRemoteUrl(remoteUrl),
+		"project.defaultbranch": defaultBranch,
+		"gitorigin.hostname": tool ? gitOriginHostname(tool) : "no-origin-url",
+		"gitorigin.path": gitRemotePath(stripGitSuffix(origin)),
+		"gitorigin.port": tool?.port ? `:${tool.port}` : "",
+		"file.path": filePath,
+		"file.path.result": filePath,
+		"file.path.source": lineAware.filename,
+		"file.line": lineAware.line.result.toString(),
+		"file.line.result": lineAware.line.result.toString(),
+		"file.line.source": lineAware.line.source.toString(),
+	};
+};
+
+export const getToolUrl = async (
+	commit?: LineAttachedCommit,
+): Promise<Uri | undefined> => {
+	if (!commit || isUncommitted(commit.commit)) {
+		return;
+	}
+	const tokens = await generateUrlTokens(commit);
+
+	if (tokens === undefined) {
+		return;
+	}
+
+	const parsedUrl = parseTokens(getProperty("commitUrl"), tokens);
+
+	if (isUrl(parsedUrl)) {
+		return Uri.parse(parsedUrl, true);
+	}
+
+	errorMessage(
+		`Malformed gitblame.commitUrl: '${parsedUrl}' from '${getProperty(
+			"commitUrl",
+		)}'`,
+	);
+};
diff --git a/src/git/util/git-command.ts b/src/git/util/git-command.ts
new file mode 100644
index 00000000..7740a625
--- /dev/null
+++ b/src/git/util/git-command.ts
@@ -0,0 +1,165 @@
+import { type ChildProcess, spawn } from "node:child_process";
+import { access } from "node:fs/promises";
+import { dirname, join } from "node:path";
+
+import { extensions } from "vscode";
+
+import type { GitExtension } from "../../../types/git.js";
+import { execute } from "../../util/execute.js";
+import { getActiveTextEditor } from "../../util/get-active.js";
+import { Logger } from "../../util/logger.js";
+import { getProperty } from "../../util/property.js";
+import { split } from "../../util/split.js";
+import { validEditor } from "../../util/valid-editor.js";
+
+export const getGitCommand = (): string => {
+	const vscodeGit = extensions.getExtension<GitExtension>("vscode.git");
+
+	if (vscodeGit?.exports.enabled) {
+		return vscodeGit.exports.getAPI(1).git.path;
+	}
+
+	return "git";
+};
+
+const runGit = (cwd: string, ...args: string[]): Promise<string> =>
+	execute(getGitCommand(), args, {
+		cwd: dirname(cwd),
+		env: { ...process.env, LC_ALL: "C" },
+	});
+
+export const getActiveFileOrigin = async (
+	remoteName: string,
+): Promise<string> => {
+	const activeEditor = getActiveTextEditor();
+
+	if (!validEditor(activeEditor)) {
+		return "";
+	}
+
+	return runGit(
+		activeEditor.document.fileName,
+		"ls-remote",
+		"--get-url",
+		remoteName,
+	);
+};
+
+export const getRemoteUrl = async (fallbackRemote: string): Promise<string> => {
+	const activeEditor = getActiveTextEditor();
+
+	if (!validEditor(activeEditor)) {
+		return "";
+	}
+
+	const { fileName } = activeEditor.document;
+	const currentBranch = await runGit(
+		fileName,
+		"symbolic-ref",
+		"-q",
+		"--short",
+		"HEAD",
+	);
+	const curRemote = await runGit(
+		fileName,
+		"config",
+		`branch.${currentBranch}.remote`,
+	);
+	return runGit(
+		fileName,
+		"config",
+		`remote.${curRemote || fallbackRemote}.url`,
+	);
+};
+
+export const getGitFolder = async (fileName: string): Promise<string> =>
+	runGit(fileName, "rev-parse", "--absolute-git-dir");
+
+export const blameProcess = (
+	realpathFileName: string,
+	revsFile: string | undefined,
+): ChildProcess => {
+	const args = ["blame", "-C", "--incremental", "--", realpathFileName];
+
+	if (getProperty("ignoreWhitespace")) {
+		args.splice(1, 0, "-w");
+	}
+
+	if (revsFile) {
+		args.splice(1, 0, "-S", revsFile);
+	}
+
+	Logger.info(`${getGitCommand()} ${args.join(" ")}`);
+
+	return spawn(getGitCommand(), args, {
+		cwd: dirname(realpathFileName),
+		stdio: ["ignore", null, null],
+		env: {
+			...process.env,
+			LC_ALL: "C",
+			GIT_PAGER: "cat",
+		},
+	});
+};
+
+export const getRevsFile = async (
+	realFileName: string,
+): Promise<string | undefined> => {
+	const possibleRevsFiles = getProperty("revsFile");
+	if (possibleRevsFiles.length === 0) {
+		return undefined;
+	}
+
+	const gitRoot = await getGitFolder(realFileName);
+	const projectRoot = dirname(gitRoot);
+	const revsFiles = await Promise.allSettled(
+		possibleRevsFiles
+			.map((fileName) => join(projectRoot, fileName))
+			.map((path) => access(path).then(() => path)),
+	);
+
+	return revsFiles.filter(
+		(promise): promise is PromiseFulfilledResult<string> =>
+			promise.status === "fulfilled",
+	)[0]?.value;
+};
+
+export const getRelativePathOfActiveFile = async (): Promise<string> => {
+	const activeEditor = getActiveTextEditor();
+
+	if (!validEditor(activeEditor)) {
+		return "";
+	}
+
+	const { fileName } = activeEditor.document;
+	return runGit(fileName, "ls-files", "--full-name", "--", fileName);
+};
+
+export const getDefaultBranch = async (remote: string): Promise<string> => {
+	const activeEditor = getActiveTextEditor();
+
+	if (!validEditor(activeEditor)) {
+		return "";
+	}
+
+	const rawRemoteDefaultBranch = await runGit(
+		activeEditor.document.fileName,
+		"rev-parse",
+		"--abbrev-ref",
+		`${remote}/HEAD`,
+	);
+
+	return split(rawRemoteDefaultBranch, "/")[1];
+};
+
+export const getGitEmail = async (
+	realFileName: string,
+): Promise<string | undefined> => {
+	const email = await runGit(dirname(realFileName), "config", "user.email");
+
+	if (email === "") {
+		return undefined;
+	}
+
+	return `<${email}>`;
+};
diff --git a/src/git/util/is-hash.ts b/src/git/util/is-hash.ts
new file mode 100644
index 00000000..1e839d41
--- /dev/null
+++ b/src/git/util/is-hash.ts
@@ -0,0 +1,14 @@
+import type { Commit } from "./stream-parsing.js";
+
+export function isHash(hash: string, allowHead = false): boolean {
+	if (allowHead && hash === "HEAD") {
+		return true;
+	}
+	const length = hash.length;
+	return (length === 40 || length === 64) && /^[a-z0-9]+$/.test(hash);
+}
+
+export function isUncommitted(commit: Commit): boolean {
+	const length = commit.hash.length;
+	return (length === 40 || length === 64) && /^0+$/.test(commit.hash);
+}
diff --git a/src/git/util/origin-url-to-tool-url.ts b/src/git/util/origin-url-to-tool-url.ts
new file mode 100644
index 00000000..45c4e510
--- /dev/null
+++ b/src/git/util/origin-url-to-tool-url.ts
@@ -0,0 +1,19 @@
+import { URL } from "node:url";
+
+import { stripGitRemoteUrl } from "./strip-git-remote-url.js";
+
+export const originUrlToToolUrl = (url: string): URL | undefined => {
+	const httpProtocol = /^(https?):/.exec(url)?.[1];
+
+	let uri: URL;
+
+	try {
+		uri = new URL(`${httpProtocol ?? "https"}://${stripGitRemoteUrl(url)}`);
+	} catch {
+		return;
+	}
+
+	uri.port = httpProtocol ? uri.port : "";
+
+	return uri;
+};
diff --git a/src/git/util/project-name-from-origin.ts b/src/git/util/project-name-from-origin.ts
new file mode 100644
index 00000000..3a4f4c61
--- /dev/null
+++ b/src/git/util/project-name-from-origin.ts
@@ -0,0 +1,2 @@
+export const projectNameFromOrigin = (origin: string): string =>
+	/([a-zA-Z0-9_~%+.-]*?)(\.git)?$/.exec(origin)?.[1] ?? "";
diff --git a/src/git/util/stream-parsing.ts b/src/git/util/stream-parsing.ts
new file mode 100644
index 00000000..fde1e7b7
--- /dev/null
+++ b/src/git/util/stream-parsing.ts
@@ -0,0 +1,199 @@
+import { split } from "../../util/split.js";
+import { isHash } from "./is-hash.js";
+
+export type CommitAuthor = {
+	name: string;
+	mail: string;
+	isCurrentUser: boolean;
+	timestamp: string;
+	date: Date;
+	tz: string;
+};
+
+export type Commit = {
+	hash: string;
+	author: CommitAuthor;
+	committer: CommitAuthor;
+	summary: string;
+};
+
+export type FileAttachedCommit<T = Commit> = {
+	commit: T;
+	filename: string;
+};
+
+export type Line = {
+	source: number;
+	result: number;
+};
+
+export type LineAttachedCommit<T = Commit> = FileAttachedCommit<T> & {
+	line: Line;
+};
+
+export type CommitRegistry = Map<string, Commit>;
+
+const newCommitInfo = (hash: string): Commit => ({
+	author: {
+		mail: "",
+		name: "",
+		isCurrentUser: false,
+		timestamp: "",
+		date: new Date(),
+		tz: "",
+	},
+	committer: {
+		mail: "",
+		name: "",
+		isCurrentUser: false,
+		timestamp: "",
+		date: new Date(),
+		tz: "",
+	},
+	hash: hash,
+	summary: "",
+});
+
+const newLocationAttachedCommit = (commitInfo: Commit): FileAttachedCommit => ({
+	commit: commitInfo,
+	filename: "",
+});
+
+const WAIT_N_LINES = 10;
+
+async function* splitChunk(chunk: Buffer): AsyncGenerator<[string, string]> {
+	let lastIndex = 0;
+	let lineCount = 1;
+	while (lastIndex < chunk.length) {
+		const nextIndex = chunk.indexOf("\n", lastIndex);
+
+		yield split(chunk.toString("utf8", lastIndex, nextIndex));
+
+		// This is an attempt to mitigate main thread hogging.
+		if (lineCount % WAIT_N_LINES === 0) {
+			await new Promise<void>((resolve) => setImmediate(resolve));
+		}
+
+		lineCount += 1;
+		lastIndex = nextIndex + 1;
+	}
+}
+
+const fillOwner = (
+	owner: CommitAuthor,
+	dataPoint: string,
+	value: string,
+	currentUserEmail: string | undefined,
+): void => {
+	if (dataPoint === "time") {
+		owner.timestamp = value;
+		owner.date = new Date(Number.parseInt(value, 10) * 1000);
+	} else if (dataPoint === "tz") {
+		owner.tz = value;
+	} else if (dataPoint === "mail") {
+		owner.mail = value;
+		owner.isCurrentUser = currentUserEmail === value;
+	} else if (dataPoint === "") {
+		owner.name = value;
+	}
+};
+
+const processAuthorLine = (
+	key: string,
+	value: string,
+	commitInfo: Commit,
+	currentUserEmail: string | undefined,
+): void => {
+	const [author, dataPoint] = split(key, "-");
+
+	if (author === "author" || author === "committer") {
+		fillOwner(commitInfo[author], dataPoint, value, currentUserEmail);
+	}
+};
+
+const isCoverageLine = (hash: string, coverage: string): boolean =>
+	isHash(hash) && /^\d+ \d+ \d+$/.test(coverage);
+
+const processLine = (
+	key: string,
+	value: string,
+	commitInfo: Commit,
+	currentUserEmail: string | undefined,
+): void => {
+	if (key === "summary") {
+		commitInfo.summary = value;
+	} else if (isHash(key)) {
+		commitInfo.hash = key;
+	} else if (key.startsWith("author") || key.startsWith("committer")) {
+		processAuthorLine(key, value, commitInfo, currentUserEmail);
+	}
+};
+
+function* processCoverage(coverage: string): Generator<Line> {
+	const [source, result, lines] = coverage.split(" ").map(Number);
+
+	for (let i = 0; i < lines; i++) {
+		yield {
+			source: source + i,
+			result: result + i,
+		};
+	}
+}
+
+function* commitFilter(
+	fileAttached: FileAttachedCommit | undefined,
+	lines: Generator<Line> | undefined,
+	registry: CommitRegistry,
+): Generator<LineAttachedCommit> {
+	if (fileAttached === undefined || lines === undefined) {
+		return;
+	}
+
+	registry.set(fileAttached.commit.hash, fileAttached.commit);
+
+	for (const line of lines) {
+		yield {
+			...fileAttached,
+			line,
+		};
+	}
+}
+
+/**
+ * Here we process incremental git blame output. Two things are important to understand:
+ *   - Commit info blocks always start with hash/line-info and end with filename
+ *   - What it contains can change with future git versions
+ *
+ * @see https://github.com/git/git/blob/9d530dc/Documentation/git-blame.txt#L198
+ *
+ * @param dataChunk Chunk of `--incremental` git blame output
+ * @param commitRegistry Keeping track of previously encountered commit information
+ */
+export async function* processChunk(
+	dataChunk: Buffer,
+	currentUserEmail: string | undefined,
+	commitRegistry: CommitRegistry,
+): AsyncGenerator<LineAttachedCommit, void> {
+	let commitLocation: FileAttachedCommit | undefined;
+	let coverageGenerator: Generator<Line> | undefined;
+
+	for await (const [key, value] of splitChunk(dataChunk)) {
+		if (isCoverageLine(key, value)) {
+			commitLocation = newLocationAttachedCommit(
+				commitRegistry.get(key) ?? newCommitInfo(key),
+			);
+			coverageGenerator = processCoverage(value);
+		}
+
+		if (commitLocation) {
+			if (key === "filename") {
+				commitLocation.filename = value;
+				yield* commitFilter(commitLocation, coverageGenerator, commitRegistry);
+			} else {
+				processLine(key, value, commitLocation.commit, currentUserEmail);
+			}
+		}
+	}
+
+	yield* commitFilter(commitLocation, coverageGenerator, commitRegistry);
+}
diff --git a/src/git/util/strip-git-remote-url.ts b/src/git/util/strip-git-remote-url.ts
new file mode 100644
index 00000000..639570e8
--- /dev/null
+++ b/src/git/util/strip-git-remote-url.ts
@@ -0,0 +1,10 @@
+export const stripGitSuffix = (rawUrl: string): string =>
+	rawUrl.replace(/\.git$/i, "");
+
+export const stripGitRemoteUrl = (rawUrl: string): string =>
+	// Remove .git-suffix
+	stripGitSuffix(rawUrl)
+		// Remove protocol, username and/or password
+		.replace(/^([a-z-]+:\/\/)?([\w%:\\]+?@)?/i, "")
+		// Convert hostname:path to hostname/path
+		.replace(/:([a-z_.~+%-][a-z0-9_.~+%-]+)\/?/i, "/$1/");
diff --git a/src/gitblame.ts b/src/gitblame.ts
deleted file mode 100644
index 86a77be4..00000000
--- a/src/gitblame.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-
-
-export class GitBlame {
-    
-    private _blamed: Object;
-    
-    constructor(private repoPath: string, private gitBlameProcess) {
-        this._blamed = {};
-    }
-    
-    getBlameInfo(fileName: string): Thenable<any> {
-        const self = this;
-        return new Promise<any>((resolve, reject) => {
-            
-            if (self.needsBlame(fileName)) {
-                self.blameFile(self.repoPath, fileName).then((blameInfo) => {
-                    self._blamed[fileName] = blameInfo;
-                    resolve(blameInfo);
-                }, (err) => {
-                    reject();
-                });
-            } else {
-                resolve(self._blamed[fileName]);
-            }
-        });
-    }
-    
-    needsBlame(fileName: string): boolean {
-        return !(fileName in this._blamed);
-    }
-    
-    blameFile(repo: string, fileName: string): Thenable<Object> {
-        const self = this;
-        return new Promise<Object>((resolve, reject) => {
-            const blameInfo = {
-                'lines': {},
-                'commits': {}
-            };
-            
-            self.gitBlameProcess(repo, {
-                file: fileName
-            }).on('data', (type, data) => {
-                // outputs in Porcelain format.
-                if (type === 'line') {
-                    blameInfo['lines'][data.finalLine] = data;
-                } else if (type === 'commit' && !(data.hash in blameInfo['commits'])) {
-                    blameInfo['commits'][data.hash] = data;
-                }
-            }).on('error', (err) => {
-                reject(err);
-            }).on('end', () => {
-                resolve(blameInfo)
-            });
-        });
-    }
-    
-    dispose() {
-        // Nothing to release.
-    }
-}
-
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 00000000..1ff6b9d3
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,34 @@
+import { commands, type Disposable, type ExtensionContext } from "vscode";
+
+import { Extension } from "./git/extension.js";
+import { Logger } from "./util/logger.js";
+
+const registerCommand = (name: string, callback: () => void): Disposable => {
+	return commands.registerCommand(`gitblame.${name}`, callback);
+};
+
+export const activate = (context: ExtensionContext): void => {
+	const app = new Extension();
+
+	context.subscriptions.push(
+		app,
+		Logger.getInstance(),
+		registerCommand("quickInfo", () => {
+			app.showMessage();
+		}),
+		registerCommand("online", () => {
+			app.blameLink();
+		}),
+		registerCommand("addCommitHashToClipboard", () => {
+			app.copyHash();
+		}),
+		registerCommand("addToolUrlToClipboard", () => {
+			app.copyToolUrl();
+		}),
+		registerCommand("gitShow", () => {
+			app.runGitShow();
+		}),
+	);
+
+	app.updateView();
+};
diff --git a/src/util/ago.ts b/src/util/ago.ts
new file mode 100644
index 00000000..1de47ee0
--- /dev/null
+++ b/src/util/ago.ts
@@ -0,0 +1,28 @@
+import { env } from "vscode";
+
+const SECOND = 1000;
+const MINUTE = 60 * SECOND;
+const HOUR = 60 * MINUTE;
+const DAY = 24 * HOUR;
+const YEAR = 365.25 * DAY;
+const MONTH = YEAR / 12;
+
+const timeUnits: [Intl.RelativeTimeFormatUnit, number][] = [
+	["year", YEAR],
+	["month", MONTH],
+	["day", DAY],
+	["hour", HOUR],
+	["minute", MINUTE],
+];
+export const between = (now: Date, compare: Date): string => {
+	const diff = now.valueOf() - compare.valueOf();
+	const relativeTime = new Intl.RelativeTimeFormat(env.language);
+
+	for (const [unit, scale] of timeUnits) {
+		if (diff > scale) {
+			return relativeTime.format(-1 * Math.round(diff / scale), unit);
+		}
+	}
+
+	return "right now";
+};
diff --git a/src/util/execute.ts b/src/util/execute.ts
new file mode 100644
index 00000000..9b1828cc
--- /dev/null
+++ b/src/util/execute.ts
@@ -0,0 +1,27 @@
+import { type ExecOptions, execFile } from "node:child_process";
+
+import { Logger } from "./logger.js";
+
+export const execute = async (
+	command: string,
+	args: string[],
+	options: ExecOptions = {},
+): Promise<string> => {
+	Logger.info(`${command} ${args.join(" ")}`);
+
+	return new Promise((resolve) =>
+		execFile(
+			command,
+			args,
+			{ ...options, encoding: "utf8" },
+			(error, stdout, stderr): void => {
+				if (error || stderr) {
+					Logger.error(error || stderr);
+					resolve("");
+				} else {
+					resolve(stdout.trim());
+				}
+			},
+		),
+	);
+};
diff --git a/src/util/get-active.ts b/src/util/get-active.ts
new file mode 100644
index 00000000..92506b18
--- /dev/null
+++ b/src/util/get-active.ts
@@ -0,0 +1,16 @@
+import { window } from "vscode";
+
+import type { PartialTextEditor } from "./valid-editor.js";
+
+export const getActiveTextEditor = (): PartialTextEditor | undefined =>
+	window.activeTextEditor;
+
+export const NO_FILE_OR_PLACE = "N:-1";
+
+export const getFilePosition = ({
+	document,
+	selection,
+}: PartialTextEditor): string =>
+	document.uri.scheme !== "file"
+		? NO_FILE_OR_PLACE
+		: `${document.fileName}:${selection.active.line}`;
diff --git a/src/util/is-url.ts b/src/util/is-url.ts
new file mode 100644
index 00000000..39dfcb5c
--- /dev/null
+++ b/src/util/is-url.ts
@@ -0,0 +1,19 @@
+import { URL } from "node:url";
+
+export const isUrl = (check: string): boolean => {
+	let url: URL;
+	try {
+		url = new URL(check);
+	} catch {
+		return false;
+	}
+
+	if (
+		url.href !== check ||
+		(url.protocol !== "http:" && url.protocol !== "https:")
+	) {
+		return false;
+	}
+
+	return !!(url.hostname && url.pathname);
+};
diff --git a/src/util/logger.ts b/src/util/logger.ts
new file mode 100644
index 00000000..d0e51049
--- /dev/null
+++ b/src/util/logger.ts
@@ -0,0 +1,41 @@
+import { format } from "node:util";
+import { type LogOutputChannel, window } from "vscode";
+
+export class Logger {
+	private static instance?: Logger;
+	private readonly out: LogOutputChannel;
+
+	public static getInstance(): Logger {
+		Logger.instance ??= new Logger();
+		return Logger.instance;
+	}
+
+	private constructor() {
+		this.out = window.createOutputChannel("Git Blame", {
+			log: true,
+		});
+	}
+
+	public static error(error: unknown): void {
+		if (error instanceof Error) {
+			Logger.getInstance().out.error(error);
+		}
+	}
+
+	public static info(info: string): void {
+		Logger.getInstance().out.info(info);
+	}
+
+	public static debug(debug: string, ...args: (string | number)[]): void {
+		Logger.getInstance().out.debug(format(debug, ...args));
+	}
+
+	public static trace(debug: string, ...args: (string | number)[]): void {
+		Logger.getInstance().out.trace(format(debug, ...args));
+	}
+
+	public dispose(): void {
+		Logger.instance = undefined;
+		this.out.dispose();
+	}
+}
diff --git a/src/util/message.ts b/src/util/message.ts
new file mode 100644
index 00000000..39ddc8ef
--- /dev/null
+++ b/src/util/message.ts
@@ -0,0 +1,14 @@
+import { type MessageItem, window } from "vscode";
+
+export const infoMessage = <T extends MessageItem>(
+	message: string,
+	item: undefined | T[] = [],
+): Promise<T | undefined> => {
+	return Promise.resolve(window.showInformationMessage(message, ...item));
+};
+
+export const errorMessage = (
+	message: string,
+	...items: string[]
+): Promise<string | undefined> =>
+	Promise.resolve(window.showErrorMessage(message, ...items));
diff --git a/src/util/property.ts b/src/util/property.ts
new file mode 100644
index 00000000..818ff26d
--- /dev/null
+++ b/src/util/property.ts
@@ -0,0 +1,33 @@
+import { workspace } from "vscode";
+
+export type PropertiesMap = {
+	commitUrl: string;
+	remoteName: string;
+	ignoreWhitespace: boolean;
+	infoMessageFormat: string;
+	statusBarMessageFormat: string;
+	statusBarMessageNoCommit: string;
+	statusBarPositionPriority: number | undefined;
+	pluralWebPathSubstrings: string[] | undefined;
+	statusBarMessageClickAction:
+		| "Show info message"
+		| "Open tool URL"
+		| "Open git show"
+		| "Copy hash to clipboard";
+	inlineMessageFormat: string;
+	inlineMessageNoCommit: string;
+	inlineMessageEnabled: boolean;
+	inlineMessageMargin: number;
+	currentUserAlias: string;
+	delayBlame: number;
+	parallelBlames: number;
+	maxLineCount: number;
+	revsFile: string[];
+};
+
+// getConfiguration has an unfortunate typing that does not
+// take any possible default values into consideration.
+export const getProperty = <Key extends keyof PropertiesMap>(
+	name: Key,
+): PropertiesMap[Key] =>
+	workspace.getConfiguration("gitblame").get(name) as PropertiesMap[Key];
diff --git a/src/util/split.ts b/src/util/split.ts
new file mode 100644
index 00000000..ff0ba48a
--- /dev/null
+++ b/src/util/split.ts
@@ -0,0 +1,9 @@
+export const split = (target: string, char = " "): [string, string] => {
+	const index = target.indexOf(char[0]);
+
+	if (index === -1) {
+		return [target, ""];
+	}
+
+	return [target.substr(0, index), target.substr(index + 1).trim()];
+};
diff --git a/src/util/text-decorator.ts b/src/util/text-decorator.ts
new file mode 100644
index 00000000..bf30dc8b
--- /dev/null
+++ b/src/util/text-decorator.ts
@@ -0,0 +1,200 @@
+import type { Commit, CommitAuthor } from "../git/util/stream-parsing.js";
+import { between } from "./ago.js";
+import { getProperty } from "./property.js";
+
+type InfoTokenFunctionWithParameter = (value?: string) => string;
+type InfoTokenFunction = InfoTokenFunctionWithParameter | string;
+
+export type InfoTokens = {
+	[key: string]: InfoTokenFunction | undefined;
+};
+
+export type InfoTokenNormalizedCommitInfo = {
+	"author.mail": string;
+	"author.name": string;
+	"author.timestamp": string;
+	"author.tz": string;
+	"author.date": string;
+	"commit.hash": InfoTokenFunctionWithParameter;
+	"commit.hash_short": InfoTokenFunctionWithParameter;
+	"commit.summary": InfoTokenFunctionWithParameter;
+	"committer.mail": string;
+	"committer.name": string;
+	"committer.timestamp": string;
+	"committer.tz": string;
+	"committer.date": string;
+	"time.ago": string;
+	"time.c_ago": string;
+};
+
+type TokenReplaceGroup = [InfoTokenFunction, string?, string?];
+
+export const normalizeCommitInfoTokens = ({
+	author,
+	committer,
+	hash,
+	summary,
+}: Commit): InfoTokenNormalizedCommitInfo => {
+	const now = new Date();
+	const toIso = ({ date }: CommitAuthor) => date.toISOString().slice(0, 10);
+
+	const ago = between(now, author.date);
+	const cAgo = between(now, committer.date);
+	const shortness =
+		(target: string, fallbackLength: string) =>
+		(length = ""): string => {
+			return target.substr(0, Number.parseInt(length || fallbackLength, 10));
+		};
+	const currentUserAlias = getProperty("currentUserAlias");
+
+	return {
+		"author.mail": author.mail,
+		"author.name":
+			author.isCurrentUser && currentUserAlias ? currentUserAlias : author.name,
+		"author.timestamp": author.timestamp,
+		"author.tz": author.tz,
+		"author.date": toIso(author),
+		"committer.mail": committer.mail,
+		"committer.name":
+			committer.isCurrentUser && currentUserAlias
+				? currentUserAlias
+				: committer.name,
+		"committer.timestamp": committer.timestamp,
+		"committer.tz": committer.tz,
+		"committer.date": toIso(committer),
+		"commit.hash": shortness(hash, "64"),
+		"commit.hash_short": shortness(hash, "7"),
+		"commit.summary": shortness(summary, "65536"),
+		"time.ago": ago,
+		"time.c_ago": cAgo,
+	};
+};
+
+enum MODE {
+	OUT = 0,
+	IN = 1,
+	START = 2,
+}
+
+const createIndexOrEnd =
+	(target: string, index: number, endIndex: number) => (char: string) => {
+		const indexOfChar = target.indexOf(char, index);
+		if (indexOfChar === -1 || indexOfChar > endIndex) {
+			return endIndex;
+		}
+
+		return indexOfChar;
+	};
+const createSubSectionOrEmpty =
+	(target: string, endIndex: number) =>
+	(startIndex: number, lastIndex: number) => {
+		if (lastIndex === startIndex || endIndex === startIndex) {
+			return "";
+		}
+
+		return target.substring(startIndex + 1, lastIndex);
+	};
+
+function createTokenReplaceGroup<T extends InfoTokens>(
+	infoTokens: T,
+	target: string,
+	index: number,
+): TokenReplaceGroup {
+	const endIndex = target.indexOf("}", index);
+	const indexOrEnd = createIndexOrEnd(target, index, endIndex);
+	const subSectionOrEmpty = createSubSectionOrEmpty(target, endIndex);
+
+	const parameterIndex = indexOrEnd(",");
+	const modifierIndex = indexOrEnd("|");
+	const functionName = target.substring(
+		index,
+		Math.min(parameterIndex, modifierIndex),
+	);
+
+	return [
+		infoTokens[functionName] ?? functionName,
+		subSectionOrEmpty(modifierIndex, endIndex),
+		subSectionOrEmpty(parameterIndex, modifierIndex),
+	];
+}
+
+function* parse<T extends InfoTokens>(
+	target: string,
+	infoTokens: T,
+): Generator<TokenReplaceGroup> {
+	let lastSplit = 0;
+	let startIndex = 0;
+	let mode = MODE.OUT;
+
+	for (let index = 0; index < target.length; index++) {
+		if (mode === MODE.OUT && target[index] === "$") {
+			mode = MODE.START;
+		} else if (mode === MODE.START && target[index] === "{") {
+			mode = MODE.IN;
+			startIndex = index - 1;
+			yield [target.slice(lastSplit, startIndex)];
+			lastSplit = startIndex;
+		} else if (mode === MODE.START) {
+			mode = MODE.OUT;
+		} else if (mode === MODE.IN) {
+			mode = MODE.OUT;
+			const endIndex = target.indexOf("}", index);
+			if (endIndex === -1) {
+				break;
+			}
+
+			yield createTokenReplaceGroup(infoTokens, target, index);
+
+			lastSplit = endIndex + 1;
+		}
+	}
+
+	yield [target.slice(lastSplit)];
+}
+
+const modify = (value: string, modifier = ""): string => {
+	if (modifier === "u") {
+		return value.toUpperCase();
+	}
+	if (modifier === "l") {
+		return value.toLowerCase();
+	}
+	if (modifier) {
+		return `${value}|${modifier}`;
+	}
+
+	return value;
+};
+
+const sanitizeToken = (token: string): string => {
+	return token.replace(/\u202e/g, "");
+};
+
+export const parseTokens = <T extends InfoTokens>(
+	target: string,
+	infoTokens: T,
+): string => {
+	let out = "";
+
+	for (const [funcStr, mod, param] of parse(target, infoTokens)) {
+		if (typeof funcStr === "string") {
+			out += modify(funcStr, mod);
+		} else {
+			out += modify(funcStr(param), mod);
+		}
+	}
+
+	return sanitizeToken(out);
+};
+
+export const toStatusBarTextView = (commit: Commit): string =>
+	parseTokens(
+		getProperty("statusBarMessageFormat"),
+		normalizeCommitInfoTokens(commit),
+	);
+
+export const toInlineTextView = (commit: Commit): string =>
+	parseTokens(
+		getProperty("inlineMessageFormat"),
+		normalizeCommitInfoTokens(commit),
+	);
diff --git a/src/util/valid-editor.ts b/src/util/valid-editor.ts
new file mode 100644
index 00000000..7ce66335
--- /dev/null
+++ b/src/util/valid-editor.ts
@@ -0,0 +1,24 @@
+import type {
+	Position as FullPosition,
+	TextDocument,
+	TextEditor,
+} from "vscode";
+
+export type Document = Pick<
+	TextDocument,
+	"uri" | "isUntitled" | "fileName" | "lineCount"
+>;
+export type Position = Pick<FullPosition, "line">;
+export type PartialSelection = {
+	active: Position;
+};
+export type PartialTextEditor = {
+	readonly document: Document;
+	selection: PartialSelection;
+
+	setDecorations?: TextEditor["setDecorations"];
+};
+
+export const validEditor = (
+	editor?: PartialTextEditor,
+): editor is PartialTextEditor => editor?.document.uri.scheme === "file";
diff --git a/src/view.ts b/src/view.ts
index 1cbf71e5..9644751a 100644
--- a/src/view.ts
+++ b/src/view.ts
@@ -1,30 +1,221 @@
+import {
+	type Disposable,
+	Position,
+	Range,
+	StatusBarAlignment,
+	type StatusBarItem,
+	type TextEditorDecorationType,
+	ThemeColor,
+	window,
+	workspace,
+} from "vscode";
+import { isUncommitted } from "./git/util/is-hash.js";
+import type { Commit } from "./git/util/stream-parsing.js";
+import { getActiveTextEditor } from "./util/get-active.js";
+import { Logger } from "./util/logger.js";
+import { getProperty } from "./util/property.js";
+import {
+	toInlineTextView,
+	toStatusBarTextView,
+} from "./util/text-decorator.js";
+import { type PartialTextEditor, validEditor } from "./util/valid-editor.js";
 
-import {StatusBarItem} from 'vscode';
+const MESSAGE_NO_INFO = "No info about the current line";
 
+export class StatusBarView {
+	private statusBar: StatusBarItem;
+	private readonly decorationType: TextEditorDecorationType;
+	private readonly configChange: Disposable;
+	private readonly ongoingViewUpdateRejects: Set<() => void> = new Set();
 
-export interface IView {
-    
-    /**
-     * Refresh the view. 
-     */
-    refresh(text: string): void;
-}
+	private statusBarText = "";
+	private statusBarTooltip = "";
+	private statusBarCommand = false;
+	private statusBarPriority: number | undefined = undefined;
 
-export class StatusBarView implements IView {
-    
-    private _statusBarItem: StatusBarItem;
-    
-    constructor(statusBarItem: StatusBarItem) {
-        this._statusBarItem = statusBarItem;
-        this._statusBarItem.command = "extension.blame"
-    };
-    
-    refresh(text: string) {
-        this._statusBarItem.text = '$(git-commit) ' + text;
-        this._statusBarItem.tooltip = 'git blame';
-        // this._statusBarItem.command = 'extension.blame';
-        this._statusBarItem.show();
-    }
-}
+	constructor() {
+		this.decorationType = window.createTextEditorDecorationType({});
+		this.statusBarPriority = getProperty("statusBarPositionPriority");
+
+		this.statusBar = this.createStatusBarItem();
+		this.configChange = workspace.onDidChangeConfiguration((e) => {
+			if (e.affectsConfiguration("gitblame")) {
+				const newPriority = getProperty("statusBarPositionPriority");
+				if (this.statusBarPriority !== newPriority) {
+					this.statusBarPriority = newPriority;
+					this.statusBar = this.createStatusBarItem();
+				}
+			}
+		});
+	}
+
+	public set(
+		commit: Commit | undefined,
+		editor: PartialTextEditor | undefined,
+		useDelay = true,
+	): void {
+		if (!commit) {
+			this.clear();
+		} else if (isUncommitted(commit)) {
+			this.text(
+				getProperty("statusBarMessageNoCommit"),
+				false,
+				MESSAGE_NO_INFO,
+			);
+			if (editor) {
+				void this.createLineDecoration(
+					getProperty("inlineMessageNoCommit"),
+					editor,
+					useDelay,
+				);
+			}
+		} else {
+			this.text(toStatusBarTextView(commit), true);
+			if (editor) {
+				void this.createLineDecoration(
+					toInlineTextView(commit),
+					editor,
+					useDelay,
+				);
+			}
+		}
+	}
+
+	public clear(): void {
+		this.text("", false, MESSAGE_NO_INFO);
+		this.removeLineDecoration();
+	}
+
+	public activity(): void {
+		this.text("$(extensions-refresh)", false, "Waiting for git blame response");
+	}
+
+	public fileToLong(): void {
+		const maxLineCount = getProperty("maxLineCount");
+		this.text(
+			"",
+			false,
+			`No blame information is available. File has more than ${maxLineCount} lines`,
+		);
+	}
+
+	public dispose(): void {
+		this.statusBar?.dispose();
+		this.decorationType.dispose();
+		this.configChange.dispose();
+	}
+
+	private command(): string {
+		return {
+			"Open tool URL": "gitblame.online",
+			"Open git show": "gitblame.gitShow",
+			"Copy hash to clipboard": "gitblame.addCommitHashToClipboard",
+			"Show info message": "gitblame.quickInfo",
+		}[getProperty("statusBarMessageClickAction")];
+	}
+
+	private updateStatusBar(statusBar: StatusBarItem) {
+		statusBar.text = this.statusBarText;
+		statusBar.tooltip = this.statusBarTooltip;
+		statusBar.command = this.statusBarCommand ? this.command() : undefined;
+
+		Logger.debug(
+			"Updating status bar item with: Text:'%s', tooltip:'git blame%s', command:%s",
+			statusBar.text,
+			statusBar.tooltip,
+			statusBar.command ?? "",
+		);
+	}
+
+	private text(text: string, command: boolean, tooltip = ""): void {
+		const suffix = command || !tooltip ? "" : ` - ${tooltip}`;
+		this.statusBarText = `$(git-commit) ${text.trimEnd()}`;
+		this.statusBarTooltip = `git blame${suffix}`;
+		this.statusBarCommand = command;
 
+		this.updateStatusBar(this.statusBar);
+	}
 
+	private createStatusBarItem(): StatusBarItem {
+		this.statusBar?.dispose();
+
+		const statusBar = window.createStatusBarItem(
+			StatusBarAlignment.Right,
+			this.statusBarPriority,
+		);
+
+		this.updateStatusBar(statusBar);
+
+		statusBar.show();
+
+		return statusBar;
+	}
+
+	private async createLineDecoration(
+		text: string,
+		editor: PartialTextEditor,
+		useDelay: boolean,
+	): Promise<void> {
+		if (!getProperty("inlineMessageEnabled")) {
+			return;
+		}
+
+		this.removeLineDecoration();
+		// Add new decoration
+		if (useDelay && (await this.delayUpdate(getProperty("delayBlame")))) {
+			const margin = getProperty("inlineMessageMargin");
+			const decorationPosition = new Position(
+				editor.selection.active.line,
+				Number.MAX_SAFE_INTEGER,
+			);
+			editor.setDecorations?.(this.decorationType, [
+				{
+					renderOptions: {
+						after: {
+							contentText: text,
+							margin: `0 0 0 ${margin}rem`,
+							color: new ThemeColor("gitblame.inlineMessage"),
+						},
+					},
+					range: new Range(decorationPosition, decorationPosition),
+				},
+			]);
+		}
+	}
+
+	private removeLineDecoration(): void {
+		const editor = getActiveTextEditor();
+		editor?.setDecorations?.(this.decorationType, []);
+	}
+
+	public preUpdate(
+		textEditor: PartialTextEditor | undefined,
+	): textEditor is PartialTextEditor {
+		if (!validEditor(textEditor)) {
+			this.clear();
+			return false;
+		}
+		for (const rejects of this.ongoingViewUpdateRejects) {
+			rejects();
+		}
+		this.ongoingViewUpdateRejects.clear();
+		this.activity();
+
+		return true;
+	}
+
+	private async delayUpdate(delay: number): Promise<boolean> {
+		if (delay > 0) {
+			try {
+				return await new Promise((resolve, reject) => {
+					this.ongoingViewUpdateRejects.add(reject);
+					setTimeout(() => resolve(true), delay);
+				});
+			} catch {
+				return false;
+			}
+		}
+
+		return true;
+	}
+}
diff --git a/test/extension.test.ts b/test/extension.test.ts
deleted file mode 100644
index a306fbf0..00000000
--- a/test/extension.test.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-// 
-// Note: This example test is leveraging the Mocha test framework.
-// Please refer to their documentation on https://mochajs.org/ for help.
-//
-
-// The module 'assert' provides assertion methods from node
-import * as assert from 'assert';
-
-// You can import and use all API from the 'vscode' module
-// as well as import your extension to test it
-import * as vscode from 'vscode';
-import * as myExtension from '../src/extension';
-import {GitBlame} from '../src/gitblame';
-import {GitBlameController, TextDecorator} from '../src/controller';
-
-// Defines a Mocha test suite to group tests of similar kind together
-suite("Extension Tests", () => {
-
-	// Defines a Mocha unit test
-	test("Something 1", () => {
-		assert.equal(-1, [1, 2, 3].indexOf(5));
-		assert.equal(-1, [1, 2, 3].indexOf(0));
-	});
-});
-
-suite('GitBlame Tests', () => {
-    
-    test('Date Calculations', () => {
-        
-        const decorator = new TextDecorator();
-        
-        assert.equal('3 months ago', decorator.toDateText(new Date(2015, 4), new Date(2015, 1)));
-        assert.equal('4 days ago', decorator.toDateText(new Date(2015, 1, 5), new Date(2015, 1, 1)));
-        
-    });
-});
\ No newline at end of file
diff --git a/test/fixture/git-stream-blame-incremental-multi-chunk-result.json b/test/fixture/git-stream-blame-incremental-multi-chunk-result.json
new file mode 100644
index 00000000..13b0aea5
--- /dev/null
+++ b/test/fixture/git-stream-blame-incremental-multi-chunk-result.json
@@ -0,0 +1,3863 @@
+[
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"hash": "6d326b9edbf4c2ee1c3ad155d16012acf3c0cc0c",
+			"summary": "Increase precision of PDF layout dirty tracking"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 66,
+			"result": 66
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"hash": "6d326b9edbf4c2ee1c3ad155d16012acf3c0cc0c",
+			"summary": "Increase precision of PDF layout dirty tracking"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 67,
+			"result": 67
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"hash": "6d326b9edbf4c2ee1c3ad155d16012acf3c0cc0c",
+			"summary": "Increase precision of PDF layout dirty tracking"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 68,
+			"result": 68
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"hash": "6d326b9edbf4c2ee1c3ad155d16012acf3c0cc0c",
+			"summary": "Increase precision of PDF layout dirty tracking"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 69,
+			"result": 69
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"hash": "6d326b9edbf4c2ee1c3ad155d16012acf3c0cc0c",
+			"summary": "Increase precision of PDF layout dirty tracking"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 70,
+			"result": 70
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"hash": "6d326b9edbf4c2ee1c3ad155d16012acf3c0cc0c",
+			"summary": "Increase precision of PDF layout dirty tracking"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 71,
+			"result": 71
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"hash": "6d326b9edbf4c2ee1c3ad155d16012acf3c0cc0c",
+			"summary": "Increase precision of PDF layout dirty tracking"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 72,
+			"result": 72
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"hash": "6d326b9edbf4c2ee1c3ad155d16012acf3c0cc0c",
+			"summary": "Increase precision of PDF layout dirty tracking"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 73,
+			"result": 73
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"hash": "6d326b9edbf4c2ee1c3ad155d16012acf3c0cc0c",
+			"summary": "Increase precision of PDF layout dirty tracking"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 74,
+			"result": 74
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"hash": "6d326b9edbf4c2ee1c3ad155d16012acf3c0cc0c",
+			"summary": "Increase precision of PDF layout dirty tracking"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 75,
+			"result": 75
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"hash": "6d326b9edbf4c2ee1c3ad155d16012acf3c0cc0c",
+			"summary": "Increase precision of PDF layout dirty tracking"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 76,
+			"result": 76
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"hash": "6d326b9edbf4c2ee1c3ad155d16012acf3c0cc0c",
+			"summary": "Increase precision of PDF layout dirty tracking"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 77,
+			"result": 77
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"hash": "6d326b9edbf4c2ee1c3ad155d16012acf3c0cc0c",
+			"summary": "Increase precision of PDF layout dirty tracking"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 78,
+			"result": 78
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"hash": "6d326b9edbf4c2ee1c3ad155d16012acf3c0cc0c",
+			"summary": "Increase precision of PDF layout dirty tracking"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 79,
+			"result": 79
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"hash": "6d326b9edbf4c2ee1c3ad155d16012acf3c0cc0c",
+			"summary": "Increase precision of PDF layout dirty tracking"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 119,
+			"result": 119
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"hash": "6d326b9edbf4c2ee1c3ad155d16012acf3c0cc0c",
+			"summary": "Increase precision of PDF layout dirty tracking"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 120,
+			"result": 120
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"hash": "6d326b9edbf4c2ee1c3ad155d16012acf3c0cc0c",
+			"summary": "Increase precision of PDF layout dirty tracking"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 121,
+			"result": 121
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"hash": "6d326b9edbf4c2ee1c3ad155d16012acf3c0cc0c",
+			"summary": "Increase precision of PDF layout dirty tracking"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 122,
+			"result": 122
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"hash": "6d326b9edbf4c2ee1c3ad155d16012acf3c0cc0c",
+			"summary": "Increase precision of PDF layout dirty tracking"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 123,
+			"result": 123
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"hash": "6d326b9edbf4c2ee1c3ad155d16012acf3c0cc0c",
+			"summary": "Increase precision of PDF layout dirty tracking"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 126,
+			"result": 126
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"hash": "6d326b9edbf4c2ee1c3ad155d16012acf3c0cc0c",
+			"summary": "Increase precision of PDF layout dirty tracking"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 127,
+			"result": 127
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"hash": "6d326b9edbf4c2ee1c3ad155d16012acf3c0cc0c",
+			"summary": "Increase precision of PDF layout dirty tracking"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 128,
+			"result": 128
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"hash": "6d326b9edbf4c2ee1c3ad155d16012acf3c0cc0c",
+			"summary": "Increase precision of PDF layout dirty tracking"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 129,
+			"result": 129
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"hash": "6d326b9edbf4c2ee1c3ad155d16012acf3c0cc0c",
+			"summary": "Increase precision of PDF layout dirty tracking"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 130,
+			"result": 130
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"hash": "6d326b9edbf4c2ee1c3ad155d16012acf3c0cc0c",
+			"summary": "Increase precision of PDF layout dirty tracking"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 131,
+			"result": 131
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"hash": "6d326b9edbf4c2ee1c3ad155d16012acf3c0cc0c",
+			"summary": "Increase precision of PDF layout dirty tracking"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 132,
+			"result": 132
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"hash": "6d326b9edbf4c2ee1c3ad155d16012acf3c0cc0c",
+			"summary": "Increase precision of PDF layout dirty tracking"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 133,
+			"result": 133
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1568932927",
+				"date": "2019-09-19T22:42:07.000Z",
+				"tz": "+0000"
+			},
+			"hash": "6d326b9edbf4c2ee1c3ad155d16012acf3c0cc0c",
+			"summary": "Increase precision of PDF layout dirty tracking"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 134,
+			"result": 134
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1566519176",
+				"date": "2019-08-23T00:12:56.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1566519176",
+				"date": "2019-08-23T00:12:56.000Z",
+				"tz": "+0000"
+			},
+			"hash": "e4bd75222c6a9379a8e3e4aea5364934445d8bb1",
+			"summary": "Clarify page rectangles in DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 77,
+			"result": 84
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1566519176",
+				"date": "2019-08-23T00:12:56.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1566519176",
+				"date": "2019-08-23T00:12:56.000Z",
+				"tz": "+0000"
+			},
+			"hash": "e4bd75222c6a9379a8e3e4aea5364934445d8bb1",
+			"summary": "Clarify page rectangles in DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 82,
+			"result": 89
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1566519176",
+				"date": "2019-08-23T00:12:56.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1566519176",
+				"date": "2019-08-23T00:12:56.000Z",
+				"tz": "+0000"
+			},
+			"hash": "e4bd75222c6a9379a8e3e4aea5364934445d8bb1",
+			"summary": "Clarify page rectangles in DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 83,
+			"result": 90
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1566519176",
+				"date": "2019-08-23T00:12:56.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1566519176",
+				"date": "2019-08-23T00:12:56.000Z",
+				"tz": "+0000"
+			},
+			"hash": "e4bd75222c6a9379a8e3e4aea5364934445d8bb1",
+			"summary": "Clarify page rectangles in DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 84,
+			"result": 91
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1566519176",
+				"date": "2019-08-23T00:12:56.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1566519176",
+				"date": "2019-08-23T00:12:56.000Z",
+				"tz": "+0000"
+			},
+			"hash": "e4bd75222c6a9379a8e3e4aea5364934445d8bb1",
+			"summary": "Clarify page rectangles in DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 85,
+			"result": 92
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1566519176",
+				"date": "2019-08-23T00:12:56.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1566519176",
+				"date": "2019-08-23T00:12:56.000Z",
+				"tz": "+0000"
+			},
+			"hash": "e4bd75222c6a9379a8e3e4aea5364934445d8bb1",
+			"summary": "Clarify page rectangles in DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 86,
+			"result": 93
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1566519176",
+				"date": "2019-08-23T00:12:56.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1566519176",
+				"date": "2019-08-23T00:12:56.000Z",
+				"tz": "+0000"
+			},
+			"hash": "e4bd75222c6a9379a8e3e4aea5364934445d8bb1",
+			"summary": "Clarify page rectangles in DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 87,
+			"result": 94
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1566519176",
+				"date": "2019-08-23T00:12:56.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1566519176",
+				"date": "2019-08-23T00:12:56.000Z",
+				"tz": "+0000"
+			},
+			"hash": "e4bd75222c6a9379a8e3e4aea5364934445d8bb1",
+			"summary": "Clarify page rectangles in DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 88,
+			"result": 95
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1566519176",
+				"date": "2019-08-23T00:12:56.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1566519176",
+				"date": "2019-08-23T00:12:56.000Z",
+				"tz": "+0000"
+			},
+			"hash": "e4bd75222c6a9379a8e3e4aea5364934445d8bb1",
+			"summary": "Clarify page rectangles in DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 89,
+			"result": 96
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1566519176",
+				"date": "2019-08-23T00:12:56.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1566519176",
+				"date": "2019-08-23T00:12:56.000Z",
+				"tz": "+0000"
+			},
+			"hash": "e4bd75222c6a9379a8e3e4aea5364934445d8bb1",
+			"summary": "Clarify page rectangles in DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 108,
+			"result": 110
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1566519176",
+				"date": "2019-08-23T00:12:56.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1566519176",
+				"date": "2019-08-23T00:12:56.000Z",
+				"tz": "+0000"
+			},
+			"hash": "e4bd75222c6a9379a8e3e4aea5364934445d8bb1",
+			"summary": "Clarify page rectangles in DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 109,
+			"result": 111
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1566519176",
+				"date": "2019-08-23T00:12:56.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1566519176",
+				"date": "2019-08-23T00:12:56.000Z",
+				"tz": "+0000"
+			},
+			"hash": "e4bd75222c6a9379a8e3e4aea5364934445d8bb1",
+			"summary": "Clarify page rectangles in DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 110,
+			"result": 112
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1566519176",
+				"date": "2019-08-23T00:12:56.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1566519176",
+				"date": "2019-08-23T00:12:56.000Z",
+				"tz": "+0000"
+			},
+			"hash": "e4bd75222c6a9379a8e3e4aea5364934445d8bb1",
+			"summary": "Clarify page rectangles in DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 111,
+			"result": 113
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1566519176",
+				"date": "2019-08-23T00:12:56.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1566519176",
+				"date": "2019-08-23T00:12:56.000Z",
+				"tz": "+0000"
+			},
+			"hash": "e4bd75222c6a9379a8e3e4aea5364934445d8bb1",
+			"summary": "Clarify page rectangles in DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 112,
+			"result": 114
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1566519176",
+				"date": "2019-08-23T00:12:56.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1566519176",
+				"date": "2019-08-23T00:12:56.000Z",
+				"tz": "+0000"
+			},
+			"hash": "e4bd75222c6a9379a8e3e4aea5364934445d8bb1",
+			"summary": "Clarify page rectangles in DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 113,
+			"result": 115
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1566519176",
+				"date": "2019-08-23T00:12:56.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1566519176",
+				"date": "2019-08-23T00:12:56.000Z",
+				"tz": "+0000"
+			},
+			"hash": "e4bd75222c6a9379a8e3e4aea5364934445d8bb1",
+			"summary": "Clarify page rectangles in DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 114,
+			"result": 116
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1566519176",
+				"date": "2019-08-23T00:12:56.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1566519176",
+				"date": "2019-08-23T00:12:56.000Z",
+				"tz": "+0000"
+			},
+			"hash": "e4bd75222c6a9379a8e3e4aea5364934445d8bb1",
+			"summary": "Clarify page rectangles in DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 115,
+			"result": 117
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1566519176",
+				"date": "2019-08-23T00:12:56.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1566519176",
+				"date": "2019-08-23T00:12:56.000Z",
+				"tz": "+0000"
+			},
+			"hash": "e4bd75222c6a9379a8e3e4aea5364934445d8bb1",
+			"summary": "Clarify page rectangles in DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 116,
+			"result": 118
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1566519176",
+				"date": "2019-08-23T00:12:56.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1566519176",
+				"date": "2019-08-23T00:12:56.000Z",
+				"tz": "+0000"
+			},
+			"hash": "e4bd75222c6a9379a8e3e4aea5364934445d8bb1",
+			"summary": "Clarify page rectangles in DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 122,
+			"result": 138
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565810396",
+				"date": "2019-08-14T19:19:56.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565810396",
+				"date": "2019-08-14T19:19:56.000Z",
+				"tz": "+0000"
+			},
+			"hash": "ff7ec676a7222317628ca23a1409ebaf691c52b9",
+			"summary": "Move page layout rectangles into DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 8,
+			"result": 8
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565810396",
+				"date": "2019-08-14T19:19:56.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565810396",
+				"date": "2019-08-14T19:19:56.000Z",
+				"tz": "+0000"
+			},
+			"hash": "ff7ec676a7222317628ca23a1409ebaf691c52b9",
+			"summary": "Move page layout rectangles into DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 11,
+			"result": 11
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565810396",
+				"date": "2019-08-14T19:19:56.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565810396",
+				"date": "2019-08-14T19:19:56.000Z",
+				"tz": "+0000"
+			},
+			"hash": "ff7ec676a7222317628ca23a1409ebaf691c52b9",
+			"summary": "Move page layout rectangles into DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 79,
+			"result": 86
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565810396",
+				"date": "2019-08-14T19:19:56.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565810396",
+				"date": "2019-08-14T19:19:56.000Z",
+				"tz": "+0000"
+			},
+			"hash": "ff7ec676a7222317628ca23a1409ebaf691c52b9",
+			"summary": "Move page layout rectangles into DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 80,
+			"result": 87
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565810396",
+				"date": "2019-08-14T19:19:56.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565810396",
+				"date": "2019-08-14T19:19:56.000Z",
+				"tz": "+0000"
+			},
+			"hash": "ff7ec676a7222317628ca23a1409ebaf691c52b9",
+			"summary": "Move page layout rectangles into DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 81,
+			"result": 88
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565810396",
+				"date": "2019-08-14T19:19:56.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565810396",
+				"date": "2019-08-14T19:19:56.000Z",
+				"tz": "+0000"
+			},
+			"hash": "ff7ec676a7222317628ca23a1409ebaf691c52b9",
+			"summary": "Move page layout rectangles into DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 83,
+			"result": 97
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565810396",
+				"date": "2019-08-14T19:19:56.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565810396",
+				"date": "2019-08-14T19:19:56.000Z",
+				"tz": "+0000"
+			},
+			"hash": "ff7ec676a7222317628ca23a1409ebaf691c52b9",
+			"summary": "Move page layout rectangles into DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 84,
+			"result": 98
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565810396",
+				"date": "2019-08-14T19:19:56.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565810396",
+				"date": "2019-08-14T19:19:56.000Z",
+				"tz": "+0000"
+			},
+			"hash": "ff7ec676a7222317628ca23a1409ebaf691c52b9",
+			"summary": "Move page layout rectangles into DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 85,
+			"result": 99
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565810396",
+				"date": "2019-08-14T19:19:56.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565810396",
+				"date": "2019-08-14T19:19:56.000Z",
+				"tz": "+0000"
+			},
+			"hash": "ff7ec676a7222317628ca23a1409ebaf691c52b9",
+			"summary": "Move page layout rectangles into DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 86,
+			"result": 100
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565810396",
+				"date": "2019-08-14T19:19:56.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565810396",
+				"date": "2019-08-14T19:19:56.000Z",
+				"tz": "+0000"
+			},
+			"hash": "ff7ec676a7222317628ca23a1409ebaf691c52b9",
+			"summary": "Move page layout rectangles into DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 87,
+			"result": 101
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565810396",
+				"date": "2019-08-14T19:19:56.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565810396",
+				"date": "2019-08-14T19:19:56.000Z",
+				"tz": "+0000"
+			},
+			"hash": "ff7ec676a7222317628ca23a1409ebaf691c52b9",
+			"summary": "Move page layout rectangles into DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 88,
+			"result": 102
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565810396",
+				"date": "2019-08-14T19:19:56.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565810396",
+				"date": "2019-08-14T19:19:56.000Z",
+				"tz": "+0000"
+			},
+			"hash": "ff7ec676a7222317628ca23a1409ebaf691c52b9",
+			"summary": "Move page layout rectangles into DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 89,
+			"result": 103
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565810396",
+				"date": "2019-08-14T19:19:56.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565810396",
+				"date": "2019-08-14T19:19:56.000Z",
+				"tz": "+0000"
+			},
+			"hash": "ff7ec676a7222317628ca23a1409ebaf691c52b9",
+			"summary": "Move page layout rectangles into DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 90,
+			"result": 104
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565810396",
+				"date": "2019-08-14T19:19:56.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565810396",
+				"date": "2019-08-14T19:19:56.000Z",
+				"tz": "+0000"
+			},
+			"hash": "ff7ec676a7222317628ca23a1409ebaf691c52b9",
+			"summary": "Move page layout rectangles into DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 91,
+			"result": 105
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565810396",
+				"date": "2019-08-14T19:19:56.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565810396",
+				"date": "2019-08-14T19:19:56.000Z",
+				"tz": "+0000"
+			},
+			"hash": "ff7ec676a7222317628ca23a1409ebaf691c52b9",
+			"summary": "Move page layout rectangles into DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 92,
+			"result": 106
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565810396",
+				"date": "2019-08-14T19:19:56.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565810396",
+				"date": "2019-08-14T19:19:56.000Z",
+				"tz": "+0000"
+			},
+			"hash": "ff7ec676a7222317628ca23a1409ebaf691c52b9",
+			"summary": "Move page layout rectangles into DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 93,
+			"result": 107
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565810396",
+				"date": "2019-08-14T19:19:56.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565810396",
+				"date": "2019-08-14T19:19:56.000Z",
+				"tz": "+0000"
+			},
+			"hash": "ff7ec676a7222317628ca23a1409ebaf691c52b9",
+			"summary": "Move page layout rectangles into DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 105,
+			"result": 137
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565208336",
+				"date": "2019-08-07T20:05:36.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565208336",
+				"date": "2019-08-07T20:05:36.000Z",
+				"tz": "+0000"
+			},
+			"hash": "9a62bf44b280fa0260b5c63676f488126997869b",
+			"summary": "Refactor default page orientation to an enum"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 11,
+			"result": 13
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565208336",
+				"date": "2019-08-07T20:05:36.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565208336",
+				"date": "2019-08-07T20:05:36.000Z",
+				"tz": "+0000"
+			},
+			"hash": "9a62bf44b280fa0260b5c63676f488126997869b",
+			"summary": "Refactor default page orientation to an enum"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 36,
+			"result": 38
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565208336",
+				"date": "2019-08-07T20:05:36.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565208336",
+				"date": "2019-08-07T20:05:36.000Z",
+				"tz": "+0000"
+			},
+			"hash": "9a62bf44b280fa0260b5c63676f488126997869b",
+			"summary": "Refactor default page orientation to an enum"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 37,
+			"result": 39
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565208336",
+				"date": "2019-08-07T20:05:36.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565208336",
+				"date": "2019-08-07T20:05:36.000Z",
+				"tz": "+0000"
+			},
+			"hash": "9a62bf44b280fa0260b5c63676f488126997869b",
+			"summary": "Refactor default page orientation to an enum"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 38,
+			"result": 40
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565208336",
+				"date": "2019-08-07T20:05:36.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565208336",
+				"date": "2019-08-07T20:05:36.000Z",
+				"tz": "+0000"
+			},
+			"hash": "9a62bf44b280fa0260b5c63676f488126997869b",
+			"summary": "Refactor default page orientation to an enum"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 47,
+			"result": 49
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<chinsenj@google.com>",
+				"name": "Jeremy Chinsen",
+				"isCurrentUser": false,
+				"timestamp": "1565143134",
+				"date": "2019-08-07T01:58:54.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565143134",
+				"date": "2019-08-07T01:58:54.000Z",
+				"tz": "+0000"
+			},
+			"hash": "08beb48cb405bad33ba7ee38ec413456da70163e",
+			"summary": "Add Document::GetSingleViewLayout() and draw_utils::GetRectForSingleView()."
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 84,
+			"result": 85
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"hash": "eb9e000cfa6e1d9d407b3c5871cc7b617ba5fa57",
+			"summary": "Split options from chrome_pdf::DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 20,
+			"result": 23
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"hash": "eb9e000cfa6e1d9d407b3c5871cc7b617ba5fa57",
+			"summary": "Split options from chrome_pdf::DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 21,
+			"result": 24
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"hash": "eb9e000cfa6e1d9d407b3c5871cc7b617ba5fa57",
+			"summary": "Split options from chrome_pdf::DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 22,
+			"result": 25
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"hash": "eb9e000cfa6e1d9d407b3c5871cc7b617ba5fa57",
+			"summary": "Split options from chrome_pdf::DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 25,
+			"result": 28
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"hash": "eb9e000cfa6e1d9d407b3c5871cc7b617ba5fa57",
+			"summary": "Split options from chrome_pdf::DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 26,
+			"result": 29
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"hash": "eb9e000cfa6e1d9d407b3c5871cc7b617ba5fa57",
+			"summary": "Split options from chrome_pdf::DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 27,
+			"result": 30
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"hash": "eb9e000cfa6e1d9d407b3c5871cc7b617ba5fa57",
+			"summary": "Split options from chrome_pdf::DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 28,
+			"result": 31
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"hash": "eb9e000cfa6e1d9d407b3c5871cc7b617ba5fa57",
+			"summary": "Split options from chrome_pdf::DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 29,
+			"result": 32
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"hash": "eb9e000cfa6e1d9d407b3c5871cc7b617ba5fa57",
+			"summary": "Split options from chrome_pdf::DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 30,
+			"result": 33
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"hash": "eb9e000cfa6e1d9d407b3c5871cc7b617ba5fa57",
+			"summary": "Split options from chrome_pdf::DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 31,
+			"result": 34
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"hash": "eb9e000cfa6e1d9d407b3c5871cc7b617ba5fa57",
+			"summary": "Split options from chrome_pdf::DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 32,
+			"result": 35
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"hash": "eb9e000cfa6e1d9d407b3c5871cc7b617ba5fa57",
+			"summary": "Split options from chrome_pdf::DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 33,
+			"result": 36
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"hash": "eb9e000cfa6e1d9d407b3c5871cc7b617ba5fa57",
+			"summary": "Split options from chrome_pdf::DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 34,
+			"result": 37
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"hash": "eb9e000cfa6e1d9d407b3c5871cc7b617ba5fa57",
+			"summary": "Split options from chrome_pdf::DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 43,
+			"result": 41
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"hash": "eb9e000cfa6e1d9d407b3c5871cc7b617ba5fa57",
+			"summary": "Split options from chrome_pdf::DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 44,
+			"result": 42
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"hash": "eb9e000cfa6e1d9d407b3c5871cc7b617ba5fa57",
+			"summary": "Split options from chrome_pdf::DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 45,
+			"result": 43
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"hash": "eb9e000cfa6e1d9d407b3c5871cc7b617ba5fa57",
+			"summary": "Split options from chrome_pdf::DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 46,
+			"result": 44
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"hash": "eb9e000cfa6e1d9d407b3c5871cc7b617ba5fa57",
+			"summary": "Split options from chrome_pdf::DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 47,
+			"result": 45
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"hash": "eb9e000cfa6e1d9d407b3c5871cc7b617ba5fa57",
+			"summary": "Split options from chrome_pdf::DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 48,
+			"result": 46
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"hash": "eb9e000cfa6e1d9d407b3c5871cc7b617ba5fa57",
+			"summary": "Split options from chrome_pdf::DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 49,
+			"result": 47
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"hash": "eb9e000cfa6e1d9d407b3c5871cc7b617ba5fa57",
+			"summary": "Split options from chrome_pdf::DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 50,
+			"result": 48
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"hash": "eb9e000cfa6e1d9d407b3c5871cc7b617ba5fa57",
+			"summary": "Split options from chrome_pdf::DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 53,
+			"result": 50
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"hash": "eb9e000cfa6e1d9d407b3c5871cc7b617ba5fa57",
+			"summary": "Split options from chrome_pdf::DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 54,
+			"result": 51
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"hash": "eb9e000cfa6e1d9d407b3c5871cc7b617ba5fa57",
+			"summary": "Split options from chrome_pdf::DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 61,
+			"result": 58
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"hash": "eb9e000cfa6e1d9d407b3c5871cc7b617ba5fa57",
+			"summary": "Split options from chrome_pdf::DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 62,
+			"result": 59
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"hash": "eb9e000cfa6e1d9d407b3c5871cc7b617ba5fa57",
+			"summary": "Split options from chrome_pdf::DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 66,
+			"result": 63
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"hash": "eb9e000cfa6e1d9d407b3c5871cc7b617ba5fa57",
+			"summary": "Split options from chrome_pdf::DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 67,
+			"result": 64
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565119532",
+				"date": "2019-08-06T19:25:32.000Z",
+				"tz": "+0000"
+			},
+			"hash": "eb9e000cfa6e1d9d407b3c5871cc7b617ba5fa57",
+			"summary": "Split options from chrome_pdf::DocumentLayout"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 93,
+			"result": 124
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<chinsenj@google.com>",
+				"name": "Jeremy Chinsen",
+				"isCurrentUser": false,
+				"timestamp": "1565052017",
+				"date": "2019-08-06T00:40:17.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565052017",
+				"date": "2019-08-06T00:40:17.000Z",
+				"tz": "+0000"
+			},
+			"hash": "d6fd27ce86e82f6f1b782e9398dbbb7343f883bd",
+			"summary": "Add DocumentLayout::GetTwoUpViewLayout() and PDFiumEngine::LoadPagesInTwoUpView()."
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 8,
+			"result": 9
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<chinsenj@google.com>",
+				"name": "Jeremy Chinsen",
+				"isCurrentUser": false,
+				"timestamp": "1565052017",
+				"date": "2019-08-06T00:40:17.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565052017",
+				"date": "2019-08-06T00:40:17.000Z",
+				"tz": "+0000"
+			},
+			"hash": "d6fd27ce86e82f6f1b782e9398dbbb7343f883bd",
+			"summary": "Add DocumentLayout::GetTwoUpViewLayout() and PDFiumEngine::LoadPagesInTwoUpView()."
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 9,
+			"result": 10
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<chinsenj@google.com>",
+				"name": "Jeremy Chinsen",
+				"isCurrentUser": false,
+				"timestamp": "1565052017",
+				"date": "2019-08-06T00:40:17.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565052017",
+				"date": "2019-08-06T00:40:17.000Z",
+				"tz": "+0000"
+			},
+			"hash": "d6fd27ce86e82f6f1b782e9398dbbb7343f883bd",
+			"summary": "Add DocumentLayout::GetTwoUpViewLayout() and PDFiumEngine::LoadPagesInTwoUpView()."
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 11,
+			"result": 14
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<chinsenj@google.com>",
+				"name": "Jeremy Chinsen",
+				"isCurrentUser": false,
+				"timestamp": "1565052017",
+				"date": "2019-08-06T00:40:17.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565052017",
+				"date": "2019-08-06T00:40:17.000Z",
+				"tz": "+0000"
+			},
+			"hash": "d6fd27ce86e82f6f1b782e9398dbbb7343f883bd",
+			"summary": "Add DocumentLayout::GetTwoUpViewLayout() and PDFiumEngine::LoadPagesInTwoUpView()."
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 59,
+			"result": 108
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<thestig@chromium.org>",
+				"name": "Lei Zhang",
+				"isCurrentUser": false,
+				"timestamp": "1565051283",
+				"date": "2019-08-06T00:28:03.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565051283",
+				"date": "2019-08-06T00:28:03.000Z",
+				"tz": "+0000"
+			},
+			"hash": "4906c103664937af53aba5d651626c5d5b003d52",
+			"summary": "Move document layout constants from PDFiumEngine to DocumentLayout."
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 8,
+			"result": 12
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<thestig@chromium.org>",
+				"name": "Lei Zhang",
+				"isCurrentUser": false,
+				"timestamp": "1565051283",
+				"date": "2019-08-06T00:28:03.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565051283",
+				"date": "2019-08-06T00:28:03.000Z",
+				"tz": "+0000"
+			},
+			"hash": "4906c103664937af53aba5d651626c5d5b003d52",
+			"summary": "Move document layout constants from PDFiumEngine to DocumentLayout."
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 21,
+			"result": 52
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<thestig@chromium.org>",
+				"name": "Lei Zhang",
+				"isCurrentUser": false,
+				"timestamp": "1565051283",
+				"date": "2019-08-06T00:28:03.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565051283",
+				"date": "2019-08-06T00:28:03.000Z",
+				"tz": "+0000"
+			},
+			"hash": "4906c103664937af53aba5d651626c5d5b003d52",
+			"summary": "Move document layout constants from PDFiumEngine to DocumentLayout."
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 22,
+			"result": 53
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<thestig@chromium.org>",
+				"name": "Lei Zhang",
+				"isCurrentUser": false,
+				"timestamp": "1565051283",
+				"date": "2019-08-06T00:28:03.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565051283",
+				"date": "2019-08-06T00:28:03.000Z",
+				"tz": "+0000"
+			},
+			"hash": "4906c103664937af53aba5d651626c5d5b003d52",
+			"summary": "Move document layout constants from PDFiumEngine to DocumentLayout."
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 23,
+			"result": 54
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<thestig@chromium.org>",
+				"name": "Lei Zhang",
+				"isCurrentUser": false,
+				"timestamp": "1565051283",
+				"date": "2019-08-06T00:28:03.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1565051283",
+				"date": "2019-08-06T00:28:03.000Z",
+				"tz": "+0000"
+			},
+			"hash": "4906c103664937af53aba5d651626c5d5b003d52",
+			"summary": "Move document layout constants from PDFiumEngine to DocumentLayout."
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 24,
+			"result": 55
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"hash": "bd80ce77af4144b5f79957a499361b8f6f0f71e1",
+			"summary": "Split layout state from PDFiumEngine"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 1,
+			"result": 1
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"hash": "bd80ce77af4144b5f79957a499361b8f6f0f71e1",
+			"summary": "Split layout state from PDFiumEngine"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 2,
+			"result": 2
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"hash": "bd80ce77af4144b5f79957a499361b8f6f0f71e1",
+			"summary": "Split layout state from PDFiumEngine"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 3,
+			"result": 3
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"hash": "bd80ce77af4144b5f79957a499361b8f6f0f71e1",
+			"summary": "Split layout state from PDFiumEngine"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 4,
+			"result": 4
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"hash": "bd80ce77af4144b5f79957a499361b8f6f0f71e1",
+			"summary": "Split layout state from PDFiumEngine"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 5,
+			"result": 5
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"hash": "bd80ce77af4144b5f79957a499361b8f6f0f71e1",
+			"summary": "Split layout state from PDFiumEngine"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 6,
+			"result": 6
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"hash": "bd80ce77af4144b5f79957a499361b8f6f0f71e1",
+			"summary": "Split layout state from PDFiumEngine"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 7,
+			"result": 7
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"hash": "bd80ce77af4144b5f79957a499361b8f6f0f71e1",
+			"summary": "Split layout state from PDFiumEngine"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 8,
+			"result": 15
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"hash": "bd80ce77af4144b5f79957a499361b8f6f0f71e1",
+			"summary": "Split layout state from PDFiumEngine"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 9,
+			"result": 16
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"hash": "bd80ce77af4144b5f79957a499361b8f6f0f71e1",
+			"summary": "Split layout state from PDFiumEngine"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 10,
+			"result": 17
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"hash": "bd80ce77af4144b5f79957a499361b8f6f0f71e1",
+			"summary": "Split layout state from PDFiumEngine"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 11,
+			"result": 18
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"hash": "bd80ce77af4144b5f79957a499361b8f6f0f71e1",
+			"summary": "Split layout state from PDFiumEngine"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 12,
+			"result": 19
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"hash": "bd80ce77af4144b5f79957a499361b8f6f0f71e1",
+			"summary": "Split layout state from PDFiumEngine"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 13,
+			"result": 20
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"hash": "bd80ce77af4144b5f79957a499361b8f6f0f71e1",
+			"summary": "Split layout state from PDFiumEngine"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 14,
+			"result": 21
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"hash": "bd80ce77af4144b5f79957a499361b8f6f0f71e1",
+			"summary": "Split layout state from PDFiumEngine"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 15,
+			"result": 22
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"hash": "bd80ce77af4144b5f79957a499361b8f6f0f71e1",
+			"summary": "Split layout state from PDFiumEngine"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 18,
+			"result": 26
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"hash": "bd80ce77af4144b5f79957a499361b8f6f0f71e1",
+			"summary": "Split layout state from PDFiumEngine"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 19,
+			"result": 27
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"hash": "bd80ce77af4144b5f79957a499361b8f6f0f71e1",
+			"summary": "Split layout state from PDFiumEngine"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 20,
+			"result": 56
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"hash": "bd80ce77af4144b5f79957a499361b8f6f0f71e1",
+			"summary": "Split layout state from PDFiumEngine"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 21,
+			"result": 57
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"hash": "bd80ce77af4144b5f79957a499361b8f6f0f71e1",
+			"summary": "Split layout state from PDFiumEngine"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 24,
+			"result": 60
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"hash": "bd80ce77af4144b5f79957a499361b8f6f0f71e1",
+			"summary": "Split layout state from PDFiumEngine"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 25,
+			"result": 61
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"hash": "bd80ce77af4144b5f79957a499361b8f6f0f71e1",
+			"summary": "Split layout state from PDFiumEngine"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 26,
+			"result": 62
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"hash": "bd80ce77af4144b5f79957a499361b8f6f0f71e1",
+			"summary": "Split layout state from PDFiumEngine"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 38,
+			"result": 65
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"hash": "bd80ce77af4144b5f79957a499361b8f6f0f71e1",
+			"summary": "Split layout state from PDFiumEngine"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 41,
+			"result": 80
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"hash": "bd80ce77af4144b5f79957a499361b8f6f0f71e1",
+			"summary": "Split layout state from PDFiumEngine"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 42,
+			"result": 81
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"hash": "bd80ce77af4144b5f79957a499361b8f6f0f71e1",
+			"summary": "Split layout state from PDFiumEngine"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 43,
+			"result": 82
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"hash": "bd80ce77af4144b5f79957a499361b8f6f0f71e1",
+			"summary": "Split layout state from PDFiumEngine"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 44,
+			"result": 83
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"hash": "bd80ce77af4144b5f79957a499361b8f6f0f71e1",
+			"summary": "Split layout state from PDFiumEngine"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 56,
+			"result": 109
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"hash": "bd80ce77af4144b5f79957a499361b8f6f0f71e1",
+			"summary": "Split layout state from PDFiumEngine"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 63,
+			"result": 125
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"hash": "bd80ce77af4144b5f79957a499361b8f6f0f71e1",
+			"summary": "Split layout state from PDFiumEngine"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 64,
+			"result": 135
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"hash": "bd80ce77af4144b5f79957a499361b8f6f0f71e1",
+			"summary": "Split layout state from PDFiumEngine"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 65,
+			"result": 136
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"hash": "bd80ce77af4144b5f79957a499361b8f6f0f71e1",
+			"summary": "Split layout state from PDFiumEngine"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 66,
+			"result": 139
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"hash": "bd80ce77af4144b5f79957a499361b8f6f0f71e1",
+			"summary": "Split layout state from PDFiumEngine"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 67,
+			"result": 140
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"hash": "bd80ce77af4144b5f79957a499361b8f6f0f71e1",
+			"summary": "Split layout state from PDFiumEngine"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 68,
+			"result": 141
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"hash": "bd80ce77af4144b5f79957a499361b8f6f0f71e1",
+			"summary": "Split layout state from PDFiumEngine"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 69,
+			"result": 142
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<kmoon@chromium.org>",
+				"name": "K Moon",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"committer": {
+				"mail": "<commit-bot@chromium.org>",
+				"name": "Commit Bot",
+				"isCurrentUser": false,
+				"timestamp": "1564169270",
+				"date": "2019-07-26T19:27:50.000Z",
+				"tz": "+0000"
+			},
+			"hash": "bd80ce77af4144b5f79957a499361b8f6f0f71e1",
+			"summary": "Split layout state from PDFiumEngine"
+		},
+		"filename": "pdf/document_layout.h",
+		"line": {
+			"source": 70,
+			"result": 143
+		}
+	}
+]
diff --git a/test/fixture/git-stream-blame-incremental-multi-chunk.json b/test/fixture/git-stream-blame-incremental-multi-chunk.json
new file mode 100644
index 00000000..b93dd9de
--- /dev/null
+++ b/test/fixture/git-stream-blame-incremental-multi-chunk.json
@@ -0,0 +1,12 @@
+[
+	"6d326b9edbf4c2ee1c3ad155d16012acf3c0cc0c 66 66 14\nauthor K Moon\nauthor-mail <kmoon@chromium.org>\nauthor-time 1568932927\nauthor-tz +0000\ncommitter Commit Bot\ncommitter-mail <commit-bot@chromium.org>\ncommitter-time 1568932927\ncommitter-tz +0000\nsummary Increase precision of PDF layout dirty tracking\nprevious 2247054876159fa1b40d5a55ca45731cde234eef pdf/document_layout.h\nfilename pdf/document_layout.h\n6d326b9edbf4c2ee1c3ad155d16012acf3c0cc0c 119 119 5\nprevious 2247054876159fa1b40d5a55ca45731cde234eef pdf/document_layout.h\nfilename pdf/document_layout.h\n6d326b9edbf4c2ee1c3ad155d16012acf3c0cc0c 126 126 9\nprevious 2247054876159fa1b40d5a55ca45731cde234eef pdf/document_layout.h\nfilename pdf/document_layout.h\n",
+	"e4bd75222c6a9379a8e3e4aea5364934445d8bb1 77 84 1\nauthor K Moon\nauthor-mail <kmoon@chromium.org>\nauthor-time 1566519176\nauthor-tz +0000\ncommitter Commit Bot\ncommitter-mail <commit-bot@chromium.org>\ncommitter-time 1566519176\ncommitter-tz +0000\nsummary Clarify page rectangles in DocumentLayout\nprevious 736d778dd9ae7afc1cf41ae92d102cfd16027c22 pdf/document_layout.h\nfilename pdf/document_layout.h\ne4bd75222c6a9379a8e3e4aea5364934445d8bb1 82 89 8\nprevious 736d778dd9ae7afc1cf41ae92d102cfd16027c22 pdf/document_layout.h\nfilename pdf/document_layout.h\ne4bd75222c6a9379a8e3e4aea5364934445d8bb1 108 110 9\nprevious 736d778dd9ae7afc1cf41ae92d102cfd16027c22 pdf/document_layout.h\nfilename pdf/document_layout.h\ne4bd75222c6a9379a8e3e4aea5364934445d8bb1 122 138 1\nprevious 736d778dd9ae7afc1cf41ae92d102cfd16027c22 pdf/document_layout.h\nfilename pdf/document_layout.h\n",
+	"ff7ec676a7222317628ca23a1409ebaf691c52b9 8 8 1\nauthor K Moon\nauthor-mail <kmoon@chromium.org>\nauthor-time 1565810396\nauthor-tz +0000\ncommitter Commit Bot\ncommitter-mail <commit-bot@chromium.org>\ncommitter-time 1565810396\ncommitter-tz +0000\nsummary Move page layout rectangles into DocumentLayout\nprevious 2db2d2878bb872a7b7196d1bc9cece8c6e330bbf pdf/document_layout.h\nfilename pdf/document_layout.h\nff7ec676a7222317628ca23a1409ebaf691c52b9 11 11 1\nprevious 2db2d2878bb872a7b7196d1bc9cece8c6e330bbf pdf/document_layout.h\nfilename pdf/document_layout.h\nff7ec676a7222317628ca23a1409ebaf691c52b9 79 86 3\nprevious 2db2d2878bb872a7b7196d1bc9cece8c6e330bbf pdf/document_layout.h\nfilename pdf/document_layout.h\nff7ec676a7222317628ca23a1409ebaf691c52b9 83 97 11\nprevious 2db2d2878bb872a7b7196d1bc9cece8c6e330bbf pdf/document_layout.h\nfilename pdf/document_layout.h\nff7ec676a7222317628ca23a1409ebaf691c52b9 105 137 1\nprevious 2db2d2878bb872a7b7196d1bc9cece8c6e330bbf pdf/document_layout.h\nfilename pdf/document_layout.h\n",
+	"9a62bf44b280fa0260b5c63676f488126997869b 11 13 1\nauthor K Moon\nauthor-mail <kmoon@chromium.org>\nauthor-time 1565208336\nauthor-tz +0000\ncommitter Commit Bot\ncommitter-mail <commit-bot@chromium.org>\ncommitter-time 1565208336\ncommitter-tz +0000\nsummary Refactor default page orientation to an enum\nprevious 14c4b07d0cb8702a69e32a7dbc99c6269e06256b pdf/document_layout.h\nfilename pdf/document_layout.h\n9a62bf44b280fa0260b5c63676f488126997869b 36 38 3\nprevious 14c4b07d0cb8702a69e32a7dbc99c6269e06256b pdf/document_layout.h\nfilename pdf/document_layout.h\n9a62bf44b280fa0260b5c63676f488126997869b 47 49 1\nprevious 14c4b07d0cb8702a69e32a7dbc99c6269e06256b pdf/document_layout.h\nfilename pdf/document_layout.h\n",
+	"08beb48cb405bad33ba7ee38ec413456da70163e 84 85 1\nauthor Jeremy Chinsen\nauthor-mail <chinsenj@google.com>\nauthor-time 1565143134\nauthor-tz +0000\ncommitter Commit Bot\ncommitter-mail <commit-bot@chromium.org>\ncommitter-time 1565143134\ncommitter-tz +0000\nsummary Add Document::GetSingleViewLayout() and draw_utils::GetRectForSingleView().\nprevious c70212ee4a5838f10af318d5b77eaf47aa57863d pdf/document_layout.h\nfilename pdf/document_layout.h\n",
+	"eb9e000cfa6e1d9d407b3c5871cc7b617ba5fa57 20 23 3\nauthor K Moon\nauthor-mail <kmoon@chromium.org>\nauthor-time 1565119532\nauthor-tz +0000\ncommitter Commit Bot\ncommitter-mail <commit-bot@chromium.org>\ncommitter-time 1565119532\ncommitter-tz +0000\nsummary Split options from chrome_pdf::DocumentLayout\nprevious f2ea3ba5d0bf0bcbc7327a654b51eb3d2d44ebff pdf/document_layout.h\nfilename pdf/document_layout.h\neb9e000cfa6e1d9d407b3c5871cc7b617ba5fa57 25 28 10\nprevious f2ea3ba5d0bf0bcbc7327a654b51eb3d2d44ebff pdf/document_layout.h\nfilename pdf/document_layout.h\neb9e000cfa6e1d9d407b3c5871cc7b617ba5fa57 43 41 8\nprevious f2ea3ba5d0bf0bcbc7327a654b51eb3d2d44ebff pdf/document_layout.h\nfilename pdf/document_layout.h\neb9e000cfa6e1d9d407b3c5871cc7b617ba5fa57 53 50 2\nprevious f2ea3ba5d0bf0bcbc7327a654b51eb3d2d44ebff pdf/document_layout.h\nfilename pdf/document_layout.h\neb9e000cfa6e1d9d407b3c5871cc7b617ba5fa57 61 58 2\nprevious f2ea3ba5d0bf0bcbc7327a654b51eb3d2d44ebff pdf/document_layout.h\nfilename pdf/document_layout.h\n",
+	"eb9e000cfa6e1d9d407b3c5871cc7b617ba5fa57 66 63 2\nprevious f2ea3ba5d0bf0bcbc7327a654b51eb3d2d44ebff pdf/document_layout.h\nfilename pdf/document_layout.h\neb9e000cfa6e1d9d407b3c5871cc7b617ba5fa57 93 124 1\nprevious f2ea3ba5d0bf0bcbc7327a654b51eb3d2d44ebff pdf/document_layout.h\nfilename pdf/document_layout.h\n",
+	"d6fd27ce86e82f6f1b782e9398dbbb7343f883bd 8 9 2\nauthor Jeremy Chinsen\nauthor-mail <chinsenj@google.com>\nauthor-time 1565052017\nauthor-tz +0000\ncommitter Commit Bot\ncommitter-mail <commit-bot@chromium.org>\ncommitter-time 1565052017\ncommitter-tz +0000\nsummary Add DocumentLayout::GetTwoUpViewLayout() and PDFiumEngine::LoadPagesInTwoUpView().\nprevious 5ca0e755e21a090f5143ae84ce65c8838d295094 pdf/document_layout.h\nfilename pdf/document_layout.h\nd6fd27ce86e82f6f1b782e9398dbbb7343f883bd 11 14 1\nprevious 5ca0e755e21a090f5143ae84ce65c8838d295094 pdf/document_layout.h\nfilename pdf/document_layout.h\nd6fd27ce86e82f6f1b782e9398dbbb7343f883bd 59 108 1\nprevious 5ca0e755e21a090f5143ae84ce65c8838d295094 pdf/document_layout.h\nfilename pdf/document_layout.h\n",
+	"4906c103664937af53aba5d651626c5d5b003d52 8 12 1\nauthor Lei Zhang\nauthor-mail <thestig@chromium.org>\nauthor-time 1565051283\nauthor-tz +0000\ncommitter Commit Bot\ncommitter-mail <commit-bot@chromium.org>\ncommitter-time 1565051283\ncommitter-tz +0000\nsummary Move document layout constants from PDFiumEngine to DocumentLayout.\nprevious 5880a554eedea370a9e4d10b1debde99e92aad6f pdf/document_layout.h\nfilename pdf/document_layout.h\n4906c103664937af53aba5d651626c5d5b003d52 21 52 4\nprevious 5880a554eedea370a9e4d10b1debde99e92aad6f pdf/document_layout.h\nfilename pdf/document_layout.h\n",
+	"bd80ce77af4144b5f79957a499361b8f6f0f71e1 1 1 7\nauthor K Moon\nauthor-mail <kmoon@chromium.org>\nauthor-time 1564169270\nauthor-tz +0000\ncommitter Commit Bot\ncommitter-mail <commit-bot@chromium.org>\ncommitter-time 1564169270\ncommitter-tz +0000\nsummary Split layout state from PDFiumEngine\nfilename pdf/document_layout.h\nbd80ce77af4144b5f79957a499361b8f6f0f71e1 8 15 8\nfilename pdf/document_layout.h\nbd80ce77af4144b5f79957a499361b8f6f0f71e1 18 26 2\nfilename pdf/document_layout.h\nbd80ce77af4144b5f79957a499361b8f6f0f71e1 20 56 2\nfilename pdf/document_layout.h\nbd80ce77af4144b5f79957a499361b8f6f0f71e1 24 60 3\nfilename pdf/document_layout.h\nbd80ce77af4144b5f79957a499361b8f6f0f71e1 38 65 1\nfilename pdf/document_layout.h\nbd80ce77af4144b5f79957a499361b8f6f0f71e1 41 80 4\nfilename pdf/document_layout.h\nbd80ce77af4144b5f79957a499361b8f6f0f71e1 56 109 1\nfilename pdf/document_layout.h\nbd80ce77af4144b5f79957a499361b8f6f0f71e1 63 125 1\nfilename pdf/document_layout.h\nbd80ce77af4144b5f79957a499361b8f6f0f71e1 64 135 2\nfilename pdf/document_layout.h\nbd80ce77af4144b5f79957a499361b8f6f0f71e1 66 139 5\nfilename pdf/document_layout.h\n"
+]
diff --git a/test/fixture/git-stream-blame-incremental-result.json b/test/fixture/git-stream-blame-incremental-result.json
new file mode 100644
index 00000000..2c164b64
--- /dev/null
+++ b/test/fixture/git-stream-blame-incremental-result.json
@@ -0,0 +1,1136 @@
+[
+	{
+		"commit": {
+			"author": {
+				"mail": "<not.committed.yet>",
+				"name": "Not Committed Yet",
+				"isCurrentUser": false,
+				"timestamp": "1601475715",
+				"date": "2020-09-30T14:21:55.000Z",
+				"tz": "+0200"
+			},
+			"committer": {
+				"mail": "<not.committed.yet>",
+				"name": "Not Committed Yet",
+				"isCurrentUser": false,
+				"timestamp": "1601475715",
+				"date": "2020-09-30T14:21:55.000Z",
+				"tz": "+0200"
+			},
+			"hash": "0000000000000000000000000000000000000000",
+			"summary": "Version of src/git/stream.ts from src/git/stream.ts"
+		},
+		"filename": "src/git/stream.ts",
+		"line": {
+			"source": 6,
+			"result": 6
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1601153539",
+				"date": "2020-09-26T20:52:19.000Z",
+				"tz": "+0200"
+			},
+			"committer": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1601153539",
+				"date": "2020-09-26T20:52:19.000Z",
+				"tz": "+0200"
+			},
+			"hash": "57463acc9ac14a81020067da1979b34c502b7478",
+			"summary": "Moving the parsing to its own file"
+		},
+		"filename": "src/git/stream.ts",
+		"line": {
+			"source": 4,
+			"result": 4
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1601153539",
+				"date": "2020-09-26T20:52:19.000Z",
+				"tz": "+0200"
+			},
+			"committer": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1601153539",
+				"date": "2020-09-26T20:52:19.000Z",
+				"tz": "+0200"
+			},
+			"hash": "57463acc9ac14a81020067da1979b34c502b7478",
+			"summary": "Moving the parsing to its own file"
+		},
+		"filename": "src/git/stream.ts",
+		"line": {
+			"source": 33,
+			"result": 27
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1601153539",
+				"date": "2020-09-26T20:52:19.000Z",
+				"tz": "+0200"
+			},
+			"committer": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1601153539",
+				"date": "2020-09-26T20:52:19.000Z",
+				"tz": "+0200"
+			},
+			"hash": "57463acc9ac14a81020067da1979b34c502b7478",
+			"summary": "Moving the parsing to its own file"
+		},
+		"filename": "src/git/stream.ts",
+		"line": {
+			"source": 34,
+			"result": 28
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1601153539",
+				"date": "2020-09-26T20:52:19.000Z",
+				"tz": "+0200"
+			},
+			"committer": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1601153539",
+				"date": "2020-09-26T20:52:19.000Z",
+				"tz": "+0200"
+			},
+			"hash": "57463acc9ac14a81020067da1979b34c502b7478",
+			"summary": "Moving the parsing to its own file"
+		},
+		"filename": "src/git/stream.ts",
+		"line": {
+			"source": 36,
+			"result": 30
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1601138853",
+				"date": "2020-09-26T16:47:33.000Z",
+				"tz": "+0200"
+			},
+			"committer": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1601138853",
+				"date": "2020-09-26T16:47:33.000Z",
+				"tz": "+0200"
+			},
+			"hash": "78049765bd69de4b7fc535ee4aeabb644bcf3028",
+			"summary": "Refactoring"
+		},
+		"filename": "src/git/stream.ts",
+		"line": {
+			"source": 24,
+			"result": 5
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1601138853",
+				"date": "2020-09-26T16:47:33.000Z",
+				"tz": "+0200"
+			},
+			"committer": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1601138853",
+				"date": "2020-09-26T16:47:33.000Z",
+				"tz": "+0200"
+			},
+			"hash": "78049765bd69de4b7fc535ee4aeabb644bcf3028",
+			"summary": "Refactoring"
+		},
+		"filename": "src/git/stream.ts",
+		"line": {
+			"source": 34,
+			"result": 8
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1601138853",
+				"date": "2020-09-26T16:47:33.000Z",
+				"tz": "+0200"
+			},
+			"committer": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1601138853",
+				"date": "2020-09-26T16:47:33.000Z",
+				"tz": "+0200"
+			},
+			"hash": "78049765bd69de4b7fc535ee4aeabb644bcf3028",
+			"summary": "Refactoring"
+		},
+		"filename": "src/git/stream.ts",
+		"line": {
+			"source": 38,
+			"result": 12
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1601138853",
+				"date": "2020-09-26T16:47:33.000Z",
+				"tz": "+0200"
+			},
+			"committer": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1601138853",
+				"date": "2020-09-26T16:47:33.000Z",
+				"tz": "+0200"
+			},
+			"hash": "78049765bd69de4b7fc535ee4aeabb644bcf3028",
+			"summary": "Refactoring"
+		},
+		"filename": "src/git/stream.ts",
+		"line": {
+			"source": 39,
+			"result": 13
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1601138853",
+				"date": "2020-09-26T16:47:33.000Z",
+				"tz": "+0200"
+			},
+			"committer": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1601138853",
+				"date": "2020-09-26T16:47:33.000Z",
+				"tz": "+0200"
+			},
+			"hash": "78049765bd69de4b7fc535ee4aeabb644bcf3028",
+			"summary": "Refactoring"
+		},
+		"filename": "src/git/stream.ts",
+		"line": {
+			"source": 40,
+			"result": 14
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1601138853",
+				"date": "2020-09-26T16:47:33.000Z",
+				"tz": "+0200"
+			},
+			"committer": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1601138853",
+				"date": "2020-09-26T16:47:33.000Z",
+				"tz": "+0200"
+			},
+			"hash": "78049765bd69de4b7fc535ee4aeabb644bcf3028",
+			"summary": "Refactoring"
+		},
+		"filename": "src/git/stream.ts",
+		"line": {
+			"source": 41,
+			"result": 15
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1601138853",
+				"date": "2020-09-26T16:47:33.000Z",
+				"tz": "+0200"
+			},
+			"committer": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1601138853",
+				"date": "2020-09-26T16:47:33.000Z",
+				"tz": "+0200"
+			},
+			"hash": "78049765bd69de4b7fc535ee4aeabb644bcf3028",
+			"summary": "Refactoring"
+		},
+		"filename": "src/git/stream.ts",
+		"line": {
+			"source": 42,
+			"result": 16
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1601138853",
+				"date": "2020-09-26T16:47:33.000Z",
+				"tz": "+0200"
+			},
+			"committer": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1601138853",
+				"date": "2020-09-26T16:47:33.000Z",
+				"tz": "+0200"
+			},
+			"hash": "78049765bd69de4b7fc535ee4aeabb644bcf3028",
+			"summary": "Refactoring"
+		},
+		"filename": "src/git/stream.ts",
+		"line": {
+			"source": 44,
+			"result": 18
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1601138853",
+				"date": "2020-09-26T16:47:33.000Z",
+				"tz": "+0200"
+			},
+			"committer": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1601138853",
+				"date": "2020-09-26T16:47:33.000Z",
+				"tz": "+0200"
+			},
+			"hash": "78049765bd69de4b7fc535ee4aeabb644bcf3028",
+			"summary": "Refactoring"
+		},
+		"filename": "src/git/stream.ts",
+		"line": {
+			"source": 45,
+			"result": 19
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1601138853",
+				"date": "2020-09-26T16:47:33.000Z",
+				"tz": "+0200"
+			},
+			"committer": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1601138853",
+				"date": "2020-09-26T16:47:33.000Z",
+				"tz": "+0200"
+			},
+			"hash": "78049765bd69de4b7fc535ee4aeabb644bcf3028",
+			"summary": "Refactoring"
+		},
+		"filename": "src/git/stream.ts",
+		"line": {
+			"source": 46,
+			"result": 20
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1601138853",
+				"date": "2020-09-26T16:47:33.000Z",
+				"tz": "+0200"
+			},
+			"committer": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1601138853",
+				"date": "2020-09-26T16:47:33.000Z",
+				"tz": "+0200"
+			},
+			"hash": "78049765bd69de4b7fc535ee4aeabb644bcf3028",
+			"summary": "Refactoring"
+		},
+		"filename": "src/git/stream.ts",
+		"line": {
+			"source": 47,
+			"result": 21
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1601138853",
+				"date": "2020-09-26T16:47:33.000Z",
+				"tz": "+0200"
+			},
+			"committer": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1601138853",
+				"date": "2020-09-26T16:47:33.000Z",
+				"tz": "+0200"
+			},
+			"hash": "78049765bd69de4b7fc535ee4aeabb644bcf3028",
+			"summary": "Refactoring"
+		},
+		"filename": "src/git/stream.ts",
+		"line": {
+			"source": 65,
+			"result": 39
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1601138853",
+				"date": "2020-09-26T16:47:33.000Z",
+				"tz": "+0200"
+			},
+			"committer": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1601138853",
+				"date": "2020-09-26T16:47:33.000Z",
+				"tz": "+0200"
+			},
+			"hash": "78049765bd69de4b7fc535ee4aeabb644bcf3028",
+			"summary": "Refactoring"
+		},
+		"filename": "src/git/stream.ts",
+		"line": {
+			"source": 66,
+			"result": 40
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1579705267",
+				"date": "2020-01-22T15:01:07.000Z",
+				"tz": "+0100"
+			},
+			"committer": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1579705267",
+				"date": "2020-01-22T15:01:07.000Z",
+				"tz": "+0100"
+			},
+			"hash": "09bf1237bf2290188cc626eddf9aa13c62593b4b",
+			"summary": "Closes #58"
+		},
+		"filename": "src/git/stream.ts",
+		"line": {
+			"source": 38,
+			"result": 10
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1579705267",
+				"date": "2020-01-22T15:01:07.000Z",
+				"tz": "+0100"
+			},
+			"committer": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1579705267",
+				"date": "2020-01-22T15:01:07.000Z",
+				"tz": "+0100"
+			},
+			"hash": "09bf1237bf2290188cc626eddf9aa13c62593b4b",
+			"summary": "Closes #58"
+		},
+		"filename": "src/git/stream.ts",
+		"line": {
+			"source": 39,
+			"result": 11
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1579705267",
+				"date": "2020-01-22T15:01:07.000Z",
+				"tz": "+0100"
+			},
+			"committer": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1579705267",
+				"date": "2020-01-22T15:01:07.000Z",
+				"tz": "+0100"
+			},
+			"hash": "09bf1237bf2290188cc626eddf9aa13c62593b4b",
+			"summary": "Closes #58"
+		},
+		"filename": "src/git/stream.ts",
+		"line": {
+			"source": 44,
+			"result": 22
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1579705267",
+				"date": "2020-01-22T15:01:07.000Z",
+				"tz": "+0100"
+			},
+			"committer": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1579705267",
+				"date": "2020-01-22T15:01:07.000Z",
+				"tz": "+0100"
+			},
+			"hash": "09bf1237bf2290188cc626eddf9aa13c62593b4b",
+			"summary": "Closes #58"
+		},
+		"filename": "src/git/stream.ts",
+		"line": {
+			"source": 45,
+			"result": 23
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1579705267",
+				"date": "2020-01-22T15:01:07.000Z",
+				"tz": "+0100"
+			},
+			"committer": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1579705267",
+				"date": "2020-01-22T15:01:07.000Z",
+				"tz": "+0100"
+			},
+			"hash": "09bf1237bf2290188cc626eddf9aa13c62593b4b",
+			"summary": "Closes #58"
+		},
+		"filename": "src/git/stream.ts",
+		"line": {
+			"source": 46,
+			"result": 24
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1579705267",
+				"date": "2020-01-22T15:01:07.000Z",
+				"tz": "+0100"
+			},
+			"committer": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1579705267",
+				"date": "2020-01-22T15:01:07.000Z",
+				"tz": "+0100"
+			},
+			"hash": "09bf1237bf2290188cc626eddf9aa13c62593b4b",
+			"summary": "Closes #58"
+		},
+		"filename": "src/git/stream.ts",
+		"line": {
+			"source": 49,
+			"result": 29
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1579705267",
+				"date": "2020-01-22T15:01:07.000Z",
+				"tz": "+0100"
+			},
+			"committer": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1579705267",
+				"date": "2020-01-22T15:01:07.000Z",
+				"tz": "+0100"
+			},
+			"hash": "09bf1237bf2290188cc626eddf9aa13c62593b4b",
+			"summary": "Closes #58"
+		},
+		"filename": "src/git/stream.ts",
+		"line": {
+			"source": 57,
+			"result": 33
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1579705267",
+				"date": "2020-01-22T15:01:07.000Z",
+				"tz": "+0100"
+			},
+			"committer": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1579705267",
+				"date": "2020-01-22T15:01:07.000Z",
+				"tz": "+0100"
+			},
+			"hash": "09bf1237bf2290188cc626eddf9aa13c62593b4b",
+			"summary": "Closes #58"
+		},
+		"filename": "src/git/stream.ts",
+		"line": {
+			"source": 58,
+			"result": 34
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1579705267",
+				"date": "2020-01-22T15:01:07.000Z",
+				"tz": "+0100"
+			},
+			"committer": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1579705267",
+				"date": "2020-01-22T15:01:07.000Z",
+				"tz": "+0100"
+			},
+			"hash": "09bf1237bf2290188cc626eddf9aa13c62593b4b",
+			"summary": "Closes #58"
+		},
+		"filename": "src/git/stream.ts",
+		"line": {
+			"source": 59,
+			"result": 35
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1579705267",
+				"date": "2020-01-22T15:01:07.000Z",
+				"tz": "+0100"
+			},
+			"committer": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1579705267",
+				"date": "2020-01-22T15:01:07.000Z",
+				"tz": "+0100"
+			},
+			"hash": "09bf1237bf2290188cc626eddf9aa13c62593b4b",
+			"summary": "Closes #58"
+		},
+		"filename": "src/git/stream.ts",
+		"line": {
+			"source": 66,
+			"result": 38
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1562163115",
+				"date": "2019-07-03T14:11:55.000Z",
+				"tz": "+0200"
+			},
+			"committer": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1562163115",
+				"date": "2019-07-03T14:11:55.000Z",
+				"tz": "+0200"
+			},
+			"hash": "f6d2b098174aa630d59eae5fc99a01f3a591b8ad",
+			"summary": "Add TSyringe for IoC"
+		},
+		"filename": "src/git/stream.ts",
+		"line": {
+			"source": 16,
+			"result": 7
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1562163115",
+				"date": "2019-07-03T14:11:55.000Z",
+				"tz": "+0200"
+			},
+			"committer": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1562163115",
+				"date": "2019-07-03T14:11:55.000Z",
+				"tz": "+0200"
+			},
+			"hash": "f6d2b098174aa630d59eae5fc99a01f3a591b8ad",
+			"summary": "Add TSyringe for IoC"
+		},
+		"filename": "src/git/stream.ts",
+		"line": {
+			"source": 23,
+			"result": 9
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1562163115",
+				"date": "2019-07-03T14:11:55.000Z",
+				"tz": "+0200"
+			},
+			"committer": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1562163115",
+				"date": "2019-07-03T14:11:55.000Z",
+				"tz": "+0200"
+			},
+			"hash": "f6d2b098174aa630d59eae5fc99a01f3a591b8ad",
+			"summary": "Add TSyringe for IoC"
+		},
+		"filename": "src/git/stream.ts",
+		"line": {
+			"source": 46,
+			"result": 36
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1562163115",
+				"date": "2019-07-03T14:11:55.000Z",
+				"tz": "+0200"
+			},
+			"committer": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1562163115",
+				"date": "2019-07-03T14:11:55.000Z",
+				"tz": "+0200"
+			},
+			"hash": "f6d2b098174aa630d59eae5fc99a01f3a591b8ad",
+			"summary": "Add TSyringe for IoC"
+		},
+		"filename": "src/git/stream.ts",
+		"line": {
+			"source": 47,
+			"result": 37
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1561587471",
+				"date": "2019-06-26T22:17:51.000Z",
+				"tz": "+0200"
+			},
+			"committer": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1561588275",
+				"date": "2019-06-26T22:31:15.000Z",
+				"tz": "+0200"
+			},
+			"hash": "41b858cfcf019fa34597e14719c910fa1409ee66",
+			"summary": "Factory is now responsible for workspace knowledge"
+		},
+		"filename": "src/git/stream.ts",
+		"line": {
+			"source": 1,
+			"result": 1
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1561587471",
+				"date": "2019-06-26T22:17:51.000Z",
+				"tz": "+0200"
+			},
+			"committer": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1561588275",
+				"date": "2019-06-26T22:31:15.000Z",
+				"tz": "+0200"
+			},
+			"hash": "41b858cfcf019fa34597e14719c910fa1409ee66",
+			"summary": "Factory is now responsible for workspace knowledge"
+		},
+		"filename": "src/git/stream.ts",
+		"line": {
+			"source": 4,
+			"result": 3
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1520550992",
+				"date": "2018-03-08T23:16:32.000Z",
+				"tz": "+0100"
+			},
+			"committer": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1520550992",
+				"date": "2018-03-08T23:16:32.000Z",
+				"tz": "+0100"
+			},
+			"hash": "a8cb1e0ba4f8f03ab685bb51a1ac83cc299c020d",
+			"summary": "Adding TSLint and linting the whole project"
+		},
+		"filename": "src/git/stream.ts",
+		"line": {
+			"source": 47,
+			"result": 31
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1500502996",
+				"date": "2017-07-19T22:23:16.000Z",
+				"tz": "+0200"
+			},
+			"committer": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1500502996",
+				"date": "2017-07-19T22:23:16.000Z",
+				"tz": "+0200"
+			},
+			"hash": "3d238f6c9309c7314b3be7be7a4c3f6b3e3de80b",
+			"summary": "2.0.0 - New blame engine and clearer logging"
+		},
+		"filename": "src/git/stream.ts",
+		"line": {
+			"source": 3,
+			"result": 2
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1500502996",
+				"date": "2017-07-19T22:23:16.000Z",
+				"tz": "+0200"
+			},
+			"committer": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1500502996",
+				"date": "2017-07-19T22:23:16.000Z",
+				"tz": "+0200"
+			},
+			"hash": "3d238f6c9309c7314b3be7be7a4c3f6b3e3de80b",
+			"summary": "2.0.0 - New blame engine and clearer logging"
+		},
+		"filename": "src/git/stream.ts",
+		"line": {
+			"source": 40,
+			"result": 17
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1500502996",
+				"date": "2017-07-19T22:23:16.000Z",
+				"tz": "+0200"
+			},
+			"committer": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1500502996",
+				"date": "2017-07-19T22:23:16.000Z",
+				"tz": "+0200"
+			},
+			"hash": "3d238f6c9309c7314b3be7be7a4c3f6b3e3de80b",
+			"summary": "2.0.0 - New blame engine and clearer logging"
+		},
+		"filename": "src/git/stream.ts",
+		"line": {
+			"source": 42,
+			"result": 25
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1500502996",
+				"date": "2017-07-19T22:23:16.000Z",
+				"tz": "+0200"
+			},
+			"committer": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1500502996",
+				"date": "2017-07-19T22:23:16.000Z",
+				"tz": "+0200"
+			},
+			"hash": "3d238f6c9309c7314b3be7be7a4c3f6b3e3de80b",
+			"summary": "2.0.0 - New blame engine and clearer logging"
+		},
+		"filename": "src/git/stream.ts",
+		"line": {
+			"source": 43,
+			"result": 26
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1500502996",
+				"date": "2017-07-19T22:23:16.000Z",
+				"tz": "+0200"
+			},
+			"committer": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1500502996",
+				"date": "2017-07-19T22:23:16.000Z",
+				"tz": "+0200"
+			},
+			"hash": "3d238f6c9309c7314b3be7be7a4c3f6b3e3de80b",
+			"summary": "2.0.0 - New blame engine and clearer logging"
+		},
+		"filename": "src/git/stream.ts",
+		"line": {
+			"source": 59,
+			"result": 32
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1500502996",
+				"date": "2017-07-19T22:23:16.000Z",
+				"tz": "+0200"
+			},
+			"committer": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1500502996",
+				"date": "2017-07-19T22:23:16.000Z",
+				"tz": "+0200"
+			},
+			"hash": "3d238f6c9309c7314b3be7be7a4c3f6b3e3de80b",
+			"summary": "2.0.0 - New blame engine and clearer logging"
+		},
+		"filename": "src/git/stream.ts",
+		"line": {
+			"source": 70,
+			"result": 41
+		}
+	},
+	{
+		"commit": {
+			"author": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1500502996",
+				"date": "2017-07-19T22:23:16.000Z",
+				"tz": "+0200"
+			},
+			"committer": {
+				"mail": "<albin.jacobsson@gmail.com>",
+				"name": "Albin Jacobsson",
+				"isCurrentUser": false,
+				"timestamp": "1500502996",
+				"date": "2017-07-19T22:23:16.000Z",
+				"tz": "+0200"
+			},
+			"hash": "3d238f6c9309c7314b3be7be7a4c3f6b3e3de80b",
+			"summary": "2.0.0 - New blame engine and clearer logging"
+		},
+		"filename": "src/git/stream.ts",
+		"line": {
+			"source": 176,
+			"result": 42
+		}
+	}
+]
diff --git a/test/fixture/git-stream-blame-incremental.chunks b/test/fixture/git-stream-blame-incremental.chunks
new file mode 100644
index 00000000..ff51d4a4
--- /dev/null
+++ b/test/fixture/git-stream-blame-incremental.chunks
@@ -0,0 +1,144 @@
+0000000000000000000000000000000000000000 6 6 1
+author Not Committed Yet
+author-mail <not.committed.yet>
+author-time 1601475715
+author-tz +0200
+committer Not Committed Yet
+committer-mail <not.committed.yet>
+committer-time 1601475715
+committer-tz +0200
+summary Version of src/git/stream.ts from src/git/stream.ts
+previous 57463acc9ac14a81020067da1979b34c502b7478 src/git/stream.ts
+filename src/git/stream.ts
+57463acc9ac14a81020067da1979b34c502b7478 4 4 1
+author Albin Jacobsson
+author-mail <albin.jacobsson@gmail.com>
+author-time 1601153539
+author-tz +0200
+committer Albin Jacobsson
+committer-mail <albin.jacobsson@gmail.com>
+committer-time 1601153539
+committer-tz +0200
+summary Moving the parsing to its own file
+previous 897ebd1121fecddbbcb92899a8d9650ffe47209b src/git/stream.ts
+filename src/git/stream.ts
+57463acc9ac14a81020067da1979b34c502b7478 33 27 2
+previous 897ebd1121fecddbbcb92899a8d9650ffe47209b src/git/stream.ts
+filename src/git/stream.ts
+57463acc9ac14a81020067da1979b34c502b7478 36 30 1
+previous 897ebd1121fecddbbcb92899a8d9650ffe47209b src/git/stream.ts
+filename src/git/stream.ts
+78049765bd69de4b7fc535ee4aeabb644bcf3028 24 5 1
+author Albin Jacobsson
+author-mail <albin.jacobsson@gmail.com>
+author-time 1601138853
+author-tz +0200
+committer Albin Jacobsson
+committer-mail <albin.jacobsson@gmail.com>
+committer-time 1601138853
+committer-tz +0200
+summary Refactoring
+previous c6f5a6d4b03cb951ed9c0283098d13bf4c39ecca src/git/stream.ts
+filename src/git/stream.ts
+78049765bd69de4b7fc535ee4aeabb644bcf3028 34 8 1
+previous c6f5a6d4b03cb951ed9c0283098d13bf4c39ecca src/git/stream.ts
+filename src/git/stream.ts
+78049765bd69de4b7fc535ee4aeabb644bcf3028 38 12 5
+previous c6f5a6d4b03cb951ed9c0283098d13bf4c39ecca src/git/stream.ts
+filename src/git/stream.ts
+78049765bd69de4b7fc535ee4aeabb644bcf3028 44 18 4
+previous c6f5a6d4b03cb951ed9c0283098d13bf4c39ecca src/git/stream.ts
+filename src/git/stream.ts
+78049765bd69de4b7fc535ee4aeabb644bcf3028 65 39 2
+previous c6f5a6d4b03cb951ed9c0283098d13bf4c39ecca src/git/stream.ts
+filename src/git/stream.ts
+09bf1237bf2290188cc626eddf9aa13c62593b4b 38 10 2
+author Albin Jacobsson
+author-mail <albin.jacobsson@gmail.com>
+author-time 1579705267
+author-tz +0100
+committer Albin Jacobsson
+committer-mail <albin.jacobsson@gmail.com>
+committer-time 1579705267
+committer-tz +0100
+summary Closes #58
+previous 2e18ae16f8e44b634f4f5a0992402cc444e64895 src/git/stream.ts
+filename src/git/stream.ts
+09bf1237bf2290188cc626eddf9aa13c62593b4b 44 22 3
+previous 2e18ae16f8e44b634f4f5a0992402cc444e64895 src/git/stream.ts
+filename src/git/stream.ts
+09bf1237bf2290188cc626eddf9aa13c62593b4b 49 29 1
+previous 2e18ae16f8e44b634f4f5a0992402cc444e64895 src/git/stream.ts
+filename src/git/stream.ts
+09bf1237bf2290188cc626eddf9aa13c62593b4b 57 33 3
+previous 2e18ae16f8e44b634f4f5a0992402cc444e64895 src/git/stream.ts
+filename src/git/stream.ts
+09bf1237bf2290188cc626eddf9aa13c62593b4b 66 38 1
+previous 2e18ae16f8e44b634f4f5a0992402cc444e64895 src/git/stream.ts
+filename src/git/stream.ts
+f6d2b098174aa630d59eae5fc99a01f3a591b8ad 16 7 1
+author Albin Jacobsson
+author-mail <albin.jacobsson@gmail.com>
+author-time 1562163115
+author-tz +0200
+committer Albin Jacobsson
+committer-mail <albin.jacobsson@gmail.com>
+committer-time 1562163115
+committer-tz +0200
+summary Add TSyringe for IoC
+previous 830e9df171adf0d1065da93dbb44fd57a76da62f src/git/stream.ts
+filename src/git/stream.ts
+f6d2b098174aa630d59eae5fc99a01f3a591b8ad 23 9 1
+previous 830e9df171adf0d1065da93dbb44fd57a76da62f src/git/stream.ts
+filename src/git/stream.ts
+f6d2b098174aa630d59eae5fc99a01f3a591b8ad 46 36 2
+previous 830e9df171adf0d1065da93dbb44fd57a76da62f src/git/stream.ts
+filename src/git/stream.ts
+41b858cfcf019fa34597e14719c910fa1409ee66 1 1 1
+author Albin Jacobsson
+author-mail <albin.jacobsson@gmail.com>
+author-time 1561587471
+author-tz +0200
+committer Albin Jacobsson
+committer-mail <albin.jacobsson@gmail.com>
+committer-time 1561588275
+committer-tz +0200
+summary Factory is now responsible for workspace knowledge
+previous 3495b29c4d41a7b07ae93118de53163263bc8c98 src/git/stream.ts
+filename src/git/stream.ts
+41b858cfcf019fa34597e14719c910fa1409ee66 4 3 1
+previous 3495b29c4d41a7b07ae93118de53163263bc8c98 src/git/stream.ts
+filename src/git/stream.ts
+a8cb1e0ba4f8f03ab685bb51a1ac83cc299c020d 47 31 1
+author Albin Jacobsson
+author-mail <albin.jacobsson@gmail.com>
+author-time 1520550992
+author-tz +0100
+committer Albin Jacobsson
+committer-mail <albin.jacobsson@gmail.com>
+committer-time 1520550992
+committer-tz +0100
+summary Adding TSLint and linting the whole project
+previous 7c8fe8fb7f02725fe113b99d6c67773c21611001 src/git/stream.ts
+filename src/git/stream.ts
+3d238f6c9309c7314b3be7be7a4c3f6b3e3de80b 3 2 1
+author Albin Jacobsson
+author-mail <albin.jacobsson@gmail.com>
+author-time 1500502996
+author-tz +0200
+committer Albin Jacobsson
+committer-mail <albin.jacobsson@gmail.com>
+committer-time 1500502996
+committer-tz +0200
+summary 2.0.0 - New blame engine and clearer logging
+filename src/git/stream.ts
+3d238f6c9309c7314b3be7be7a4c3f6b3e3de80b 40 17 1
+filename src/git/stream.ts
+3d238f6c9309c7314b3be7be7a4c3f6b3e3de80b 42 25 2
+filename src/git/stream.ts
+3d238f6c9309c7314b3be7be7a4c3f6b3e3de80b 59 32 1
+filename src/git/stream.ts
+3d238f6c9309c7314b3be7be7a4c3f6b3e3de80b 70 41 1
+filename src/git/stream.ts
+3d238f6c9309c7314b3be7be7a4c3f6b3e3de80b 176 42 1
+filename src/git/stream.ts
diff --git a/test/index.ts b/test/index.ts
deleted file mode 100644
index c438886b..00000000
--- a/test/index.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-// 
-// PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING  
-//
-// This file is providing the test runner to use when running extension tests.
-// By default the test runner in use is Mocha based.
-// 
-// You can provide your own test runner if you want to override it by exporting
-// a function run(testRoot: string, clb: (error:Error) => void) that the extension
-// host can call to run the tests. The test runner is expected to use console.log
-// to report the results back to the caller. When the tests are finished, return
-// a possible error to the callback or null if none.
-
-var testRunner = require('vscode/lib/testrunner');
-
-// You can directly control Mocha options by uncommenting the following lines
-// See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info
-testRunner.configure({
-	ui: 'tdd', 		// the TDD UI is being used in extension.test.ts (suite, test, etc.)
-	useColors: true // colored output from test results
-});
-
-module.exports = testRunner;
\ No newline at end of file
diff --git a/test/run-test.ts b/test/run-test.ts
new file mode 100644
index 00000000..77554080
--- /dev/null
+++ b/test/run-test.ts
@@ -0,0 +1,23 @@
+import { resolve } from "node:path";
+import { runTests } from "@vscode/test-electron";
+
+async function main(): Promise<void> {
+	try {
+		// Download VS Code, unzip it and run the integration test
+		const exitCode = await runTests({
+			extensionDevelopmentPath: resolve(__dirname, ".."),
+			extensionTestsPath: resolve(__dirname, "suite", "index.js"),
+			launchArgs: ["--disable-extensions"],
+		});
+
+		if (exitCode !== 0) {
+			process.exit(1);
+		}
+	} catch {
+		process.exit(1);
+	}
+
+	process.exit(0);
+}
+
+main();
diff --git a/test/suite/execcommand.test.ts b/test/suite/execcommand.test.ts
new file mode 100644
index 00000000..75535ae7
--- /dev/null
+++ b/test/suite/execcommand.test.ts
@@ -0,0 +1,18 @@
+import * as assert from "node:assert";
+import { getGitCommand } from "../../src/git/util/git-command.js";
+import { execute } from "../../src/util/execute.js";
+
+suite("Execute Command", (): void => {
+	test("Simple command", async (): Promise<void> => {
+		const gitCommand = getGitCommand();
+		const commandResult = await execute(gitCommand, ["--version"]);
+
+		assert.ok(commandResult);
+	});
+
+	test("Unavailable command", async (): Promise<void> => {
+		const commandResult = await execute("not-a-real-command", []);
+
+		assert.strictEqual(commandResult, "");
+	});
+});
diff --git a/test/suite/generate-url-tokens.test.ts b/test/suite/generate-url-tokens.test.ts
new file mode 100644
index 00000000..e4d16c34
--- /dev/null
+++ b/test/suite/generate-url-tokens.test.ts
@@ -0,0 +1,631 @@
+import * as assert from "node:assert";
+import { match, stub } from "sinon";
+import { Uri } from "vscode";
+import { generateUrlTokens } from "../../src/git/util/get-tool-url.js";
+import type { LineAttachedCommit } from "../../src/git/util/stream-parsing.js";
+import * as execcommand from "../../src/util/execute.js";
+import * as getActive from "../../src/util/get-active.js";
+import * as property from "../../src/util/property.js";
+import { parseTokens } from "../../src/util/text-decorator.js";
+
+suite("Generate URL Tokens", () => {
+	const call = (
+		func: string | ((index: string | undefined) => string | undefined),
+		arg?: string,
+	) => (typeof func === "function" ? func(arg) : func);
+
+	const exampleCommit: LineAttachedCommit = {
+		commit: {
+			author: {
+				mail: "<vdavydov.dev@gmail.com>",
+				name: "Vladimir Davydov",
+				isCurrentUser: false,
+				timestamp: "1423781950",
+				date: new Date(1423781950000),
+				tz: "-0800",
+			},
+			committer: {
+				mail: "<torvalds@linux-foundation.org>",
+				name: "Linus Torvalds",
+				isCurrentUser: false,
+				timestamp: "1423796049",
+				date: new Date(1423796049000),
+				tz: "-0800",
+			},
+			hash: "60d3fd32a7a9da4c8c93a9f89cfda22a0b4c65ce",
+			summary: "list_lru: introduce per-memcg lists",
+		},
+		filename: "directory/example.file",
+		line: {
+			source: 10,
+			result: 100,
+		},
+	};
+	test("http:// origin", async () => {
+		const activeEditorStub = stub(getActive, "getActiveTextEditor");
+		const execcommandStub = stub(execcommand, "execute");
+		const propertyStub = stub(property, "getProperty");
+		activeEditorStub.returns({
+			document: {
+				isUntitled: false,
+				fileName: "/fake.file",
+				uri: Uri.parse("/fake.file"),
+				lineCount: 1024,
+			},
+			selection: {
+				active: {
+					line: 1,
+				},
+			},
+		});
+		execcommandStub
+			.withArgs(
+				match.string,
+				["symbolic-ref", "-q", "--short", "HEAD"],
+				match.object,
+			)
+			.resolves("master");
+		execcommandStub
+			.withArgs(match.string, ["config", "branch.master.remote"], match.object)
+			.resolves("origin");
+		execcommandStub
+			.withArgs(match.string, ["config", "remote.origin.url"], match.object)
+			.resolves("https://github.com/Sertion/vscode-gitblame.git");
+		execcommandStub
+			.withArgs(
+				match.string,
+				["ls-remote", "--get-url", "origin"],
+				match.object,
+			)
+			.resolves("https://github.com/Sertion/vscode-gitblame.git");
+		execcommandStub
+			.withArgs(
+				match.string,
+				["ls-files", "--full-name", "--", "/fake.file"],
+				match.object,
+			)
+			.resolves("/fake.file");
+		execcommandStub
+			.withArgs(
+				match.string,
+				["rev-parse", "--abbrev-ref", "origin/HEAD"],
+				match.object,
+			)
+			.resolves("origin/main");
+		propertyStub.withArgs("remoteName").returns("origin");
+
+		const tokens = await generateUrlTokens(exampleCommit);
+
+		activeEditorStub.restore();
+		execcommandStub.restore();
+		propertyStub.restore();
+
+		if (tokens === undefined) {
+			assert.notStrictEqual(tokens, undefined);
+			return;
+		}
+
+		assert.strictEqual(call(tokens["gitorigin.hostname"], ""), "github.com");
+		assert.strictEqual(call(tokens["gitorigin.hostname"], "0"), "github");
+		assert.strictEqual(call(tokens["gitorigin.hostname"], "1"), "com");
+		assert.strictEqual(
+			call(tokens["gitorigin.path"], ""),
+			"/Sertion/vscode-gitblame",
+		);
+		assert.strictEqual(call(tokens["gitorigin.path"], "0"), "Sertion");
+		assert.strictEqual(call(tokens["gitorigin.path"], "1"), "vscode-gitblame");
+		assert.strictEqual(
+			call(tokens.hash),
+			"60d3fd32a7a9da4c8c93a9f89cfda22a0b4c65ce",
+		);
+		assert.strictEqual(call(tokens["project.name"]), "vscode-gitblame");
+		assert.strictEqual(
+			call(tokens["project.remote"]),
+			"github.com/Sertion/vscode-gitblame",
+		);
+		assert.strictEqual(call(tokens["file.path"]), "/fake.file");
+	});
+
+	test("git@ origin", async () => {
+		const activeEditorStub = stub(getActive, "getActiveTextEditor");
+		const execcommandStub = stub(execcommand, "execute");
+		const propertyStub = stub(property, "getProperty");
+		activeEditorStub.returns({
+			document: {
+				isUntitled: false,
+				fileName: "/fake.file",
+				uri: Uri.parse("/fake.file"),
+				lineCount: 1024,
+			},
+			selection: {
+				active: {
+					line: 1,
+				},
+			},
+		});
+		execcommandStub
+			.withArgs(
+				match.string,
+				["symbolic-ref", "-q", "--short", "HEAD"],
+				match.object,
+			)
+			.resolves("master");
+		execcommandStub
+			.withArgs(match.string, ["config", "branch.master.remote"], match.object)
+			.resolves("origin");
+		execcommandStub
+			.withArgs(match.string, ["config", "remote.origin.url"], match.object)
+			.resolves("git@github.com:Sertion/vscode-gitblame.git");
+		execcommandStub
+			.withArgs(
+				match.string,
+				["ls-remote", "--get-url", "origin"],
+				match.object,
+			)
+			.resolves("git@github.com:Sertion/vscode-gitblame.git");
+		execcommandStub
+			.withArgs(
+				match.string,
+				["ls-files", "--full-name", "--", "/fake.file"],
+				match.object,
+			)
+			.resolves("/fake.file");
+		execcommandStub
+			.withArgs(
+				match.string,
+				["rev-parse", "--abbrev-ref", "origin/HEAD"],
+				match.object,
+			)
+			.resolves("origin/main");
+		propertyStub.withArgs("remoteName").returns("origin");
+
+		const tokens = await generateUrlTokens(exampleCommit);
+
+		activeEditorStub.restore();
+		execcommandStub.restore();
+		propertyStub.restore();
+
+		if (tokens === undefined) {
+			assert.notStrictEqual(tokens, undefined);
+			return;
+		}
+
+		assert.strictEqual(call(tokens["gitorigin.hostname"], ""), "github.com");
+		assert.strictEqual(call(tokens["gitorigin.hostname"], "0"), "github");
+		assert.strictEqual(call(tokens["gitorigin.hostname"], "1"), "com");
+		assert.strictEqual(
+			call(tokens["gitorigin.path"], ""),
+			"/Sertion/vscode-gitblame",
+		);
+		assert.strictEqual(call(tokens["gitorigin.path"], "0"), "Sertion");
+		assert.strictEqual(call(tokens["gitorigin.path"], "1"), "vscode-gitblame");
+		assert.strictEqual(
+			call(tokens.hash),
+			"60d3fd32a7a9da4c8c93a9f89cfda22a0b4c65ce",
+		);
+		assert.strictEqual(call(tokens["project.name"]), "vscode-gitblame");
+		assert.strictEqual(
+			call(tokens["project.remote"]),
+			"github.com/Sertion/vscode-gitblame",
+		);
+		assert.strictEqual(call(tokens["file.path"]), "/fake.file");
+	});
+
+	test("ssh://git@ origin", async () => {
+		const activeEditorStub = stub(getActive, "getActiveTextEditor");
+		const execcommandStub = stub(execcommand, "execute");
+		const propertyStub = stub(property, "getProperty");
+		activeEditorStub.returns({
+			document: {
+				isUntitled: false,
+				fileName: "/fake.file",
+				uri: Uri.parse("/fake.file"),
+				lineCount: 1024,
+			},
+			selection: {
+				active: {
+					line: 22,
+				},
+			},
+		});
+		execcommandStub
+			.withArgs(
+				match.string,
+				["symbolic-ref", "-q", "--short", "HEAD"],
+				match.object,
+			)
+			.resolves("master");
+		execcommandStub
+			.withArgs(match.string, ["config", "branch.master.remote"], match.object)
+			.resolves("origin");
+		execcommandStub
+			.withArgs(match.string, ["config", "remote.origin.url"], match.object)
+			.resolves("ssh://git@github.com/Sertion/vscode-gitblame.git");
+		execcommandStub
+			.withArgs(
+				match.string,
+				["ls-remote", "--get-url", "origin"],
+				match.object,
+			)
+			.resolves("ssh://git@github.com/Sertion/vscode-gitblame.git");
+		execcommandStub
+			.withArgs(
+				match.string,
+				["ls-files", "--full-name", "--", "/fake.file"],
+				match.object,
+			)
+			.resolves("/fake.file");
+		execcommandStub
+			.withArgs(
+				match.string,
+				["rev-parse", "--abbrev-ref", "origin/HEAD"],
+				match.object,
+			)
+			.resolves("origin/main");
+		propertyStub.withArgs("remoteName").returns("origin");
+
+		const tokens = await generateUrlTokens(exampleCommit);
+
+		activeEditorStub.restore();
+		execcommandStub.restore();
+		propertyStub.restore();
+
+		if (tokens === undefined) {
+			assert.notStrictEqual(tokens, undefined);
+			return;
+		}
+
+		assert.strictEqual(call(tokens["gitorigin.hostname"], ""), "github.com");
+		assert.strictEqual(call(tokens["gitorigin.hostname"], "0"), "github");
+		assert.strictEqual(call(tokens["gitorigin.hostname"], "1"), "com");
+		assert.strictEqual(
+			call(tokens["gitorigin.path"], ""),
+			"/Sertion/vscode-gitblame",
+		);
+		assert.strictEqual(call(tokens["gitorigin.path"], "0"), "Sertion");
+		assert.strictEqual(call(tokens["gitorigin.path"], "1"), "vscode-gitblame");
+		assert.strictEqual(
+			call(tokens.hash),
+			"60d3fd32a7a9da4c8c93a9f89cfda22a0b4c65ce",
+		);
+		assert.strictEqual(call(tokens["project.name"]), "vscode-gitblame");
+		assert.strictEqual(
+			call(tokens["project.remote"]),
+			"github.com/Sertion/vscode-gitblame",
+		);
+		assert.strictEqual(call(tokens["file.path"]), "/fake.file");
+		assert.strictEqual(call(tokens["file.line"]), "100");
+	});
+
+	test("ssh://git@git.company.com/project_x/test-repository.git origin", async () => {
+		const activeEditorStub = stub(getActive, "getActiveTextEditor");
+		const execcommandStub = stub(execcommand, "execute");
+		const propertyStub = stub(property, "getProperty");
+		activeEditorStub.returns({
+			document: {
+				isUntitled: false,
+				fileName: "/fake.file",
+				uri: Uri.parse("/fake.file"),
+				lineCount: 1024,
+			},
+			selection: {
+				active: {
+					line: 9,
+				},
+			},
+		});
+		execcommandStub
+			.withArgs(
+				match.string,
+				["symbolic-ref", "-q", "--short", "HEAD"],
+				match.object,
+			)
+			.resolves("master");
+		execcommandStub
+			.withArgs(match.string, ["config", "branch.master.remote"], match.object)
+			.resolves("origin");
+		execcommandStub
+			.withArgs(match.string, ["config", "remote.origin.url"], match.object)
+			.resolves("ssh://git@git.company.com/project_x/test-repository.git");
+		execcommandStub
+			.withArgs(
+				match.string,
+				["ls-remote", "--get-url", "origin"],
+				match.object,
+			)
+			.resolves("ssh://git@git.company.com/project_x/test-repository.git");
+		execcommandStub
+			.withArgs(
+				match.string,
+				["ls-files", "--full-name", "--", "/fake.file"],
+				match.object,
+			)
+			.resolves("/fake.file");
+		execcommandStub
+			.withArgs(
+				match.string,
+				["rev-parse", "--abbrev-ref", "origin/HEAD"],
+				match.object,
+			)
+			.resolves("origin/main");
+		propertyStub.withArgs("remoteName").returns("origin");
+
+		const tokens = await generateUrlTokens(exampleCommit);
+
+		activeEditorStub.restore();
+		execcommandStub.restore();
+		propertyStub.restore();
+
+		if (tokens === undefined) {
+			assert.notStrictEqual(tokens, undefined);
+			return;
+		}
+
+		assert.strictEqual(
+			call(tokens["gitorigin.hostname"], ""),
+			"git.company.com",
+		);
+		assert.strictEqual(call(tokens["gitorigin.hostname"], "0"), "git");
+		assert.strictEqual(call(tokens["gitorigin.hostname"], "1"), "company");
+		assert.strictEqual(call(tokens["gitorigin.hostname"], "2"), "com");
+		assert.strictEqual(
+			call(tokens["gitorigin.path"], ""),
+			"/project_x/test-repository",
+		);
+		assert.strictEqual(call(tokens["gitorigin.path"], "0"), "project_x");
+		assert.strictEqual(call(tokens["gitorigin.path"], "1"), "test-repository");
+		assert.strictEqual(
+			call(tokens.hash),
+			"60d3fd32a7a9da4c8c93a9f89cfda22a0b4c65ce",
+		);
+		assert.strictEqual(call(tokens["project.name"]), "test-repository");
+		assert.strictEqual(
+			call(tokens["project.remote"]),
+			"git.company.com/project_x/test-repository",
+		);
+		assert.strictEqual(call(tokens["file.path"]), "/fake.file");
+		assert.strictEqual(call(tokens["file.line"]), "100");
+	});
+
+	test("local development (#128 regression)", async () => {
+		const activeEditorStub = stub(getActive, "getActiveTextEditor");
+		const execcommandStub = stub(execcommand, "execute");
+		const propertyStub = stub(property, "getProperty");
+		activeEditorStub.returns({
+			document: {
+				isUntitled: false,
+				fileName: "/fake.file",
+				uri: Uri.parse("/fake.file"),
+				lineCount: 1024,
+			},
+			selection: {
+				active: {
+					line: 9,
+				},
+			},
+		});
+		execcommandStub
+			.withArgs(
+				match.string,
+				["symbolic-ref", "-q", "--short", "HEAD"],
+				match.object,
+			)
+			.resolves("master");
+		execcommandStub
+			.withArgs(match.string, ["config", "branch.master.remote"], match.object)
+			.resolves("");
+		execcommandStub
+			.withArgs(match.string, ["config", "remote.origin.url"], match.object)
+			.resolves("");
+		execcommandStub
+			.withArgs(
+				match.string,
+				["ls-remote", "--get-url", "origin"],
+				match.object,
+			)
+			.resolves("origin");
+		execcommandStub
+			.withArgs(
+				match.string,
+				["ls-files", "--full-name", "--", "/fake.file"],
+				match.object,
+			)
+			.resolves("/fake.file");
+		execcommandStub
+			.withArgs(
+				match.string,
+				["rev-parse", "--abbrev-ref", "origin/HEAD"],
+				match.object,
+			)
+			.resolves("origin/main");
+		propertyStub.withArgs("remoteName").returns("origin");
+
+		const tokens = await generateUrlTokens(exampleCommit);
+
+		activeEditorStub.restore();
+		execcommandStub.restore();
+		propertyStub.restore();
+
+		assert.strictEqual(tokens, undefined);
+	});
+});
+
+suite("Use generated URL tokens", () => {
+	const exampleCommit: LineAttachedCommit = {
+		commit: {
+			author: {
+				mail: "<vdavydov.dev@gmail.com>",
+				name: "Vladimir Davydov",
+				isCurrentUser: false,
+				timestamp: "1423781950",
+				date: new Date(1423781950000),
+				tz: "-0800",
+			},
+			committer: {
+				mail: "<torvalds@linux-foundation.org>",
+				name: "Linus Torvalds",
+				isCurrentUser: false,
+				timestamp: "1423796049",
+				date: new Date(1423796049000),
+				tz: "-0800",
+			},
+			hash: "60d3fd32a7a9da4c8c93a9f89cfda22a0b4c65ce",
+			summary: "list_lru: introduce per-memcg lists",
+		},
+		filename: "directory/example.file",
+		line: {
+			source: 10,
+			result: 100,
+		},
+	};
+
+	test("Default value", async () => {
+		const activeEditorStub = stub(getActive, "getActiveTextEditor");
+		const execcommandStub = stub(execcommand, "execute");
+		const propertyStub = stub(property, "getProperty");
+		activeEditorStub.returns({
+			document: {
+				isUntitled: false,
+				fileName: "/fake.file",
+				uri: Uri.parse("/fake.file"),
+				lineCount: 1024,
+			},
+			selection: {
+				active: {
+					line: 9,
+				},
+			},
+		});
+		execcommandStub
+			.withArgs(
+				match.string,
+				["symbolic-ref", "-q", "--short", "HEAD"],
+				match.object,
+			)
+			.resolves("master");
+		execcommandStub
+			.withArgs(match.string, ["config", "branch.master.remote"], match.object)
+			.resolves("origin");
+		execcommandStub
+			.withArgs(match.string, ["config", "remote.origin.url"], match.object)
+			.resolves("ssh://git@git.company.com/project_x/test-repository.git");
+		execcommandStub
+			.withArgs(
+				match.string,
+				["ls-remote", "--get-url", "origin"],
+				match.object,
+			)
+			.resolves("ssh://git@git.company.com/project_x/test-repository.git");
+		execcommandStub
+			.withArgs(
+				match.string,
+				["ls-files", "--full-name", "--", "/fake.file"],
+				match.object,
+			)
+			.resolves("/fake.file");
+		execcommandStub
+			.withArgs(
+				match.string,
+				["rev-parse", "--abbrev-ref", "origin/HEAD"],
+				match.object,
+			)
+			.resolves("origin/main");
+		propertyStub.withArgs("remoteName").returns("origin");
+
+		const tokens = await generateUrlTokens(exampleCommit);
+
+		activeEditorStub.restore();
+		execcommandStub.restore();
+		propertyStub.restore();
+
+		if (tokens === undefined) {
+			assert.notStrictEqual(tokens, undefined);
+			return;
+		}
+
+		const parsedUrl = parseTokens(
+			"${tool.protocol}//${gitorigin.hostname}${gitorigin.port}${gitorigin.path}${tool.commitpath}${hash}",
+			tokens,
+		);
+
+		assert.strictEqual(
+			parsedUrl,
+			"https://git.company.com/project_x/test-repository/commit/60d3fd32a7a9da4c8c93a9f89cfda22a0b4c65ce",
+		);
+	});
+
+	test("Url with port (#188 regression)", async () => {
+		const activeEditorStub = stub(getActive, "getActiveTextEditor");
+		const execcommandStub = stub(execcommand, "execute");
+		const propertyStub = stub(property, "getProperty");
+		activeEditorStub.returns({
+			document: {
+				isUntitled: false,
+				fileName: "/fake.file",
+				uri: Uri.parse("/fake.file"),
+				lineCount: 1024,
+			},
+			selection: {
+				active: {
+					line: 9,
+				},
+			},
+		});
+		execcommandStub
+			.withArgs(
+				match.string,
+				["symbolic-ref", "-q", "--short", "HEAD"],
+				match.object,
+			)
+			.resolves("master");
+		execcommandStub
+			.withArgs(match.string, ["config", "branch.master.remote"], match.object)
+			.resolves("origin");
+		execcommandStub
+			.withArgs(match.string, ["config", "remote.origin.url"], match.object)
+			.resolves("http://git.company.com:8080/project_x/test-repository.git");
+		execcommandStub
+			.withArgs(
+				match.string,
+				["ls-remote", "--get-url", "origin"],
+				match.object,
+			)
+			.resolves("http://git.company.com:8080/project_x/test-repository.git");
+		execcommandStub
+			.withArgs(
+				match.string,
+				["ls-files", "--full-name", "--", "/fake.file"],
+				match.object,
+			)
+			.resolves("/fake.file");
+		execcommandStub
+			.withArgs(
+				match.string,
+				["rev-parse", "--abbrev-ref", "origin/HEAD"],
+				match.object,
+			)
+			.resolves("origin/main");
+		propertyStub.withArgs("remoteName").returns("origin");
+
+		const tokens = await generateUrlTokens(exampleCommit);
+
+		activeEditorStub.restore();
+		execcommandStub.restore();
+		propertyStub.restore();
+
+		if (tokens === undefined) {
+			assert.notStrictEqual(tokens, undefined);
+			return;
+		}
+
+		const parsedUrl = parseTokens(
+			"${tool.protocol}//${gitorigin.hostname}${gitorigin.port}${gitorigin.path}${tool.commitpath}${hash}",
+			tokens,
+		);
+
+		assert.strictEqual(
+			parsedUrl,
+			"http://git.company.com:8080/project_x/test-repository/commit/60d3fd32a7a9da4c8c93a9f89cfda22a0b4c65ce",
+		);
+	});
+});
diff --git a/test/suite/get-tool-url.test.ts b/test/suite/get-tool-url.test.ts
new file mode 100644
index 00000000..533939d4
--- /dev/null
+++ b/test/suite/get-tool-url.test.ts
@@ -0,0 +1,88 @@
+import * as assert from "node:assert";
+
+import { gitRemotePath } from "../../src/git/util/get-tool-url.js";
+
+suite("Get tool URL: gitRemotePath", (): void => {
+	const call = (
+		func: string | ((param?: string) => string | undefined),
+		arg?: string,
+	) => (typeof func === "string" ? func : func(arg));
+
+	test("http://", (): void => {
+		const func = gitRemotePath("http://example.com/path/to/something/");
+
+		assert.strictEqual(call(func), "/path/to/something/");
+		assert.strictEqual(call(func, "0"), "path");
+		assert.strictEqual(call(func, "1"), "to");
+		assert.strictEqual(call(func, "2"), "something");
+	});
+	test("https://", (): void => {
+		const func = gitRemotePath("https://example.com/path/to/something/");
+
+		assert.strictEqual(call(func), "/path/to/something/");
+		assert.strictEqual(call(func, "0"), "path");
+		assert.strictEqual(call(func, "1"), "to");
+		assert.strictEqual(call(func, "2"), "something");
+	});
+	test("ssh://", (): void => {
+		const func = gitRemotePath("ssh://example.com/path/to/something/");
+
+		assert.strictEqual(call(func), "/path/to/something/");
+		assert.strictEqual(call(func, "0"), "path");
+		assert.strictEqual(call(func, "1"), "to");
+		assert.strictEqual(call(func, "2"), "something");
+	});
+	test("git@", (): void => {
+		const func = gitRemotePath("git@example.com:path/to/something/");
+
+		assert.strictEqual(call(func), "/path/to/something/");
+		assert.strictEqual(call(func, "0"), "path");
+		assert.strictEqual(call(func, "1"), "to");
+		assert.strictEqual(call(func, "2"), "something");
+	});
+	test("org-1234@", (): void => {
+		const func = gitRemotePath("org-1234@example.com:path/to/something/");
+
+		assert.strictEqual(call(func), "/path/to/something/");
+		assert.strictEqual(call(func, "0"), "path");
+		assert.strictEqual(call(func, "1"), "to");
+		assert.strictEqual(call(func, "2"), "something");
+	});
+	test("http:// with port", (): void => {
+		const func = gitRemotePath("http://example.com:8080/path/to/something/");
+
+		assert.strictEqual(call(func), "/path/to/something/");
+		assert.strictEqual(call(func, "0"), "path");
+		assert.strictEqual(call(func, "1"), "to");
+		assert.strictEqual(call(func, "2"), "something");
+	});
+	test("https:// with port", (): void => {
+		const func = gitRemotePath("https://example.com:8080/path/to/something/");
+
+		assert.strictEqual(call(func), "/path/to/something/");
+		assert.strictEqual(call(func, "0"), "path");
+		assert.strictEqual(call(func, "1"), "to");
+		assert.strictEqual(call(func, "2"), "something");
+	});
+	test("ssh:// with port", (): void => {
+		const func = gitRemotePath("ssh://example.com:8080/path/to/something/");
+
+		assert.strictEqual(call(func), "/path/to/something/");
+		assert.strictEqual(call(func, "0"), "path");
+		assert.strictEqual(call(func, "1"), "to");
+		assert.strictEqual(call(func, "2"), "something");
+	});
+
+	test("Empty input", (): void => {
+		const func = gitRemotePath("");
+
+		assert.strictEqual(call(func), "no-remote-url");
+		assert.strictEqual(call(func), "no-remote-url");
+	});
+	test("Weird input", (): void => {
+		const func = gitRemotePath("weird input");
+
+		assert.strictEqual(call(func), "no-remote-url");
+		assert.strictEqual(call(func), "no-remote-url");
+	});
+});
diff --git a/test/suite/git-processing.test.ts b/test/suite/git-processing.test.ts
new file mode 100644
index 00000000..64182bb4
--- /dev/null
+++ b/test/suite/git-processing.test.ts
@@ -0,0 +1,105 @@
+import * as assert from "node:assert";
+import { readFileSync } from "node:fs";
+import { resolve } from "node:path";
+
+import {
+	type Commit,
+	type CommitAuthor,
+	type CommitRegistry,
+	type LineAttachedCommit,
+	processChunk,
+} from "../../src/git/util/stream-parsing.js";
+
+type CommitAuthorStringDate = Omit<CommitAuthor, "date"> & {
+	date: string;
+};
+type CommitWithAuthorStringDate = Omit<Commit, "author" | "committer"> & {
+	author: CommitAuthorStringDate;
+	committer: CommitAuthorStringDate;
+};
+
+function load(fileName: string, buffer: true): Buffer;
+function load(fileName: string, buffer: false): string;
+function load(fileName: string, buffer: boolean): string | Buffer {
+	return readFileSync(
+		resolve(__dirname, "..", "..", "..", "test", "fixture", fileName),
+		{
+			encoding: buffer ? null : "utf-8",
+		},
+	);
+}
+function datesToString(
+	convert: LineAttachedCommit[],
+): LineAttachedCommit<CommitWithAuthorStringDate>[] {
+	const converted: LineAttachedCommit<CommitWithAuthorStringDate>[] = [];
+	for (const element of convert) {
+		converted.push({
+			...element,
+			commit: {
+				...element.commit,
+				author: {
+					...element.commit.author,
+					date: element.commit.author.date.toJSON(),
+				},
+				committer: {
+					...element.commit.committer,
+					date: element.commit.committer.date.toJSON(),
+				},
+			},
+		});
+	}
+	return converted;
+}
+
+suite("Chunk Processing", (): void => {
+	test("Process normal chunk", async (): Promise<void> => {
+		const chunk = load("git-stream-blame-incremental.chunks", true);
+		const result = JSON.parse(
+			load("git-stream-blame-incremental-result.json", false),
+		) as string[];
+
+		const registry: CommitRegistry = new Map();
+		const foundChunks: LineAttachedCommit[] = [];
+		for await (const line of processChunk(
+			chunk,
+			"not-a-real@email",
+			registry,
+		)) {
+			foundChunks.push(line);
+		}
+
+		assert.strictEqual(
+			JSON.stringify(datesToString(foundChunks)),
+			JSON.stringify(result),
+		);
+	});
+});
+
+suite("Processing Errors", (): void => {
+	test("Git chunk not starting with commit information", async (): Promise<void> => {
+		const chunks = JSON.parse(
+			load("git-stream-blame-incremental-multi-chunk.json", false),
+		) as string[];
+		const result = JSON.parse(
+			load("git-stream-blame-incremental-multi-chunk-result.json", false),
+		) as string[];
+
+		const registry: CommitRegistry = new Map();
+
+		const foundChunks: LineAttachedCommit[] = [];
+		for (const chunk of chunks) {
+			for await (const line of processChunk(
+				Buffer.from(chunk, "utf-8"),
+				"not-a-real@email",
+				registry,
+			)) {
+				foundChunks.push(line);
+			}
+		}
+
+		assert.strictEqual(
+			JSON.stringify(datesToString(foundChunks)),
+			JSON.stringify(result),
+		);
+	});
+});
diff --git a/test/suite/index.ts b/test/suite/index.ts
new file mode 100644
index 00000000..ac12eab6
--- /dev/null
+++ b/test/suite/index.ts
@@ -0,0 +1,32 @@
+import { opendir } from "node:fs/promises";
+import { resolve } from "node:path";
+
+import * as Mocha from "mocha";
+
+export async function run(): Promise<void> {
+	const mocha = new Mocha({
+		ui: "tdd",
+		color: true,
+	});
+
+	const files = await opendir(__dirname);
+
+	for await (const dirent of files) {
+		if (
+			dirent.isFile() &&
+			(dirent.name.endsWith(".test.js") || dirent.name.endsWith(".test.js"))
+		) {
+			mocha.addFile(resolve(__dirname, dirent.name));
+		}
+	}
+
+	return new Promise((resolvePromise, reject): void => {
+		mocha.run((failures): void => {
+			if (failures > 0) {
+				reject(new Error(`${failures} tests failed.`));
+			} else {
+				resolvePromise();
+			}
+		});
+	});
+}
diff --git a/test/suite/is-url.test.ts b/test/suite/is-url.test.ts
new file mode 100644
index 00000000..a0aec3c5
--- /dev/null
+++ b/test/suite/is-url.test.ts
@@ -0,0 +1,26 @@
+import * as assert from "node:assert";
+import { isUrl } from "../../src/util/is-url.js";
+
+suite("Is URL", (): void => {
+	test("Valid", (): void => {
+		assert.strictEqual(isUrl("http://github.com/"), true);
+		assert.strictEqual(isUrl("https://microsoft.com/"), true);
+		assert.strictEqual(isUrl("https://vscode.co.uk/"), true);
+		assert.strictEqual(isUrl("https://example.com/some-path"), true);
+		assert.strictEqual(isUrl("https://example.com/some-path.ext"), true);
+		assert.strictEqual(isUrl("https://host:8080/some-path.ext"), true);
+		assert.strictEqual(isUrl("https://user:pass@host:8080/path.ext"), true);
+	});
+
+	test("Invalid", (): void => {
+		assert.strictEqual(isUrl("ftp://github.com/"), false);
+		assert.strictEqual(isUrl("http:github.com"), false);
+		assert.strictEqual(isUrl("http:github.com/some-path"), false);
+		assert.strictEqual(isUrl("%"), false);
+		assert.strictEqual(isUrl("/file.ext"), false);
+		assert.strictEqual(isUrl("protocol:user@/file.ext"), false);
+		assert.strictEqual(isUrl("http://"), false);
+		assert.strictEqual(isUrl("This is not an URL"), false);
+		assert.strictEqual(isUrl("http://🐆.com/"), false);
+	});
+});
diff --git a/test/suite/origin-url-to-tool-url.test.ts b/test/suite/origin-url-to-tool-url.test.ts
new file mode 100644
index 00000000..9c8cc59d
--- /dev/null
+++ b/test/suite/origin-url-to-tool-url.test.ts
@@ -0,0 +1,161 @@
+import * as assert from "node:assert";
+import { stub } from "sinon";
+
+import { originUrlToToolUrl } from "../../src/git/util/origin-url-to-tool-url.js";
+import * as prop from "../../src/util/property.js";
+
+suite("Web URL formatting", (): void => {
+	test("https://", (): void => {
+		assert.strictEqual(
+			originUrlToToolUrl("https://example.com/user/repo.git")?.toString(),
+			"https://example.com/user/repo",
+		);
+		assert.strictEqual(
+			originUrlToToolUrl("https://example.com/user/repo")?.toString(),
+			"https://example.com/user/repo",
+		);
+	});
+
+	test("git@", (): void => {
+		assert.strictEqual(
+			originUrlToToolUrl("git@example.com:user/repo.git")?.toString(),
+			"https://example.com/user/repo",
+		);
+		assert.strictEqual(
+			originUrlToToolUrl("git@example.com:user/repo")?.toString(),
+			"https://example.com/user/repo",
+		);
+	});
+
+	test("username@", (): void => {
+		assert.strictEqual(
+			originUrlToToolUrl("username@example.com:user/repo.git")?.toString(),
+			"https://example.com/user/repo",
+		);
+		assert.strictEqual(
+			originUrlToToolUrl("username@example.com:user/repo")?.toString(),
+			"https://example.com/user/repo",
+		);
+	});
+
+	test("username:password@", (): void => {
+		assert.strictEqual(
+			originUrlToToolUrl(
+				"username:password@example.com:user/repo.git",
+			)?.toString(),
+			"https://example.com/user/repo",
+		);
+		assert.strictEqual(
+			originUrlToToolUrl("username@example.com:user/repo")?.toString(),
+			"https://example.com/user/repo",
+		);
+	});
+
+	test("https:// with port", (): void => {
+		assert.strictEqual(
+			originUrlToToolUrl("https://example.com:8080/user/repo.git")?.toString(),
+			"https://example.com:8080/user/repo",
+		);
+		assert.strictEqual(
+			originUrlToToolUrl("https://example.com:8080/user/repo")?.toString(),
+			"https://example.com:8080/user/repo",
+		);
+	});
+
+	test("http:// with port", (): void => {
+		assert.strictEqual(
+			originUrlToToolUrl("http://example.com:8080/user/repo.git")?.toString(),
+			"http://example.com:8080/user/repo",
+		);
+		assert.strictEqual(
+			originUrlToToolUrl("http://example.com:8080/user/repo")?.toString(),
+			"http://example.com:8080/user/repo",
+		);
+	});
+
+	test("git@ with port", (): void => {
+		assert.strictEqual(
+			originUrlToToolUrl("git@example.com:8080/user/repo.git")?.toString(),
+			"https://example.com/user/repo",
+		);
+		assert.strictEqual(
+			originUrlToToolUrl("git@example.com:8080/user/repo")?.toString(),
+			"https://example.com/user/repo",
+		);
+	});
+
+	test("git@ with port and password", (): void => {
+		assert.strictEqual(
+			originUrlToToolUrl("git:pass@example.com:8080/user/repo.git")?.toString(),
+			"https://example.com/user/repo",
+		);
+		assert.strictEqual(
+			originUrlToToolUrl("git@example.com:8080/user/repo")?.toString(),
+			"https://example.com/user/repo",
+		);
+	});
+
+	test("https:// with port, username and password", (): void => {
+		assert.strictEqual(
+			originUrlToToolUrl(
+				"https://user:pass@example.com:8080/user/repo.git",
+			)?.toString(),
+			"https://example.com:8080/user/repo",
+		);
+	});
+
+	test("https:// with username and password", (): void => {
+		assert.strictEqual(
+			originUrlToToolUrl(
+				"https://user:pass@example.com/user/repo.git",
+			)?.toString(),
+			"https://example.com/user/repo",
+		);
+	});
+
+	test("https:// plural", (): void => {
+		const propertyStub = stub(prop, "getProperty");
+		propertyStub.withArgs("pluralWebPathSubstrings").returns(["example.com"]);
+
+		assert.strictEqual(
+			originUrlToToolUrl("https://example.com/user/repo.git")?.toString(),
+			"https://example.com/user/repo",
+		);
+		assert.strictEqual(
+			originUrlToToolUrl("https://example.com/user/repo")?.toString(),
+			"https://example.com/user/repo",
+		);
+
+		propertyStub.restore();
+	});
+
+	test("ssh:// short host no user", (): void => {
+		assert.strictEqual(
+			originUrlToToolUrl("ssh://user@host:8080/SomeProject.git")?.toString(),
+			"https://host/SomeProject",
+		);
+		assert.strictEqual(
+			originUrlToToolUrl("ssh://user@host:8080/SomeProject")?.toString(),
+			"https://host/SomeProject",
+		);
+	});
+
+	test("non-alphanumeric in path", (): void => {
+		assert.strictEqual(
+			originUrlToToolUrl("https://example.com/us.er/repo.git")?.toString(),
+			"https://example.com/us.er/repo",
+		);
+		assert.strictEqual(
+			originUrlToToolUrl("https://example.com/user/re-po.git")?.toString(),
+			"https://example.com/user/re-po",
+		);
+		assert.strictEqual(
+			originUrlToToolUrl("https://example.com/user/re%20po.git")?.toString(),
+			"https://example.com/user/re%20po",
+		);
+		assert.strictEqual(
+			originUrlToToolUrl("ssh://user@example.com:us.er/repo.git")?.toString(),
+			"https://example.com/us.er/repo",
+		);
+	});
+});
diff --git a/test/suite/project-name-from-origin.test.ts b/test/suite/project-name-from-origin.test.ts
new file mode 100644
index 00000000..2874d75a
--- /dev/null
+++ b/test/suite/project-name-from-origin.test.ts
@@ -0,0 +1,57 @@
+import * as assert from "node:assert";
+
+import { projectNameFromOrigin } from "../../src/git/util/project-name-from-origin.js";
+
+suite("Origin to project name", (): void => {
+	test("https://", (): void => {
+		assert.strictEqual(
+			projectNameFromOrigin("https://example.com/user/repo.git"),
+			"repo",
+		);
+		assert.strictEqual(
+			projectNameFromOrigin("https://example.com/user/repo"),
+			"repo",
+		);
+	});
+
+	test("git@", (): void => {
+		assert.strictEqual(
+			projectNameFromOrigin("git@example.com/user/repo.git"),
+			"repo",
+		);
+		assert.strictEqual(
+			projectNameFromOrigin("git@example.com/user/repo"),
+			"repo",
+		);
+	});
+
+	test("longer than normal path", (): void => {
+		assert.strictEqual(
+			projectNameFromOrigin("git@example.com/company/group/user/repo.git"),
+			"repo",
+		);
+		assert.strictEqual(
+			projectNameFromOrigin("git@example.com/company/group/user/repo"),
+			"repo",
+		);
+	});
+
+	test("non-alphanumeric in path", (): void => {
+		assert.strictEqual(
+			projectNameFromOrigin("https://example.com/user/re-po.git"),
+			"re-po",
+		);
+		assert.strictEqual(
+			projectNameFromOrigin("https://example.com/us.er/repo.git"),
+			"repo",
+		);
+		assert.strictEqual(
+			projectNameFromOrigin("https://example.com/user/re.po.git"),
+			"re.po",
+		);
+		assert.strictEqual(
+			projectNameFromOrigin("https://example.com/user/re.po"),
+			"re.po",
+		);
+	});
+});
diff --git a/test/suite/queue.test.ts b/test/suite/queue.test.ts
new file mode 100644
index 00000000..df554448
--- /dev/null
+++ b/test/suite/queue.test.ts
@@ -0,0 +1,154 @@
+import * as assert from "node:assert";
+import { afterEach, beforeEach } from "mocha";
+import * as Sinon from "sinon";
+import { Queue } from "../../src/git/queue.js";
+
+const sleep = <T>(time: number, response: T): Promise<T> => {
+	return new Promise((resolve) =>
+		setTimeout(() => {
+			resolve(response);
+		}, time),
+	);
+};
+
+suite("Promise Queue", (): void => {
+	let clock: Sinon.SinonFakeTimers | undefined;
+
+	beforeEach(() => {
+		clock = Sinon.useFakeTimers();
+	});
+
+	afterEach(() => {
+		clock?.restore();
+	});
+
+	test("Create Instance", (): void => {
+		const instance = new Queue();
+
+		assert.deepStrictEqual(
+			instance instanceof Queue,
+			true,
+			"is instance of Queue",
+		);
+	});
+
+	test("Pass through when queue is short", async (): Promise<void> => {
+		const instance = new Queue();
+		const spy = Sinon.spy(() => Promise.resolve());
+
+		await instance.add(spy);
+
+		Sinon.assert.calledOnce(spy);
+	});
+
+	test("Don't run when adding to queue and it is to long, then run it when ready", async (): Promise<void> => {
+		const instance = new Queue<undefined>();
+
+		const spy1 = Sinon.spy(() => sleep(100, undefined));
+		const spy2 = Sinon.spy(() => sleep(100, undefined));
+		const spy3 = Sinon.spy(() => sleep(100, undefined));
+
+		instance.add(spy1);
+		instance.add(spy2);
+		instance.add(spy3);
+
+		await clock?.tickAsync(50);
+
+		Sinon.assert.calledOnce(spy1);
+		Sinon.assert.calledOnce(spy2);
+		Sinon.assert.notCalled(spy3);
+
+		await clock?.tickAsync(50);
+
+		Sinon.assert.calledOnce(spy2);
+
+		await clock?.runAllAsync();
+	});
+
+	test("Being able to use the value from the original promise", async (): Promise<void> => {
+		const instance = new Queue<string>();
+		const myFunc = () => Promise.resolve("UNIQUE_VALUE");
+
+		const result = await instance.add(myFunc);
+
+		assert.strictEqual(result, "UNIQUE_VALUE");
+	});
+
+	test("Being able to use the value from the original promise after queue", async (): Promise<void> => {
+		const instance = new Queue<string>();
+		const myFunc1 = () => sleep(100, "UNIQUE_VALUE_1");
+		const myFunc2 = () => sleep(100, "UNIQUE_VALUE_2");
+		const myFunc3 = () => sleep(100, "UNIQUE_VALUE_3");
+
+		const call1 = instance.add(myFunc1);
+		const call2 = instance.add(myFunc2);
+		const call3 = instance.add(myFunc3);
+
+		await clock?.tickAsync(200);
+
+		assert.strictEqual(await call1, "UNIQUE_VALUE_1");
+		assert.strictEqual(await call2, "UNIQUE_VALUE_2");
+		assert.strictEqual(await call3, "UNIQUE_VALUE_3");
+	});
+
+	test("Minimum 1 parallel queue size", async (): Promise<void> => {
+		const instance = new Queue<void>(-1);
+
+		assert.strictEqual(await instance.add(() => Promise.resolve()), undefined);
+	});
+
+	test("Increase max parallel runs more things", async (): Promise<void> => {
+		const instance = new Queue<undefined>(1);
+
+		const spy1 = Sinon.spy(() => sleep(100, undefined));
+		const spy2 = Sinon.spy(() => sleep(100, undefined));
+		const spy3 = Sinon.spy(() => sleep(100, undefined));
+
+		instance.add(spy1);
+		instance.add(spy2);
+		instance.add(spy3);
+
+		Sinon.assert.calledOnce(spy1);
+		Sinon.assert.notCalled(spy2);
+		Sinon.assert.notCalled(spy3);
+
+		instance.updateParallel(3);
+
+		Sinon.assert.calledOnce(spy2);
+		Sinon.assert.calledOnce(spy3);
+
+		await clock?.runAllAsync();
+	});
+
+	test("Decrease max parallel does not run more things", async (): Promise<void> => {
+		const instance = new Queue<undefined>(2);
+
+		const spy1 = Sinon.spy(() => sleep(100, undefined));
+		const spy2 = Sinon.spy(() => sleep(100, undefined));
+		const spy3 = Sinon.spy(() => sleep(100, undefined));
+		const spy4 = Sinon.spy(() => sleep(100, undefined));
+
+		instance.add(spy1);
+		instance.add(spy2);
+		instance.add(spy3);
+
+		Sinon.assert.calledOnce(spy1);
+		Sinon.assert.calledOnce(spy2);
+		Sinon.assert.notCalled(spy3);
+
+		instance.updateParallel(1);
+		instance.add(spy4);
+
+		await clock?.tickAsync(50);
+
+		Sinon.assert.notCalled(spy3);
+		Sinon.assert.notCalled(spy4);
+
+		await clock?.tickAsync(100);
+
+		Sinon.assert.calledOnce(spy3);
+		Sinon.assert.notCalled(spy4);
+
+		await clock?.runAllAsync();
+	});
+});
diff --git a/test/suite/split.test.ts b/test/suite/split.test.ts
new file mode 100644
index 00000000..3e3578e9
--- /dev/null
+++ b/test/suite/split.test.ts
@@ -0,0 +1,38 @@
+import * as assert from "node:assert";
+import { split } from "../../src/util/split.js";
+
+suite("Split", (): void => {
+	test("Single Space", (): void => {
+		assert.deepStrictEqual(split("single space"), ["single", "space"]);
+	});
+	test("Multiple Spaces", (): void => {
+		assert.deepStrictEqual(split("multiple spaces in this test right here"), [
+			"multiple",
+			"spaces in this test right here",
+		]);
+	});
+	test("No Spaces", (): void => {
+		assert.deepStrictEqual(split("oneword"), ["oneword", ""]);
+	});
+	test("Trim results", (): void => {
+		assert.deepStrictEqual(split("trim    result   "), ["trim", "result"]);
+	});
+	test("Single Ampersand", (): void => {
+		assert.deepStrictEqual(split("single&ampersand", "&"), [
+			"single",
+			"ampersand",
+		]);
+	});
+	test("Short second parameter", (): void => {
+		assert.deepStrictEqual(split("bad second argument", ""), [
+			"bad second argument",
+			"",
+		]);
+	});
+	test("Long second parameter", (): void => {
+		assert.deepStrictEqual(split("bad second argument long", "long"), [
+			"bad second argument ",
+			"ong",
+		]);
+	});
+});
diff --git a/test/suite/textdecorator.test.ts b/test/suite/textdecorator.test.ts
new file mode 100644
index 00000000..be24e005
--- /dev/null
+++ b/test/suite/textdecorator.test.ts
@@ -0,0 +1,526 @@
+import * as assert from "node:assert";
+import { type SinonFakeTimers, stub, useFakeTimers } from "sinon";
+import type { Commit } from "../../src/git/util/stream-parsing.js";
+import { between } from "../../src/util/ago.js";
+import * as property from "../../src/util/property.js";
+import {
+	type InfoTokenNormalizedCommitInfo,
+	type InfoTokens,
+	normalizeCommitInfoTokens,
+	parseTokens,
+} from "../../src/util/text-decorator.js";
+
+suite("Date Calculations", (): void => {
+	test("Time ago in years", (): void => {
+		assert.strictEqual(
+			between(new Date(2015, 2), new Date(2014, 1)),
+			"1 year ago",
+		);
+		assert.strictEqual(
+			between(new Date(2015, 1), new Date(2005, 1)),
+			"10 years ago",
+		);
+	});
+
+	test("Time ago in months", (): void => {
+		assert.strictEqual(
+			between(new Date(2015, 1), new Date(2015, 0)),
+			"1 month ago",
+		);
+		assert.strictEqual(
+			between(new Date(2015, 11, 10), new Date(2015, 0)),
+			"11 months ago",
+		);
+		assert.strictEqual(
+			between(new Date(2015, 1), new Date(2014, 1)),
+			"12 months ago",
+		);
+	});
+
+	test("Time ago in days", (): void => {
+		assert.strictEqual(
+			between(new Date(2015, 1, 2, 8), new Date(2015, 1, 1, 0)),
+			"1 day ago",
+		);
+		assert.strictEqual(
+			between(new Date(2015, 1, 31), new Date(2015, 1, 1)),
+			"30 days ago",
+		);
+	});
+
+	test("Time ago in hours", (): void => {
+		assert.strictEqual(
+			between(new Date(2015, 1, 1, 1, 5, 0), new Date(2015, 1, 1, 0, 0, 0)),
+			"1 hour ago",
+		);
+		assert.strictEqual(
+			between(new Date(2015, 1, 1, 23, 29, 0), new Date(2015, 1, 1, 0, 0, 0)),
+			"23 hours ago",
+		);
+		assert.strictEqual(
+			between(new Date(2015, 1, 2), new Date(2015, 1, 1)),
+			"24 hours ago",
+		);
+	});
+
+	test("Time ago in minutes", (): void => {
+		assert.strictEqual(
+			between(new Date(2015, 1, 1, 1, 5, 0), new Date(2015, 1, 1, 1, 0, 0)),
+			"5 minutes ago",
+		);
+		assert.strictEqual(
+			between(new Date(2015, 1, 1, 1, 59, 29), new Date(2015, 1, 1, 1, 0, 0)),
+			"59 minutes ago",
+		);
+		assert.strictEqual(
+			between(new Date(2015, 1, 1, 1, 0, 0), new Date(2015, 1, 1, 0, 0, 0)),
+			"60 minutes ago",
+		);
+	});
+
+	test("Right now", (): void => {
+		assert.strictEqual(
+			between(new Date(2015, 1, 1, 1, 0, 1), new Date(2015, 1, 1, 1, 0, 0)),
+			"right now",
+		);
+	});
+});
+
+suite("Token Parser", (): void => {
+	const normalizedInfo: InfoTokens = {
+		"example.token": (): string => "example-token",
+		value: (value?: string): string => {
+			if (value) {
+				return `${value}-example`;
+			}
+			return "-example";
+		},
+		"mixed.token": (): string => "mIxeD-ToKeN",
+	};
+
+	test("No token", (): void => {
+		assert.strictEqual(parseTokens("No token", normalizedInfo), "No token");
+	});
+
+	test("Invalid token", (): void => {
+		assert.strictEqual(
+			parseTokens("Invalid ${token}", normalizedInfo),
+			"Invalid token",
+		);
+	});
+
+	test("Simple replace", (): void => {
+		assert.strictEqual(
+			parseTokens("Simple ${example.token}", normalizedInfo),
+			"Simple example-token",
+		);
+	});
+
+	test("Simple replace at the start of string", (): void => {
+		assert.strictEqual(
+			parseTokens("${example.token} simple", normalizedInfo),
+			"example-token simple",
+		);
+	});
+
+	test("Simple replace only token", (): void => {
+		assert.strictEqual(
+			parseTokens("${example.token}", normalizedInfo),
+			"example-token",
+		);
+	});
+
+	test("Value replace", (): void => {
+		assert.strictEqual(
+			parseTokens("Value ${value,some-value}", normalizedInfo),
+			"Value some-value-example",
+		);
+	});
+
+	test("Function without parameter", (): void => {
+		assert.strictEqual(
+			parseTokens("Value ${value}", normalizedInfo),
+			"Value -example",
+		);
+	});
+
+	test("Modifier replace", (): void => {
+		assert.strictEqual(
+			parseTokens("Value ${mixed.token|u}", normalizedInfo),
+			"Value MIXED-TOKEN",
+		);
+		assert.strictEqual(
+			parseTokens("Value ${mixed.token|l}", normalizedInfo),
+			"Value mixed-token",
+		);
+	});
+
+	test("Modifier replace with value", (): void => {
+		test("Modifier replace", (): void => {
+			assert.strictEqual(
+				parseTokens("Value ${value,mIxEd-ToKeN|u}", normalizedInfo),
+				"Value MIXED-TOKEN-EXAMPLE",
+			);
+			assert.strictEqual(
+				parseTokens("Value ${value,mIxEd-ToKeN|l}", normalizedInfo),
+				"Value mixed-token-example",
+			);
+		});
+	});
+
+	test("Invalid modifier", (): void => {
+		assert.strictEqual(
+			parseTokens("Value ${example.token|invalidModifier}", normalizedInfo),
+			"Value example-token|invalidModifier",
+		);
+		assert.strictEqual(
+			parseTokens("Value ${example.token|invalidModifier}", normalizedInfo),
+			"Value example-token|invalidModifier",
+		);
+		assert.strictEqual(
+			parseTokens("Value ${example.token|q}", normalizedInfo),
+			"Value example-token|q",
+		);
+	});
+
+	test("Modifier without token", (): void => {
+		assert.strictEqual(
+			parseTokens("Value ${|mod}", normalizedInfo),
+			"Value |mod",
+		);
+	});
+
+	test("Token in the middle of string", (): void => {
+		assert.strictEqual(
+			parseTokens("Simple ${example.token} in a longer text", normalizedInfo),
+			"Simple example-token in a longer text",
+		);
+	});
+
+	test("Multiple tokens", (): void => {
+		assert.strictEqual(
+			parseTokens(
+				"Multiple ${example.token} in a ${length,longer} text",
+				normalizedInfo,
+			),
+			"Multiple example-token in a length text",
+		);
+	});
+
+	test("Dangling token", (): void => {
+		assert.strictEqual(
+			parseTokens("This token ${is.not.closed", normalizedInfo),
+			"This token ${is.not.closed",
+		);
+		assert.strictEqual(
+			parseTokens("This token ${is.not.closed with spaces", normalizedInfo),
+			"This token ${is.not.closed with spaces",
+		);
+		assert.strictEqual(
+			parseTokens("This token ${is.not.closed,with_params", normalizedInfo),
+			"This token ${is.not.closed,with_params",
+		);
+		assert.strictEqual(
+			parseTokens("This token ${is.not.closed|with_modifier", normalizedInfo),
+			"This token ${is.not.closed|with_modifier",
+		);
+		assert.strictEqual(
+			parseTokens(
+				"This token ${is.not.closed,with_params|and_modifier",
+				normalizedInfo,
+			),
+			"This token ${is.not.closed,with_params|and_modifier",
+		);
+	});
+});
+
+suite("Text Decorator with CommitInfoToken", (): void => {
+	let fakeTimer: SinonFakeTimers | undefined;
+	let normalizedCommitInfoTokens: InfoTokenNormalizedCommitInfo | undefined;
+	suiteSetup(() => {
+		fakeTimer = useFakeTimers({
+			now: 1_621_014_626_000,
+			toFake: ["Date"],
+			shouldAdvanceTime: false,
+		});
+		normalizedCommitInfoTokens = normalizeCommitInfoTokens(exampleCommit);
+	});
+	suiteTeardown(() => {
+		fakeTimer?.restore();
+	});
+	const exampleCommit: Commit = {
+		author: {
+			mail: "<vdavydov.dev@gmail.com>",
+			name: "Vladimir Davydov",
+			isCurrentUser: false,
+			timestamp: "1423781950",
+			date: new Date(1_423_781_950_000),
+			tz: "-0800",
+		},
+		committer: {
+			mail: "<torvalds@linux-foundation.org>",
+			name: "Linus Torvalds",
+			isCurrentUser: false,
+			timestamp: "1423796049",
+			date: new Date(1_423_796_049_000),
+			tz: "-0800",
+		},
+		hash: "60d3fd32a7a9da4c8c93a9f89cfda22a0b4c65ce",
+		summary: "list_lru: introduce per-memcg lists",
+	};
+	const check = (token: string, expect: string): void => {
+		test(`Parse "\${${token}}"`, (): void =>
+			assert.strictEqual(
+				parseTokens(`\${${token}}`, normalizedCommitInfoTokens ?? {}),
+				expect,
+			));
+	};
+
+	check("author.mail", "<vdavydov.dev@gmail.com>");
+	check("author.name", "Vladimir Davydov");
+	check("author.name|u", "VLADIMIR DAVYDOV");
+	check("author.name|l", "vladimir davydov");
+	check("author.tz", "-0800");
+	check("author.date", "2015-02-12");
+
+	check("committer.mail", "<torvalds@linux-foundation.org>");
+	check("committer.name", "Linus Torvalds");
+	check("committer.tz", "-0800");
+	check("committer.date", "2015-02-13");
+
+	check("commit.summary", "list_lru: introduce per-memcg lists");
+	check("commit.hash", "60d3fd32a7a9da4c8c93a9f89cfda22a0b4c65ce");
+	check("commit.hash_short", "60d3fd3");
+
+	check("time.ago", "6 years ago");
+	check("time.c_ago", "6 years ago");
+
+	check("commit.summary,0", "");
+	check("commit.summary,5", "list_");
+	check("commit.summary,5|u", "LIST_");
+	check("commit.hash_short,0", "");
+	check("commit.hash_short,2", "60");
+	check("commit.hash_short,39", "60d3fd32a7a9da4c8c93a9f89cfda22a0b4c65c");
+	check("commit.hash_short,100|u", "60D3FD32A7A9DA4C8C93A9F89CFDA22A0B4C65CE");
+});
+
+suite("issue #119 regressions", () => {
+	let fakeTimer: SinonFakeTimers | undefined;
+	suiteSetup(() => {
+		fakeTimer = useFakeTimers({
+			now: 1_621_014_626_000,
+			toFake: ["Date"],
+			shouldAdvanceTime: false,
+		});
+	});
+	suiteTeardown(() => {
+		fakeTimer?.restore();
+	});
+	test("commit.summary before commit.hash_short", () => {
+		const exampleCommit: Commit = {
+			author: {
+				mail: "<vdavydov.dev@gmail.com>",
+				name: "Vladimir Davydov",
+				isCurrentUser: false,
+				timestamp: "1423781950",
+				date: new Date(1_423_781_950_000),
+				tz: "-0800",
+			},
+			committer: {
+				mail: "<torvalds@linux-foundation.org>",
+				name: "Linus Torvalds",
+				isCurrentUser: false,
+				timestamp: "1423796049",
+				date: new Date(1_423_796_049_000),
+				tz: "-0800",
+			},
+			hash: "60d3fd32a7a9da4c8c93a9f89cfda22a0b4c65ce",
+			summary: "list_lru: introduce per-memcg lists",
+		};
+		const normalizedCommitInfoTokens = normalizeCommitInfoTokens(exampleCommit);
+
+		assert.strictEqual(
+			parseTokens(
+				"${commit.summary} ${commit.hash_short}",
+				normalizedCommitInfoTokens,
+			),
+			"list_lru: introduce per-memcg lists 60d3fd3",
+		);
+	});
+
+	test("commit.summary before shortened commit.hash_short", () => {
+		const exampleCommit: Commit = {
+			author: {
+				mail: "<vdavydov.dev@gmail.com>",
+				name: "Vladimir Davydov",
+				isCurrentUser: false,
+				timestamp: "1423781950",
+				date: new Date(1_423_781_950_000),
+				tz: "-0800",
+			},
+			committer: {
+				mail: "<torvalds@linux-foundation.org>",
+				name: "Linus Torvalds",
+				isCurrentUser: false,
+				timestamp: "1423796049",
+				date: new Date(1_423_796_049_000),
+				tz: "-0800",
+			},
+			hash: "60d3fd32a7a9da4c8c93a9f89cfda22a0b4c65ce",
+			summary: "list_lru: introduce per-memcg lists",
+		};
+		const normalizedCommitInfoTokens = normalizeCommitInfoTokens(exampleCommit);
+
+		assert.strictEqual(
+			parseTokens(
+				"${commit.summary} ${commit.hash_short,7}",
+				normalizedCommitInfoTokens,
+			),
+			"list_lru: introduce per-memcg lists 60d3fd3",
+		);
+	});
+
+	test("commit.summary before shortened commit.hash", () => {
+		const exampleCommit: Commit = {
+			author: {
+				mail: "<vdavydov.dev@gmail.com>",
+				name: "Vladimir Davydov",
+				isCurrentUser: false,
+				timestamp: "1423781950",
+				date: new Date(1_423_781_950_000),
+				tz: "-0800",
+			},
+			committer: {
+				mail: "<torvalds@linux-foundation.org>",
+				name: "Linus Torvalds",
+				isCurrentUser: false,
+				timestamp: "1423796049",
+				date: new Date(1_423_796_049_000),
+				tz: "-0800",
+			},
+			hash: "60d3fd32a7a9da4c8c93a9f89cfda22a0b4c65ce",
+			summary: "list_lru: introduce per-memcg lists",
+		};
+		const normalizedCommitInfoTokens = normalizeCommitInfoTokens(exampleCommit);
+
+		assert.strictEqual(
+			parseTokens(
+				"${commit.summary} ${commit.hash,7}",
+				normalizedCommitInfoTokens,
+			),
+			"list_lru: introduce per-memcg lists 60d3fd3",
+		);
+	});
+
+	test("commit.summary before shortened commit.summary", () => {
+		const exampleCommit: Commit = {
+			author: {
+				mail: "<vdavydov.dev@gmail.com>",
+				name: "Vladimir Davydov",
+				isCurrentUser: false,
+				timestamp: "1423781950",
+				date: new Date(1_423_781_950_000),
+				tz: "-0800",
+			},
+			committer: {
+				mail: "<torvalds@linux-foundation.org>",
+				name: "Linus Torvalds",
+				isCurrentUser: false,
+				timestamp: "1423796049",
+				date: new Date(1_423_796_049_000),
+				tz: "-0800",
+			},
+			hash: "60d3fd32a7a9da4c8c93a9f89cfda22a0b4c65ce",
+			summary: "list_lru: introduce per-memcg lists",
+		};
+		const normalizedCommitInfoTokens = normalizeCommitInfoTokens(exampleCommit);
+
+		assert.strictEqual(
+			parseTokens(
+				"${commit.summary} ${commit.summary,7}",
+				normalizedCommitInfoTokens,
+			),
+			"list_lru: introduce per-memcg lists list_lr",
+		);
+	});
+});
+
+suite("Text Sanitizing", () => {
+	test("removes right-to-left override characters from text", () => {
+		const exampleCommit: Commit = {
+			author: {
+				mail: "<vdavydov.dev@gmail.com>",
+				name: "Vladimir Davydov\u202e",
+				isCurrentUser: false,
+				timestamp: "1423781950",
+				date: new Date(1_423_781_950_000),
+				tz: "-0800",
+			},
+			committer: {
+				mail: "<torvalds@linux-foundation.org>",
+				name: "Linus Torvalds",
+				isCurrentUser: false,
+				timestamp: "1423796049",
+				date: new Date(1_423_796_049_000),
+				tz: "-0800",
+			},
+			hash: "60d3fd32a7a9da4c8c93a9f89cfda22a0b4c65ce",
+			summary: "list_lru: \u202eintroduce per-memcg lists",
+		};
+		const normalizedCommitInfoTokens = normalizeCommitInfoTokens(exampleCommit);
+
+		assert.strictEqual(
+			parseTokens(
+				"Blame ${author.name} (${commit.summary})",
+				normalizedCommitInfoTokens,
+			),
+			"Blame Vladimir Davydov (list_lru: introduce per-memcg lists)",
+		);
+	});
+});
+
+suite("Current User Replace", () => {
+	test("removes right-to-left override characters from text", () => {
+		const propertyStub = stub(property, "getProperty");
+		propertyStub.withArgs("currentUserAlias").returns("Current User Alias");
+
+		const exampleCommit: Commit = {
+			author: {
+				mail: "<vdavydov.dev@gmail.com>",
+				name: "Vladimir Davydov\u202e",
+				isCurrentUser: true,
+				timestamp: "1423781950",
+				date: new Date(1_423_781_950_000),
+				tz: "-0800",
+			},
+			committer: {
+				mail: "<torvalds@linux-foundation.org>",
+				name: "Linus Torvalds",
+				isCurrentUser: false,
+				timestamp: "1423796049",
+				date: new Date(1_423_796_049_000),
+				tz: "-0800",
+			},
+			hash: "60d3fd32a7a9da4c8c93a9f89cfda22a0b4c65ce",
+			summary: "list_lru: \u202eintroduce per-memcg lists",
+		};
+		const normalizedCommitInfoTokens = normalizeCommitInfoTokens(exampleCommit);
+
+		assert.strictEqual(
+			parseTokens(
+				"Blame ${author.name} (${commit.summary})",
+				normalizedCommitInfoTokens,
+			),
+			"Blame Current User Alias (list_lru: introduce per-memcg lists)",
+		);
+		assert.strictEqual(
+			parseTokens(
+				"Blame ${committer.name} (${commit.summary})",
+				normalizedCommitInfoTokens,
+			),
+			"Blame Linus Torvalds (list_lru: introduce per-memcg lists)",
+		);
+
+		propertyStub.restore();
+	});
+});
diff --git a/tsconfig.json b/tsconfig.json
index 69905df0..b5ebd8ee 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,12 +1,18 @@
 {
-	"compilerOptions": {
-		"module": "commonjs",
-		"target": "ES5",
-		"outDir": "out",
-		"noLib": true,
-		"sourceMap": true
-	},
-	"exclude": [
-		"node_modules"
-	]
+    "compilerOptions": {
+        "module": "ES2022",
+        "moduleResolution": "node",
+        "target": "ES2022",
+        "lib": [
+            "ES2020",
+            "ES2022",
+            "ESNext.Intl"
+        ],
+        "strict": true,
+        "outDir": "./out/src/",
+        "rootDir": "."
+    },
+    "exclude": [
+        "node_modules"
+    ]
 }
\ No newline at end of file
diff --git a/tsconfig.test.json b/tsconfig.test.json
new file mode 100644
index 00000000..b0c5135e
--- /dev/null
+++ b/tsconfig.test.json
@@ -0,0 +1,23 @@
+{
+    "compilerOptions": {
+        "module": "CommonJS",
+        "moduleResolution": "node",
+        "target": "es2020",
+        "strict": true,
+        "outDir": "./out",
+        "lib": [
+            "ES2020",
+            "ESNext.Intl"
+        ],
+        "sourceMap": true,
+        "experimentalDecorators": true,
+        "emitDecoratorMetadata": true
+    },
+    "include": [
+        "src/index.ts",
+        "test/**/*"
+    ],
+    "exclude": [
+        "node_modules"
+    ]
+}
\ No newline at end of file
diff --git a/types/git.d.ts b/types/git.d.ts
new file mode 100644
index 00000000..afd2a4b3
--- /dev/null
+++ b/types/git.d.ts
@@ -0,0 +1,411 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { Uri, Event, Disposable, ProviderResult, Command, CancellationToken } from 'vscode';
+export { ProviderResult } from 'vscode';
+
+export interface Git {
+	readonly path: string;
+}
+
+export interface InputBox {
+	value: string;
+}
+
+export const enum ForcePushMode {
+	Force,
+	ForceWithLease,
+	ForceWithLeaseIfIncludes,
+}
+
+export const enum RefType {
+	Head,
+	RemoteHead,
+	Tag
+}
+
+export interface Ref {
+	readonly type: RefType;
+	readonly name?: string;
+	readonly commit?: string;
+	readonly remote?: string;
+}
+
+export interface UpstreamRef {
+	readonly remote: string;
+	readonly name: string;
+	readonly commit?: string;
+}
+
+export interface Branch extends Ref {
+	readonly upstream?: UpstreamRef;
+	readonly ahead?: number;
+	readonly behind?: number;
+}
+
+export interface CommitShortStat {
+	readonly files: number;
+	readonly insertions: number;
+	readonly deletions: number;
+}
+
+export interface Commit {
+	readonly hash: string;
+	readonly message: string;
+	readonly parents: string[];
+	readonly authorDate?: Date;
+	readonly authorName?: string;
+	readonly authorEmail?: string;
+	readonly commitDate?: Date;
+	readonly shortStat?: CommitShortStat;
+}
+
+export interface Submodule {
+	readonly name: string;
+	readonly path: string;
+	readonly url: string;
+}
+
+export interface Remote {
+	readonly name: string;
+	readonly fetchUrl?: string;
+	readonly pushUrl?: string;
+	readonly isReadOnly: boolean;
+}
+
+export const enum Status {
+	INDEX_MODIFIED,
+	INDEX_ADDED,
+	INDEX_DELETED,
+	INDEX_RENAMED,
+	INDEX_COPIED,
+
+	MODIFIED,
+	DELETED,
+	UNTRACKED,
+	IGNORED,
+	INTENT_TO_ADD,
+	INTENT_TO_RENAME,
+	TYPE_CHANGED,
+
+	ADDED_BY_US,
+	ADDED_BY_THEM,
+	DELETED_BY_US,
+	DELETED_BY_THEM,
+	BOTH_ADDED,
+	BOTH_DELETED,
+	BOTH_MODIFIED
+}
+
+export interface Change {
+
+	/**
+	 * Returns either `originalUri` or `renameUri`, depending
+	 * on whether this change is a rename change. When
+	 * in doubt always use `uri` over the other two alternatives.
+	 */
+	readonly uri: Uri;
+	readonly originalUri: Uri;
+	readonly renameUri: Uri | undefined;
+	readonly status: Status;
+}
+
+export interface RepositoryState {
+	readonly HEAD: Branch | undefined;
+	readonly refs: Ref[];
+	readonly remotes: Remote[];
+	readonly submodules: Submodule[];
+	readonly rebaseCommit: Commit | undefined;
+
+	readonly mergeChanges: Change[];
+	readonly indexChanges: Change[];
+	readonly workingTreeChanges: Change[];
+	readonly untrackedChanges: Change[];
+
+	readonly onDidChange: Event<void>;
+}
+
+export interface RepositoryUIState {
+	readonly selected: boolean;
+	readonly onDidChange: Event<void>;
+}
+
+/**
+ * Log options.
+ */
+export interface LogOptions {
+	/** Max number of log entries to retrieve. If not specified, the default is 32. */
+	readonly maxEntries?: number;
+	readonly path?: string;
+	/** A commit range, such as "0a47c67f0fb52dd11562af48658bc1dff1d75a38..0bb4bdea78e1db44d728fd6894720071e303304f" */
+	readonly range?: string;
+	readonly reverse?: boolean;
+	readonly sortByAuthorDate?: boolean;
+	readonly shortStats?: boolean;
+	readonly author?: string;
+	readonly refNames?: string[];
+}
+
+export interface CommitOptions {
+	all?: boolean | 'tracked';
+	amend?: boolean;
+	signoff?: boolean;
+	signCommit?: boolean;
+	empty?: boolean;
+	noVerify?: boolean;
+	requireUserConfig?: boolean;
+	useEditor?: boolean;
+	verbose?: boolean;
+	/**
+	 * string    - execute the specified command after the commit operation
+	 * undefined - execute the command specified in git.postCommitCommand
+	 *             after the commit operation
+	 * null      - do not execute any command after the commit operation
+	 */
+	postCommitCommand?: string | null;
+}
+
+export interface FetchOptions {
+	remote?: string;
+	ref?: string;
+	all?: boolean;
+	prune?: boolean;
+	depth?: number;
+}
+
+export interface InitOptions {
+	defaultBranch?: string;
+}
+
+export interface RefQuery {
+	readonly contains?: string;
+	readonly count?: number;
+	readonly pattern?: string;
+	readonly sort?: 'alphabetically' | 'committerdate';
+}
+
+export interface BranchQuery extends RefQuery {
+	readonly remote?: boolean;
+}
+
+export interface Repository {
+
+	readonly rootUri: Uri;
+	readonly inputBox: InputBox;
+	readonly state: RepositoryState;
+	readonly ui: RepositoryUIState;
+
+	readonly onDidCommit: Event<void>;
+
+	getConfigs(): Promise<{ key: string; value: string; }[]>;
+	getConfig(key: string): Promise<string>;
+	setConfig(key: string, value: string): Promise<string>;
+	getGlobalConfig(key: string): Promise<string>;
+
+	getObjectDetails(treeish: string, path: string): Promise<{ mode: string, object: string, size: number }>;
+	detectObjectType(object: string): Promise<{ mimetype: string, encoding?: string }>;
+	buffer(ref: string, path: string): Promise<Buffer>;
+	show(ref: string, path: string): Promise<string>;
+	getCommit(ref: string): Promise<Commit>;
+
+	add(paths: string[]): Promise<void>;
+	revert(paths: string[]): Promise<void>;
+	clean(paths: string[]): Promise<void>;
+
+	apply(patch: string, reverse?: boolean): Promise<void>;
+	diff(cached?: boolean): Promise<string>;
+	diffWithHEAD(): Promise<Change[]>;
+	diffWithHEAD(path: string): Promise<string>;
+	diffWith(ref: string): Promise<Change[]>;
+	diffWith(ref: string, path: string): Promise<string>;
+	diffIndexWithHEAD(): Promise<Change[]>;
+	diffIndexWithHEAD(path: string): Promise<string>;
+	diffIndexWith(ref: string): Promise<Change[]>;
+	diffIndexWith(ref: string, path: string): Promise<string>;
+	diffBlobs(object1: string, object2: string): Promise<string>;
+	diffBetween(ref1: string, ref2: string): Promise<Change[]>;
+	diffBetween(ref1: string, ref2: string, path: string): Promise<string>;
+
+	hashObject(data: string): Promise<string>;
+
+	createBranch(name: string, checkout: boolean, ref?: string): Promise<void>;
+	deleteBranch(name: string, force?: boolean): Promise<void>;
+	getBranch(name: string): Promise<Branch>;
+	getBranches(query: BranchQuery, cancellationToken?: CancellationToken): Promise<Ref[]>;
+	getBranchBase(name: string): Promise<Branch | undefined>;
+	setBranchUpstream(name: string, upstream: string): Promise<void>;
+
+	checkIgnore(paths: string[]): Promise<Set<string>>;
+
+	getRefs(query: RefQuery, cancellationToken?: CancellationToken): Promise<Ref[]>;
+
+	getMergeBase(ref1: string, ref2: string): Promise<string | undefined>;
+
+	tag(name: string, upstream: string): Promise<void>;
+	deleteTag(name: string): Promise<void>;
+
+	status(): Promise<void>;
+	checkout(treeish: string): Promise<void>;
+
+	addRemote(name: string, url: string): Promise<void>;
+	removeRemote(name: string): Promise<void>;
+	renameRemote(name: string, newName: string): Promise<void>;
+
+	fetch(options?: FetchOptions): Promise<void>;
+	fetch(remote?: string, ref?: string, depth?: number): Promise<void>;
+	pull(unshallow?: boolean): Promise<void>;
+	push(remoteName?: string, branchName?: string, setUpstream?: boolean, force?: ForcePushMode): Promise<void>;
+
+	blame(path: string): Promise<string>;
+	log(options?: LogOptions): Promise<Commit[]>;
+
+	commit(message: string, opts?: CommitOptions): Promise<void>;
+	merge(ref: string): Promise<void>;
+	mergeAbort(): Promise<void>;
+}
+
+export interface RemoteSource {
+	readonly name: string;
+	readonly description?: string;
+	readonly url: string | string[];
+}
+
+export interface RemoteSourceProvider {
+	readonly name: string;
+	readonly icon?: string; // codicon name
+	readonly supportsQuery?: boolean;
+	getRemoteSources(query?: string): ProviderResult<RemoteSource[]>;
+	getBranches?(url: string): ProviderResult<string[]>;
+	publishRepository?(repository: Repository): Promise<void>;
+}
+
+export interface RemoteSourcePublisher {
+	readonly name: string;
+	readonly icon?: string; // codicon name
+	publishRepository(repository: Repository): Promise<void>;
+}
+
+export interface Credentials {
+	readonly username: string;
+	readonly password: string;
+}
+
+export interface CredentialsProvider {
+	getCredentials(host: Uri): ProviderResult<Credentials>;
+}
+
+export interface PostCommitCommandsProvider {
+	getCommands(repository: Repository): Command[];
+}
+
+export interface PushErrorHandler {
+	handlePushError(repository: Repository, remote: Remote, refspec: string, error: Error & { gitErrorCode: GitErrorCodes }): Promise<boolean>;
+}
+
+export interface BranchProtection {
+	readonly remote: string;
+	readonly rules: BranchProtectionRule[];
+}
+
+export interface BranchProtectionRule {
+	readonly include?: string[];
+	readonly exclude?: string[];
+}
+
+export interface BranchProtectionProvider {
+	onDidChangeBranchProtection: Event<Uri>;
+	provideBranchProtection(): BranchProtection[];
+}
+
+export type APIState = 'uninitialized' | 'initialized';
+
+export interface PublishEvent {
+	repository: Repository;
+	branch?: string;
+}
+
+export interface API {
+	readonly state: APIState;
+	readonly onDidChangeState: Event<APIState>;
+	readonly onDidPublish: Event<PublishEvent>;
+	readonly git: Git;
+	readonly repositories: Repository[];
+	readonly onDidOpenRepository: Event<Repository>;
+	readonly onDidCloseRepository: Event<Repository>;
+
+	toGitUri(uri: Uri, ref: string): Uri;
+	getRepository(uri: Uri): Repository | null;
+	init(root: Uri, options?: InitOptions): Promise<Repository | null>;
+	openRepository(root: Uri): Promise<Repository | null>
+
+	registerRemoteSourcePublisher(publisher: RemoteSourcePublisher): Disposable;
+	registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable;
+	registerCredentialsProvider(provider: CredentialsProvider): Disposable;
+	registerPostCommitCommandsProvider(provider: PostCommitCommandsProvider): Disposable;
+	registerPushErrorHandler(handler: PushErrorHandler): Disposable;
+	registerBranchProtectionProvider(root: Uri, provider: BranchProtectionProvider): Disposable;
+}
+
+export interface GitExtension {
+
+	readonly enabled: boolean;
+	readonly onDidChangeEnablement: Event<boolean>;
+
+	/**
+	 * Returns a specific API version.
+	 *
+	 * Throws error if git extension is disabled. You can listen to the
+	 * [GitExtension.onDidChangeEnablement](#GitExtension.onDidChangeEnablement) event
+	 * to know when the extension becomes enabled/disabled.
+	 *
+	 * @param version Version number.
+	 * @returns API instance
+	 */
+	getAPI(version: 1): API;
+}
+
+export const enum GitErrorCodes {
+	BadConfigFile = 'BadConfigFile',
+	AuthenticationFailed = 'AuthenticationFailed',
+	NoUserNameConfigured = 'NoUserNameConfigured',
+	NoUserEmailConfigured = 'NoUserEmailConfigured',
+	NoRemoteRepositorySpecified = 'NoRemoteRepositorySpecified',
+	NotAGitRepository = 'NotAGitRepository',
+	NotAtRepositoryRoot = 'NotAtRepositoryRoot',
+	Conflict = 'Conflict',
+	StashConflict = 'StashConflict',
+	UnmergedChanges = 'UnmergedChanges',
+	PushRejected = 'PushRejected',
+	ForcePushWithLeaseRejected = 'ForcePushWithLeaseRejected',
+	ForcePushWithLeaseIfIncludesRejected = 'ForcePushWithLeaseIfIncludesRejected',
+	RemoteConnectionError = 'RemoteConnectionError',
+	DirtyWorkTree = 'DirtyWorkTree',
+	CantOpenResource = 'CantOpenResource',
+	GitNotFound = 'GitNotFound',
+	CantCreatePipe = 'CantCreatePipe',
+	PermissionDenied = 'PermissionDenied',
+	CantAccessRemote = 'CantAccessRemote',
+	RepositoryNotFound = 'RepositoryNotFound',
+	RepositoryIsLocked = 'RepositoryIsLocked',
+	BranchNotFullyMerged = 'BranchNotFullyMerged',
+	NoRemoteReference = 'NoRemoteReference',
+	InvalidBranchName = 'InvalidBranchName',
+	BranchAlreadyExists = 'BranchAlreadyExists',
+	NoLocalChanges = 'NoLocalChanges',
+	NoStashFound = 'NoStashFound',
+	LocalChangesOverwritten = 'LocalChangesOverwritten',
+	NoUpstreamBranch = 'NoUpstreamBranch',
+	IsInSubmodule = 'IsInSubmodule',
+	WrongCase = 'WrongCase',
+	CantLockRef = 'CantLockRef',
+	CantRebaseMultipleBranches = 'CantRebaseMultipleBranches',
+	PatchDoesNotApply = 'PatchDoesNotApply',
+	NoPathFound = 'NoPathFound',
+	UnknownPath = 'UnknownPath',
+	EmptyCommitMessage = 'EmptyCommitMessage',
+	BranchFastForwardRejected = 'BranchFastForwardRejected',
+	BranchNotYetBorn = 'BranchNotYetBorn',
+	TagConflict = 'TagConflict'
+}
\ No newline at end of file
diff --git a/typings/collections.d.ts b/typings/collections.d.ts
deleted file mode 100644
index d25960a4..00000000
--- a/typings/collections.d.ts
+++ /dev/null
@@ -1,1386 +0,0 @@
-/**
- * @namespace Top level namespace for collections, a TypeScript data structure library.
- */
-declare module collections {
-    /**
-    * Function signature for comparing
-    * <0 means a is smaller
-    * = 0 means they are equal
-    * >0 means a is larger
-    */
-    interface ICompareFunction<T> {
-        (a: T, b: T): number;
-    }
-    /**
-    * Function signature for checking equality
-    */
-    interface IEqualsFunction<T> {
-        (a: T, b: T): boolean;
-    }
-    /**
-    * Function signature for Iterations. Return false to break from loop
-    */
-    interface ILoopFunction<T> {
-        (a: T): boolean | void;
-    }
-    /**
-     * Default function to compare element order.
-     * @function
-     */
-    function defaultCompare<T>(a: T, b: T): number;
-    /**
-     * Default function to test equality.
-     * @function
-     */
-    function defaultEquals<T>(a: T, b: T): boolean;
-    /**
-     * Default function to convert an object to a string.
-     * @function
-     */
-    function defaultToString(item: any): string;
-    /**
-    * Joins all the properies of the object using the provided join string
-    */
-    function makeString<T>(item: T, join?: string): string;
-    /**
-     * Checks if the given argument is a function.
-     * @function
-     */
-    function isFunction(func: any): boolean;
-    /**
-     * Checks if the given argument is undefined.
-     * @function
-     */
-    function isUndefined(obj: any): boolean;
-    /**
-     * Checks if the given argument is a string.
-     * @function
-     */
-    function isString(obj: any): boolean;
-    /**
-     * Reverses a compare function.
-     * @function
-     */
-    function reverseCompareFunction<T>(compareFunction: ICompareFunction<T>): ICompareFunction<T>;
-    /**
-     * Returns an equal function given a compare function.
-     * @function
-     */
-    function compareToEquals<T>(compareFunction: ICompareFunction<T>): IEqualsFunction<T>;
-    /**
-     * @namespace Contains various functions for manipulating arrays.
-     */
-    module arrays {
-        /**
-         * Returns the position of the first occurrence of the specified item
-         * within the specified array.
-         * @param {*} array the array in which to search the element.
-         * @param {Object} item the element to search.
-         * @param {function(Object,Object):boolean=} equalsFunction optional function used to
-         * check equality between 2 elements.
-         * @return {number} the position of the first occurrence of the specified element
-         * within the specified array, or -1 if not found.
-         */
-        function indexOf<T>(array: T[], item: T, equalsFunction?: collections.IEqualsFunction<T>): number;
-        /**
-         * Returns the position of the last occurrence of the specified element
-         * within the specified array.
-         * @param {*} array the array in which to search the element.
-         * @param {Object} item the element to search.
-         * @param {function(Object,Object):boolean=} equalsFunction optional function used to
-         * check equality between 2 elements.
-         * @return {number} the position of the last occurrence of the specified element
-         * within the specified array or -1 if not found.
-         */
-        function lastIndexOf<T>(array: T[], item: T, equalsFunction?: collections.IEqualsFunction<T>): number;
-        /**
-         * Returns true if the specified array contains the specified element.
-         * @param {*} array the array in which to search the element.
-         * @param {Object} item the element to search.
-         * @param {function(Object,Object):boolean=} equalsFunction optional function to
-         * check equality between 2 elements.
-         * @return {boolean} true if the specified array contains the specified element.
-         */
-        function contains<T>(array: T[], item: T, equalsFunction?: collections.IEqualsFunction<T>): boolean;
-        /**
-         * Removes the first ocurrence of the specified element from the specified array.
-         * @param {*} array the array in which to search element.
-         * @param {Object} item the element to search.
-         * @param {function(Object,Object):boolean=} equalsFunction optional function to
-         * check equality between 2 elements.
-         * @return {boolean} true if the array changed after this call.
-         */
-        function remove<T>(array: T[], item: T, equalsFunction?: collections.IEqualsFunction<T>): boolean;
-        /**
-         * Returns the number of elements in the specified array equal
-         * to the specified object.
-         * @param {Array} array the array in which to determine the frequency of the element.
-         * @param {Object} item the element whose frequency is to be determined.
-         * @param {function(Object,Object):boolean=} equalsFunction optional function used to
-         * check equality between 2 elements.
-         * @return {number} the number of elements in the specified array
-         * equal to the specified object.
-         */
-        function frequency<T>(array: T[], item: T, equalsFunction?: collections.IEqualsFunction<T>): number;
-        /**
-         * Returns true if the two specified arrays are equal to one another.
-         * Two arrays are considered equal if both arrays contain the same number
-         * of elements, and all corresponding pairs of elements in the two
-         * arrays are equal and are in the same order.
-         * @param {Array} array1 one array to be tested for equality.
-         * @param {Array} array2 the other array to be tested for equality.
-         * @param {function(Object,Object):boolean=} equalsFunction optional function used to
-         * check equality between elemements in the arrays.
-         * @return {boolean} true if the two arrays are equal
-         */
-        function equals<T>(array1: T[], array2: T[], equalsFunction?: collections.IEqualsFunction<T>): boolean;
-        /**
-         * Returns shallow a copy of the specified array.
-         * @param {*} array the array to copy.
-         * @return {Array} a copy of the specified array
-         */
-        function copy<T>(array: T[]): T[];
-        /**
-         * Swaps the elements at the specified positions in the specified array.
-         * @param {Array} array The array in which to swap elements.
-         * @param {number} i the index of one element to be swapped.
-         * @param {number} j the index of the other element to be swapped.
-         * @return {boolean} true if the array is defined and the indexes are valid.
-         */
-        function swap<T>(array: T[], i: number, j: number): boolean;
-        function toString<T>(array: T[]): string;
-        /**
-         * Executes the provided function once for each element present in this array
-         * starting from index 0 to length - 1.
-         * @param {Array} array The array in which to iterate.
-         * @param {function(Object):*} callback function to execute, it is
-         * invoked with one argument: the element value, to break the iteration you can
-         * optionally return false.
-         */
-        function forEach<T>(array: T[], callback: ILoopFunction<T>): void;
-    }
-    interface ILinkedListNode<T> {
-        element: T;
-        next: ILinkedListNode<T>;
-    }
-    class LinkedList<T> {
-        /**
-        * First node in the list
-        * @type {Object}
-        * @private
-        */
-        firstNode: ILinkedListNode<T>;
-        /**
-        * Last node in the list
-        * @type {Object}
-        * @private
-        */
-        private lastNode;
-        /**
-        * Number of elements in the list
-        * @type {number}
-        * @private
-        */
-        private nElements;
-        /**
-        * Creates an empty Linked List.
-        * @class A linked list is a data structure consisting of a group of nodes
-        * which together represent a sequence.
-        * @constructor
-        */
-        constructor();
-        /**
-        * Adds an element to this list.
-        * @param {Object} item element to be added.
-        * @param {number=} index optional index to add the element. If no index is specified
-        * the element is added to the end of this list.
-        * @return {boolean} true if the element was added or false if the index is invalid
-        * or if the element is undefined.
-        */
-        add(item: T, index?: number): boolean;
-        /**
-        * Returns the first element in this list.
-        * @return {*} the first element of the list or undefined if the list is
-        * empty.
-        */
-        first(): T;
-        /**
-        * Returns the last element in this list.
-        * @return {*} the last element in the list or undefined if the list is
-        * empty.
-        */
-        last(): T;
-        /**
-         * Returns the element at the specified position in this list.
-         * @param {number} index desired index.
-         * @return {*} the element at the given index or undefined if the index is
-         * out of bounds.
-         */
-        elementAtIndex(index: number): T;
-        /**
-         * Returns the index in this list of the first occurrence of the
-         * specified element, or -1 if the List does not contain this element.
-         * <p>If the elements inside this list are
-         * not comparable with the === operator a custom equals function should be
-         * provided to perform searches, the function must receive two arguments and
-         * return true if they are equal, false otherwise. Example:</p>
-         *
-         * <pre>
-         * var petsAreEqualByName = function(pet1, pet2) {
-         *  return pet1.name === pet2.name;
-         * }
-         * </pre>
-         * @param {Object} item element to search for.
-         * @param {function(Object,Object):boolean=} equalsFunction Optional
-         * function used to check if two elements are equal.
-         * @return {number} the index in this list of the first occurrence
-         * of the specified element, or -1 if this list does not contain the
-         * element.
-         */
-        indexOf(item: T, equalsFunction?: IEqualsFunction<T>): number;
-        /**
-           * Returns true if this list contains the specified element.
-           * <p>If the elements inside the list are
-           * not comparable with the === operator a custom equals function should be
-           * provided to perform searches, the function must receive two arguments and
-           * return true if they are equal, false otherwise. Example:</p>
-           *
-           * <pre>
-           * var petsAreEqualByName = function(pet1, pet2) {
-           *  return pet1.name === pet2.name;
-           * }
-           * </pre>
-           * @param {Object} item element to search for.
-           * @param {function(Object,Object):boolean=} equalsFunction Optional
-           * function used to check if two elements are equal.
-           * @return {boolean} true if this list contains the specified element, false
-           * otherwise.
-           */
-        contains(item: T, equalsFunction?: IEqualsFunction<T>): boolean;
-        /**
-         * Removes the first occurrence of the specified element in this list.
-         * <p>If the elements inside the list are
-         * not comparable with the === operator a custom equals function should be
-         * provided to perform searches, the function must receive two arguments and
-         * return true if they are equal, false otherwise. Example:</p>
-         *
-         * <pre>
-         * var petsAreEqualByName = function(pet1, pet2) {
-         *  return pet1.name === pet2.name;
-         * }
-         * </pre>
-         * @param {Object} item element to be removed from this list, if present.
-         * @return {boolean} true if the list contained the specified element.
-         */
-        remove(item: T, equalsFunction?: IEqualsFunction<T>): boolean;
-        /**
-         * Removes all of the elements from this list.
-         */
-        clear(): void;
-        /**
-         * Returns true if this list is equal to the given list.
-         * Two lists are equal if they have the same elements in the same order.
-         * @param {LinkedList} other the other list.
-         * @param {function(Object,Object):boolean=} equalsFunction optional
-         * function used to check if two elements are equal. If the elements in the lists
-         * are custom objects you should provide a function, otherwise
-         * the === operator is used to check equality between elements.
-         * @return {boolean} true if this list is equal to the given list.
-         */
-        equals(other: LinkedList<T>, equalsFunction?: IEqualsFunction<T>): boolean;
-        /**
-        * @private
-        */
-        private equalsAux(n1, n2, eqF);
-        /**
-         * Removes the element at the specified position in this list.
-         * @param {number} index given index.
-         * @return {*} removed element or undefined if the index is out of bounds.
-         */
-        removeElementAtIndex(index: number): T;
-        /**
-         * Executes the provided function once for each element present in this list in order.
-         * @param {function(Object):*} callback function to execute, it is
-         * invoked with one argument: the element value, to break the iteration you can
-         * optionally return false.
-         */
-        forEach(callback: ILoopFunction<T>): void;
-        /**
-         * Reverses the order of the elements in this linked list (makes the last
-         * element first, and the first element last).
-         */
-        reverse(): void;
-        /**
-         * Returns an array containing all of the elements in this list in proper
-         * sequence.
-         * @return {Array.<*>} an array containing all of the elements in this list,
-         * in proper sequence.
-         */
-        toArray(): T[];
-        /**
-         * Returns the number of elements in this list.
-         * @return {number} the number of elements in this list.
-         */
-        size(): number;
-        /**
-         * Returns true if this list contains no elements.
-         * @return {boolean} true if this list contains no elements.
-         */
-        isEmpty(): boolean;
-        toString(): string;
-        /**
-         * @private
-         */
-        private nodeAtIndex(index);
-        /**
-         * @private
-         */
-        private createNode(item);
-    }
-    interface IDictionaryPair<K, V> {
-        key: K;
-        value: V;
-    }
-    class Dictionary<K, V> {
-        /**
-         * Object holding the key-value pairs.
-         * @type {Object}
-         * @private
-         */
-        protected table: {
-            [key: string]: IDictionaryPair<K, V>;
-        };
-        /**
-         * Number of elements in the list.
-         * @type {number}
-         * @private
-         */
-        protected nElements: number;
-        /**
-         * Function used to convert keys to strings.
-         * @type {function(Object):string}
-         * @protected
-         */
-        protected toStr: (key: K) => string;
-        /**
-         * Creates an empty dictionary.
-         * @class <p>Dictionaries map keys to values; each key can map to at most one value.
-         * This implementation accepts any kind of objects as keys.</p>
-         *
-         * <p>If the keys are custom objects a function which converts keys to unique
-         * strings must be provided. Example:</p>
-         * <pre>
-         * function petToString(pet) {
-         *  return pet.name;
-         * }
-         * </pre>
-         * @constructor
-         * @param {function(Object):string=} toStrFunction optional function used
-         * to convert keys to strings. If the keys aren't strings or if toString()
-         * is not appropriate, a custom function which receives a key and returns a
-         * unique string must be provided.
-         */
-        constructor(toStrFunction?: (key: K) => string);
-        /**
-         * Returns the value to which this dictionary maps the specified key.
-         * Returns undefined if this dictionary contains no mapping for this key.
-         * @param {Object} key key whose associated value is to be returned.
-         * @return {*} the value to which this dictionary maps the specified key or
-         * undefined if the map contains no mapping for this key.
-         */
-        getValue(key: K): V;
-        /**
-         * Associates the specified value with the specified key in this dictionary.
-         * If the dictionary previously contained a mapping for this key, the old
-         * value is replaced by the specified value.
-         * @param {Object} key key with which the specified value is to be
-         * associated.
-         * @param {Object} value value to be associated with the specified key.
-         * @return {*} previous value associated with the specified key, or undefined if
-         * there was no mapping for the key or if the key/value are undefined.
-         */
-        setValue(key: K, value: V): V;
-        /**
-         * Removes the mapping for this key from this dictionary if it is present.
-         * @param {Object} key key whose mapping is to be removed from the
-         * dictionary.
-         * @return {*} previous value associated with specified key, or undefined if
-         * there was no mapping for key.
-         */
-        remove(key: K): V;
-        /**
-         * Returns an array containing all of the keys in this dictionary.
-         * @return {Array} an array containing all of the keys in this dictionary.
-         */
-        keys(): K[];
-        /**
-         * Returns an array containing all of the values in this dictionary.
-         * @return {Array} an array containing all of the values in this dictionary.
-         */
-        values(): V[];
-        /**
-        * Executes the provided function once for each key-value pair
-        * present in this dictionary.
-        * @param {function(Object,Object):*} callback function to execute, it is
-        * invoked with two arguments: key and value. To break the iteration you can
-        * optionally return false.
-        */
-        forEach(callback: (key: K, value: V) => any): void;
-        /**
-         * Returns true if this dictionary contains a mapping for the specified key.
-         * @param {Object} key key whose presence in this dictionary is to be
-         * tested.
-         * @return {boolean} true if this dictionary contains a mapping for the
-         * specified key.
-         */
-        containsKey(key: K): boolean;
-        /**
-        * Removes all mappings from this dictionary.
-        * @this {collections.Dictionary}
-        */
-        clear(): void;
-        /**
-         * Returns the number of keys in this dictionary.
-         * @return {number} the number of key-value mappings in this dictionary.
-         */
-        size(): number;
-        /**
-         * Returns true if this dictionary contains no mappings.
-         * @return {boolean} true if this dictionary contains no mappings.
-         */
-        isEmpty(): boolean;
-        toString(): string;
-    }
-    class LinkedDictionary<K, V> extends Dictionary<K, V> {
-        private head;
-        private tail;
-        constructor(toStrFunction?: (key: K) => string);
-        /**
-         * Inserts the new node to the 'tail' of the list, updating the
-         * neighbors, and moving 'this.tail' (the End of List indicator) that
-         * to the end.
-         */
-        private appendToTail(entry);
-        /**
-         * Retrieves a linked dictionary from the table internally
-         */
-        private getLinkedDictionaryPair(key);
-        /**
-         * Returns the value to which this dictionary maps the specified key.
-         * Returns undefined if this dictionary contains no mapping for this key.
-         * @param {Object} key key whose associated value is to be returned.
-         * @return {*} the value to which this dictionary maps the specified key or
-         * undefined if the map contains no mapping for this key.
-         */
-        getValue(key: K): V;
-        /**
-         * Removes the mapping for this key from this dictionary if it is present.
-         * Also, if a value is present for this key, the entry is removed from the
-         * insertion ordering.
-         * @param {Object} key key whose mapping is to be removed from the
-         * dictionary.
-         * @return {*} previous value associated with specified key, or undefined if
-         * there was no mapping for key.
-         */
-        remove(key: K): V;
-        /**
-        * Removes all mappings from this LinkedDictionary.
-        * @this {collections.LinkedDictionary}
-        */
-        clear(): void;
-        /**
-         * Internal function used when updating an existing KeyValue pair.
-         * It places the new value indexed by key into the table, but maintains
-         * its place in the linked ordering.
-         */
-        private replace(oldPair, newPair);
-        /**
-         * Associates the specified value with the specified key in this dictionary.
-         * If the dictionary previously contained a mapping for this key, the old
-         * value is replaced by the specified value.
-         * Updating of a key that already exists maintains its place in the
-         * insertion order into the map.
-         * @param {Object} key key with which the specified value is to be
-         * associated.
-         * @param {Object} value value to be associated with the specified key.
-         * @return {*} previous value associated with the specified key, or undefined if
-         * there was no mapping for the key or if the key/value are undefined.
-         */
-        setValue(key: K, value: V): V;
-        /**
-         * Returns an array containing all of the keys in this LinkedDictionary, ordered
-         * by insertion order.
-         * @return {Array} an array containing all of the keys in this LinkedDictionary,
-         * ordered by insertion order.
-         */
-        keys(): K[];
-        /**
-         * Returns an array containing all of the values in this LinkedDictionary, ordered by
-         * insertion order.
-         * @return {Array} an array containing all of the values in this LinkedDictionary,
-         * ordered by insertion order.
-         */
-        values(): V[];
-        /**
-        * Executes the provided function once for each key-value pair
-        * present in this LinkedDictionary. It is done in the order of insertion
-        * into the LinkedDictionary
-        * @param {function(Object,Object):*} callback function to execute, it is
-        * invoked with two arguments: key and value. To break the iteration you can
-        * optionally return false.
-        */
-        forEach(callback: (key: K, value: V) => any): void;
-    }
-    class MultiDictionary<K, V> {
-        private dict;
-        private equalsF;
-        private allowDuplicate;
-        /**
-         * Creates an empty multi dictionary.
-         * @class <p>A multi dictionary is a special kind of dictionary that holds
-         * multiple values against each key. Setting a value into the dictionary will
-         * add the value to an array at that key. Getting a key will return an array,
-         * holding all the values set to that key.
-         * You can configure to allow duplicates in the values.
-         * This implementation accepts any kind of objects as keys.</p>
-         *
-         * <p>If the keys are custom objects a function which converts keys to strings must be
-         * provided. Example:</p>
-         *
-         * <pre>
-         * function petToString(pet) {
-           *  return pet.name;
-           * }
-         * </pre>
-         * <p>If the values are custom objects a function to check equality between values
-         * must be provided. Example:</p>
-         *
-         * <pre>
-         * function petsAreEqualByAge(pet1,pet2) {
-           *  return pet1.age===pet2.age;
-           * }
-         * </pre>
-         * @constructor
-         * @param {function(Object):string=} toStrFunction optional function
-         * to convert keys to strings. If the keys aren't strings or if toString()
-         * is not appropriate, a custom function which receives a key and returns a
-         * unique string must be provided.
-         * @param {function(Object,Object):boolean=} valuesEqualsFunction optional
-         * function to check if two values are equal.
-         *
-         * @param allowDuplicateValues
-         */
-        constructor(toStrFunction?: (key: K) => string, valuesEqualsFunction?: IEqualsFunction<V>, allowDuplicateValues?: boolean);
-        /**
-        * Returns an array holding the values to which this dictionary maps
-        * the specified key.
-        * Returns an empty array if this dictionary contains no mappings for this key.
-        * @param {Object} key key whose associated values are to be returned.
-        * @return {Array} an array holding the values to which this dictionary maps
-        * the specified key.
-        */
-        getValue(key: K): V[];
-        /**
-         * Adds the value to the array associated with the specified key, if
-         * it is not already present.
-         * @param {Object} key key with which the specified value is to be
-         * associated.
-         * @param {Object} value the value to add to the array at the key
-         * @return {boolean} true if the value was not already associated with that key.
-         */
-        setValue(key: K, value: V): boolean;
-        /**
-         * Removes the specified values from the array of values associated with the
-         * specified key. If a value isn't given, all values associated with the specified
-         * key are removed.
-         * @param {Object} key key whose mapping is to be removed from the
-         * dictionary.
-         * @param {Object=} value optional argument to specify the value to remove
-         * from the array associated with the specified key.
-         * @return {*} true if the dictionary changed, false if the key doesn't exist or
-         * if the specified value isn't associated with the specified key.
-         */
-        remove(key: K, value?: V): boolean;
-        /**
-         * Returns an array containing all of the keys in this dictionary.
-         * @return {Array} an array containing all of the keys in this dictionary.
-         */
-        keys(): K[];
-        /**
-         * Returns an array containing all of the values in this dictionary.
-         * @return {Array} an array containing all of the values in this dictionary.
-         */
-        values(): V[];
-        /**
-         * Returns true if this dictionary at least one value associatted the specified key.
-         * @param {Object} key key whose presence in this dictionary is to be
-         * tested.
-         * @return {boolean} true if this dictionary at least one value associatted
-         * the specified key.
-         */
-        containsKey(key: K): boolean;
-        /**
-         * Removes all mappings from this dictionary.
-         */
-        clear(): void;
-        /**
-         * Returns the number of keys in this dictionary.
-         * @return {number} the number of key-value mappings in this dictionary.
-         */
-        size(): number;
-        /**
-         * Returns true if this dictionary contains no mappings.
-         * @return {boolean} true if this dictionary contains no mappings.
-         */
-        isEmpty(): boolean;
-    }
-    class Heap<T> {
-        /**
-         * Array used to store the elements od the heap.
-         * @type {Array.<Object>}
-         * @private
-         */
-        private data;
-        /**
-         * Function used to compare elements.
-         * @type {function(Object,Object):number}
-         * @private
-         */
-        private compare;
-        /**
-         * Creates an empty Heap.
-         * @class
-         * <p>A heap is a binary tree, where the nodes maintain the heap property:
-         * each node is smaller than each of its children and therefore a MinHeap
-         * This implementation uses an array to store elements.</p>
-         * <p>If the inserted elements are custom objects a compare function must be provided,
-         *  at construction time, otherwise the <=, === and >= operators are
-         * used to compare elements. Example:</p>
-         *
-         * <pre>
-         * function compare(a, b) {
-         *  if (a is less than b by some ordering criterion) {
-         *     return -1;
-         *  } if (a is greater than b by the ordering criterion) {
-         *     return 1;
-         *  }
-         *  // a must be equal to b
-         *  return 0;
-         * }
-         * </pre>
-         *
-         * <p>If a Max-Heap is wanted (greater elements on top) you can a provide a
-         * reverse compare function to accomplish that behavior. Example:</p>
-         *
-         * <pre>
-         * function reverseCompare(a, b) {
-         *  if (a is less than b by some ordering criterion) {
-         *     return 1;
-         *  } if (a is greater than b by the ordering criterion) {
-         *     return -1;
-         *  }
-         *  // a must be equal to b
-         *  return 0;
-         * }
-         * </pre>
-         *
-         * @constructor
-         * @param {function(Object,Object):number=} compareFunction optional
-         * function used to compare two elements. Must return a negative integer,
-         * zero, or a positive integer as the first argument is less than, equal to,
-         * or greater than the second.
-         */
-        constructor(compareFunction?: ICompareFunction<T>);
-        /**
-         * Returns the index of the left child of the node at the given index.
-         * @param {number} nodeIndex The index of the node to get the left child
-         * for.
-         * @return {number} The index of the left child.
-         * @private
-         */
-        private leftChildIndex(nodeIndex);
-        /**
-         * Returns the index of the right child of the node at the given index.
-         * @param {number} nodeIndex The index of the node to get the right child
-         * for.
-         * @return {number} The index of the right child.
-         * @private
-         */
-        private rightChildIndex(nodeIndex);
-        /**
-         * Returns the index of the parent of the node at the given index.
-         * @param {number} nodeIndex The index of the node to get the parent for.
-         * @return {number} The index of the parent.
-         * @private
-         */
-        private parentIndex(nodeIndex);
-        /**
-         * Returns the index of the smaller child node (if it exists).
-         * @param {number} leftChild left child index.
-         * @param {number} rightChild right child index.
-         * @return {number} the index with the minimum value or -1 if it doesn't
-         * exists.
-         * @private
-         */
-        private minIndex(leftChild, rightChild);
-        /**
-         * Moves the node at the given index up to its proper place in the heap.
-         * @param {number} index The index of the node to move up.
-         * @private
-         */
-        private siftUp(index);
-        /**
-         * Moves the node at the given index down to its proper place in the heap.
-         * @param {number} nodeIndex The index of the node to move down.
-         * @private
-         */
-        private siftDown(nodeIndex);
-        /**
-         * Retrieves but does not remove the root element of this heap.
-         * @return {*} The value at the root of the heap. Returns undefined if the
-         * heap is empty.
-         */
-        peek(): T;
-        /**
-         * Adds the given element into the heap.
-         * @param {*} element the element.
-         * @return true if the element was added or fals if it is undefined.
-         */
-        add(element: T): boolean;
-        /**
-         * Retrieves and removes the root element of this heap.
-         * @return {*} The value removed from the root of the heap. Returns
-         * undefined if the heap is empty.
-         */
-        removeRoot(): T;
-        /**
-         * Returns true if this heap contains the specified element.
-         * @param {Object} element element to search for.
-         * @return {boolean} true if this Heap contains the specified element, false
-         * otherwise.
-         */
-        contains(element: T): boolean;
-        /**
-         * Returns the number of elements in this heap.
-         * @return {number} the number of elements in this heap.
-         */
-        size(): number;
-        /**
-         * Checks if this heap is empty.
-         * @return {boolean} true if and only if this heap contains no items; false
-         * otherwise.
-         */
-        isEmpty(): boolean;
-        /**
-         * Removes all of the elements from this heap.
-         */
-        clear(): void;
-        /**
-         * Executes the provided function once for each element present in this heap in
-         * no particular order.
-         * @param {function(Object):*} callback function to execute, it is
-         * invoked with one argument: the element value, to break the iteration you can
-         * optionally return false.
-         */
-        forEach(callback: ILoopFunction<T>): void;
-    }
-    class Stack<T> {
-        /**
-         * List containing the elements.
-         * @type collections.LinkedList
-         * @private
-         */
-        private list;
-        /**
-         * Creates an empty Stack.
-         * @class A Stack is a Last-In-First-Out (LIFO) data structure, the last
-         * element added to the stack will be the first one to be removed. This
-         * implementation uses a linked list as a container.
-         * @constructor
-         */
-        constructor();
-        /**
-         * Pushes an item onto the top of this stack.
-         * @param {Object} elem the element to be pushed onto this stack.
-         * @return {boolean} true if the element was pushed or false if it is undefined.
-         */
-        push(elem: T): boolean;
-        /**
-         * Pushes an item onto the top of this stack.
-         * @param {Object} elem the element to be pushed onto this stack.
-         * @return {boolean} true if the element was pushed or false if it is undefined.
-         */
-        add(elem: T): boolean;
-        /**
-         * Removes the object at the top of this stack and returns that object.
-         * @return {*} the object at the top of this stack or undefined if the
-         * stack is empty.
-         */
-        pop(): T;
-        /**
-         * Looks at the object at the top of this stack without removing it from the
-         * stack.
-         * @return {*} the object at the top of this stack or undefined if the
-         * stack is empty.
-         */
-        peek(): T;
-        /**
-         * Returns the number of elements in this stack.
-         * @return {number} the number of elements in this stack.
-         */
-        size(): number;
-        /**
-         * Returns true if this stack contains the specified element.
-         * <p>If the elements inside this stack are
-         * not comparable with the === operator, a custom equals function should be
-         * provided to perform searches, the function must receive two arguments and
-         * return true if they are equal, false otherwise. Example:</p>
-         *
-         * <pre>
-         * var petsAreEqualByName (pet1, pet2) {
-         *  return pet1.name === pet2.name;
-         * }
-         * </pre>
-         * @param {Object} elem element to search for.
-         * @param {function(Object,Object):boolean=} equalsFunction optional
-         * function to check if two elements are equal.
-         * @return {boolean} true if this stack contains the specified element,
-         * false otherwise.
-         */
-        contains(elem: T, equalsFunction?: IEqualsFunction<T>): boolean;
-        /**
-         * Checks if this stack is empty.
-         * @return {boolean} true if and only if this stack contains no items; false
-         * otherwise.
-         */
-        isEmpty(): boolean;
-        /**
-         * Removes all of the elements from this stack.
-         */
-        clear(): void;
-        /**
-         * Executes the provided function once for each element present in this stack in
-         * LIFO order.
-         * @param {function(Object):*} callback function to execute, it is
-         * invoked with one argument: the element value, to break the iteration you can
-         * optionally return false.
-         */
-        forEach(callback: ILoopFunction<T>): void;
-    }
-    class Queue<T> {
-        /**
-         * List containing the elements.
-         * @type collections.LinkedList
-         * @private
-         */
-        private list;
-        /**
-         * Creates an empty queue.
-         * @class A queue is a First-In-First-Out (FIFO) data structure, the first
-         * element added to the queue will be the first one to be removed. This
-         * implementation uses a linked list as a container.
-         * @constructor
-         */
-        constructor();
-        /**
-         * Inserts the specified element into the end of this queue.
-         * @param {Object} elem the element to insert.
-         * @return {boolean} true if the element was inserted, or false if it is undefined.
-         */
-        enqueue(elem: T): boolean;
-        /**
-         * Inserts the specified element into the end of this queue.
-         * @param {Object} elem the element to insert.
-         * @return {boolean} true if the element was inserted, or false if it is undefined.
-         */
-        add(elem: T): boolean;
-        /**
-         * Retrieves and removes the head of this queue.
-         * @return {*} the head of this queue, or undefined if this queue is empty.
-         */
-        dequeue(): T;
-        /**
-         * Retrieves, but does not remove, the head of this queue.
-         * @return {*} the head of this queue, or undefined if this queue is empty.
-         */
-        peek(): T;
-        /**
-         * Returns the number of elements in this queue.
-         * @return {number} the number of elements in this queue.
-         */
-        size(): number;
-        /**
-         * Returns true if this queue contains the specified element.
-         * <p>If the elements inside this stack are
-         * not comparable with the === operator, a custom equals function should be
-         * provided to perform searches, the function must receive two arguments and
-         * return true if they are equal, false otherwise. Example:</p>
-         *
-         * <pre>
-         * var petsAreEqualByName (pet1, pet2) {
-         *  return pet1.name === pet2.name;
-         * }
-         * </pre>
-         * @param {Object} elem element to search for.
-         * @param {function(Object,Object):boolean=} equalsFunction optional
-         * function to check if two elements are equal.
-         * @return {boolean} true if this queue contains the specified element,
-         * false otherwise.
-         */
-        contains(elem: T, equalsFunction?: IEqualsFunction<T>): boolean;
-        /**
-         * Checks if this queue is empty.
-         * @return {boolean} true if and only if this queue contains no items; false
-         * otherwise.
-         */
-        isEmpty(): boolean;
-        /**
-         * Removes all of the elements from this queue.
-         */
-        clear(): void;
-        /**
-         * Executes the provided function once for each element present in this queue in
-         * FIFO order.
-         * @param {function(Object):*} callback function to execute, it is
-         * invoked with one argument: the element value, to break the iteration you can
-         * optionally return false.
-         */
-        forEach(callback: ILoopFunction<T>): void;
-    }
-    class PriorityQueue<T> {
-        private heap;
-        /**
-         * Creates an empty priority queue.
-         * @class <p>In a priority queue each element is associated with a "priority",
-         * elements are dequeued in highest-priority-first order (the elements with the
-         * highest priority are dequeued first). Priority Queues are implemented as heaps.
-         * If the inserted elements are custom objects a compare function must be provided,
-         * otherwise the <=, === and >= operators are used to compare object priority.</p>
-         * <pre>
-         * function compare(a, b) {
-         *  if (a is less than b by some ordering criterion) {
-         *     return -1;
-         *  } if (a is greater than b by the ordering criterion) {
-         *     return 1;
-         *  }
-         *  // a must be equal to b
-         *  return 0;
-         * }
-         * </pre>
-         * @constructor
-         * @param {function(Object,Object):number=} compareFunction optional
-         * function used to compare two element priorities. Must return a negative integer,
-         * zero, or a positive integer as the first argument is less than, equal to,
-         * or greater than the second.
-         */
-        constructor(compareFunction?: ICompareFunction<T>);
-        /**
-         * Inserts the specified element into this priority queue.
-         * @param {Object} element the element to insert.
-         * @return {boolean} true if the element was inserted, or false if it is undefined.
-         */
-        enqueue(element: T): boolean;
-        /**
-         * Inserts the specified element into this priority queue.
-         * @param {Object} element the element to insert.
-         * @return {boolean} true if the element was inserted, or false if it is undefined.
-         */
-        add(element: T): boolean;
-        /**
-         * Retrieves and removes the highest priority element of this queue.
-         * @return {*} the the highest priority element of this queue,
-         *  or undefined if this queue is empty.
-         */
-        dequeue(): T;
-        /**
-         * Retrieves, but does not remove, the highest priority element of this queue.
-         * @return {*} the highest priority element of this queue, or undefined if this queue is empty.
-         */
-        peek(): T;
-        /**
-         * Returns true if this priority queue contains the specified element.
-         * @param {Object} element element to search for.
-         * @return {boolean} true if this priority queue contains the specified element,
-         * false otherwise.
-         */
-        contains(element: T): boolean;
-        /**
-         * Checks if this priority queue is empty.
-         * @return {boolean} true if and only if this priority queue contains no items; false
-         * otherwise.
-         */
-        isEmpty(): boolean;
-        /**
-         * Returns the number of elements in this priority queue.
-         * @return {number} the number of elements in this priority queue.
-         */
-        size(): number;
-        /**
-         * Removes all of the elements from this priority queue.
-         */
-        clear(): void;
-        /**
-         * Executes the provided function once for each element present in this queue in
-         * no particular order.
-         * @param {function(Object):*} callback function to execute, it is
-         * invoked with one argument: the element value, to break the iteration you can
-         * optionally return false.
-         */
-        forEach(callback: ILoopFunction<T>): void;
-    }
-    class Set<T> {
-        private dictionary;
-        /**
-         * Creates an empty set.
-         * @class <p>A set is a data structure that contains no duplicate items.</p>
-         * <p>If the inserted elements are custom objects a function
-         * which converts elements to strings must be provided. Example:</p>
-         *
-         * <pre>
-         * function petToString(pet) {
-         *  return pet.name;
-         * }
-         * </pre>
-         *
-         * @constructor
-         * @param {function(Object):string=} toStringFunction optional function used
-         * to convert elements to strings. If the elements aren't strings or if toString()
-         * is not appropriate, a custom function which receives a onject and returns a
-         * unique string must be provided.
-         */
-        constructor(toStringFunction?: (item: T) => string);
-        /**
-         * Returns true if this set contains the specified element.
-         * @param {Object} element element to search for.
-         * @return {boolean} true if this set contains the specified element,
-         * false otherwise.
-         */
-        contains(element: T): boolean;
-        /**
-         * Adds the specified element to this set if it is not already present.
-         * @param {Object} element the element to insert.
-         * @return {boolean} true if this set did not already contain the specified element.
-         */
-        add(element: T): boolean;
-        /**
-         * Performs an intersecion between this an another set.
-         * Removes all values that are not present this set and the given set.
-         * @param {collections.Set} otherSet other set.
-         */
-        intersection(otherSet: Set<T>): void;
-        /**
-         * Performs a union between this an another set.
-         * Adds all values from the given set to this set.
-         * @param {collections.Set} otherSet other set.
-         */
-        union(otherSet: Set<T>): void;
-        /**
-         * Performs a difference between this an another set.
-         * Removes from this set all the values that are present in the given set.
-         * @param {collections.Set} otherSet other set.
-         */
-        difference(otherSet: Set<T>): void;
-        /**
-         * Checks whether the given set contains all the elements in this set.
-         * @param {collections.Set} otherSet other set.
-         * @return {boolean} true if this set is a subset of the given set.
-         */
-        isSubsetOf(otherSet: Set<T>): boolean;
-        /**
-         * Removes the specified element from this set if it is present.
-         * @return {boolean} true if this set contained the specified element.
-         */
-        remove(element: T): boolean;
-        /**
-         * Executes the provided function once for each element
-         * present in this set.
-         * @param {function(Object):*} callback function to execute, it is
-         * invoked with one arguments: the element. To break the iteration you can
-         * optionally return false.
-         */
-        forEach(callback: ILoopFunction<T>): void;
-        /**
-         * Returns an array containing all of the elements in this set in arbitrary order.
-         * @return {Array} an array containing all of the elements in this set.
-         */
-        toArray(): T[];
-        /**
-         * Returns true if this set contains no elements.
-         * @return {boolean} true if this set contains no elements.
-         */
-        isEmpty(): boolean;
-        /**
-         * Returns the number of elements in this set.
-         * @return {number} the number of elements in this set.
-         */
-        size(): number;
-        /**
-         * Removes all of the elements from this set.
-         */
-        clear(): void;
-        toString(): string;
-    }
-    class Bag<T> {
-        private toStrF;
-        private dictionary;
-        private nElements;
-        /**
-         * Creates an empty bag.
-         * @class <p>A bag is a special kind of set in which members are
-         * allowed to appear more than once.</p>
-         * <p>If the inserted elements are custom objects a function
-         * which converts elements to unique strings must be provided. Example:</p>
-         *
-         * <pre>
-         * function petToString(pet) {
-         *  return pet.name;
-         * }
-         * </pre>
-         *
-         * @constructor
-         * @param {function(Object):string=} toStrFunction optional function used
-         * to convert elements to strings. If the elements aren't strings or if toString()
-         * is not appropriate, a custom function which receives an object and returns a
-         * unique string must be provided.
-         */
-        constructor(toStrFunction?: (item: T) => string);
-        /**
-        * Adds nCopies of the specified object to this bag.
-        * @param {Object} element element to add.
-        * @param {number=} nCopies the number of copies to add, if this argument is
-        * undefined 1 copy is added.
-        * @return {boolean} true unless element is undefined.
-        */
-        add(element: T, nCopies?: number): boolean;
-        /**
-        * Counts the number of copies of the specified object in this bag.
-        * @param {Object} element the object to search for..
-        * @return {number} the number of copies of the object, 0 if not found
-        */
-        count(element: T): number;
-        /**
-         * Returns true if this bag contains the specified element.
-         * @param {Object} element element to search for.
-         * @return {boolean} true if this bag contains the specified element,
-         * false otherwise.
-         */
-        contains(element: T): boolean;
-        /**
-        * Removes nCopies of the specified object to this bag.
-        * If the number of copies to remove is greater than the actual number
-        * of copies in the Bag, all copies are removed.
-        * @param {Object} element element to remove.
-        * @param {number=} nCopies the number of copies to remove, if this argument is
-        * undefined 1 copy is removed.
-        * @return {boolean} true if at least 1 element was removed.
-        */
-        remove(element: T, nCopies?: number): boolean;
-        /**
-         * Returns an array containing all of the elements in this big in arbitrary order,
-         * including multiple copies.
-         * @return {Array} an array containing all of the elements in this bag.
-         */
-        toArray(): T[];
-        /**
-         * Returns a set of unique elements in this bag.
-         * @return {collections.Set<T>} a set of unique elements in this bag.
-         */
-        toSet(): Set<T>;
-        /**
-         * Executes the provided function once for each element
-         * present in this bag, including multiple copies.
-         * @param {function(Object):*} callback function to execute, it is
-         * invoked with one argument: the element. To break the iteration you can
-         * optionally return false.
-         */
-        forEach(callback: ILoopFunction<T>): void;
-        /**
-         * Returns the number of elements in this bag.
-         * @return {number} the number of elements in this bag.
-         */
-        size(): number;
-        /**
-         * Returns true if this bag contains no elements.
-         * @return {boolean} true if this bag contains no elements.
-         */
-        isEmpty(): boolean;
-        /**
-         * Removes all of the elements from this bag.
-         */
-        clear(): void;
-    }
-    class BSTree<T> {
-        private root;
-        private compare;
-        private nElements;
-        /**
-         * Creates an empty binary search tree.
-         * @class <p>A binary search tree is a binary tree in which each
-         * internal node stores an element such that the elements stored in the
-         * left subtree are less than it and the elements
-         * stored in the right subtree are greater.</p>
-         * <p>Formally, a binary search tree is a node-based binary tree data structure which
-         * has the following properties:</p>
-         * <ul>
-         * <li>The left subtree of a node contains only nodes with elements less
-         * than the node's element</li>
-         * <li>The right subtree of a node contains only nodes with elements greater
-         * than the node's element</li>
-         * <li>Both the left and right subtrees must also be binary search trees.</li>
-         * </ul>
-         * <p>If the inserted elements are custom objects a compare function must
-         * be provided at construction time, otherwise the <=, === and >= operators are
-         * used to compare elements. Example:</p>
-         * <pre>
-         * function compare(a, b) {
-         *  if (a is less than b by some ordering criterion) {
-         *     return -1;
-         *  } if (a is greater than b by the ordering criterion) {
-         *     return 1;
-         *  }
-         *  // a must be equal to b
-         *  return 0;
-         * }
-         * </pre>
-         * @constructor
-         * @param {function(Object,Object):number=} compareFunction optional
-         * function used to compare two elements. Must return a negative integer,
-         * zero, or a positive integer as the first argument is less than, equal to,
-         * or greater than the second.
-         */
-        constructor(compareFunction?: ICompareFunction<T>);
-        /**
-         * Adds the specified element to this tree if it is not already present.
-         * @param {Object} element the element to insert.
-         * @return {boolean} true if this tree did not already contain the specified element.
-         */
-        add(element: T): boolean;
-        /**
-         * Removes all of the elements from this tree.
-         */
-        clear(): void;
-        /**
-         * Returns true if this tree contains no elements.
-         * @return {boolean} true if this tree contains no elements.
-         */
-        isEmpty(): boolean;
-        /**
-         * Returns the number of elements in this tree.
-         * @return {number} the number of elements in this tree.
-         */
-        size(): number;
-        /**
-         * Returns true if this tree contains the specified element.
-         * @param {Object} element element to search for.
-         * @return {boolean} true if this tree contains the specified element,
-         * false otherwise.
-         */
-        contains(element: T): boolean;
-        /**
-         * Removes the specified element from this tree if it is present.
-         * @return {boolean} true if this tree contained the specified element.
-         */
-        remove(element: T): boolean;
-        /**
-         * Executes the provided function once for each element present in this tree in
-         * in-order.
-         * @param {function(Object):*} callback function to execute, it is invoked with one
-         * argument: the element value, to break the iteration you can optionally return false.
-         */
-        inorderTraversal(callback: ILoopFunction<T>): void;
-        /**
-         * Executes the provided function once for each element present in this tree in pre-order.
-         * @param {function(Object):*} callback function to execute, it is invoked with one
-         * argument: the element value, to break the iteration you can optionally return false.
-         */
-        preorderTraversal(callback: ILoopFunction<T>): void;
-        /**
-         * Executes the provided function once for each element present in this tree in post-order.
-         * @param {function(Object):*} callback function to execute, it is invoked with one
-         * argument: the element value, to break the iteration you can optionally return false.
-         */
-        postorderTraversal(callback: ILoopFunction<T>): void;
-        /**
-         * Executes the provided function once for each element present in this tree in
-         * level-order.
-         * @param {function(Object):*} callback function to execute, it is invoked with one
-         * argument: the element value, to break the iteration you can optionally return false.
-         */
-        levelTraversal(callback: ILoopFunction<T>): void;
-        /**
-         * Returns the minimum element of this tree.
-         * @return {*} the minimum element of this tree or undefined if this tree is
-         * is empty.
-         */
-        minimum(): T;
-        /**
-         * Returns the maximum element of this tree.
-         * @return {*} the maximum element of this tree or undefined if this tree is
-         * is empty.
-         */
-        maximum(): T;
-        /**
-         * Executes the provided function once for each element present in this tree in inorder.
-         * Equivalent to inorderTraversal.
-         * @param {function(Object):*} callback function to execute, it is
-         * invoked with one argument: the element value, to break the iteration you can
-         * optionally return false.
-         */
-        forEach(callback: ILoopFunction<T>): void;
-        /**
-         * Returns an array containing all of the elements in this tree in in-order.
-         * @return {Array} an array containing all of the elements in this tree in in-order.
-         */
-        toArray(): T[];
-        /**
-         * Returns the height of this tree.
-         * @return {number} the height of this tree or -1 if is empty.
-         */
-        height(): number;
-        /**
-        * @private
-        */
-        private searchNode(node, element);
-        /**
-        * @private
-        */
-        private transplant(n1, n2);
-        /**
-        * @private
-        */
-        private removeNode(node);
-        /**
-        * @private
-        */
-        private inorderTraversalAux(node, callback, signal);
-        /**
-        * @private
-        */
-        private levelTraversalAux(node, callback);
-        /**
-        * @private
-        */
-        private preorderTraversalAux(node, callback, signal);
-        /**
-        * @private
-        */
-        private postorderTraversalAux(node, callback, signal);
-        /**
-        * @private
-        */
-        private minimumAux(node);
-        /**
-        * @private
-        */
-        private maximumAux(node);
-        /**
-          * @private
-          */
-        private heightAux(node);
-        private insertNode(node);
-        /**
-        * @private
-        */
-        private createNode(element);
-    }
-}
diff --git a/typings/moment/moment-node.d.ts b/typings/moment/moment-node.d.ts
deleted file mode 100644
index 3471a8fc..00000000
--- a/typings/moment/moment-node.d.ts
+++ /dev/null
@@ -1,495 +0,0 @@
-// Type definitions for Moment.js 2.10.5
-// Project: https://github.com/timrwood/moment
-// Definitions by: Michael Lakerveld <https://github.com/Lakerfield>, Aaron King <https://github.com/kingdango>, Hiroki Horiuchi <https://github.com/horiuchi>, Dick van den Brink <https://github.com/DickvdBrink>, Adi Dahiya <https://github.com/adidahiya>, Matt Brooks <https://github.com/EnableSoftware>
-// Definitions: https://github.com/borisyankov/DefinitelyTyped
-
-declare module moment {
-
-    interface MomentDateObject {
-        years?: number;
-        /* One digit */
-        months?: number;
-        /* Day of the month */
-        date?: number;
-        hours?: number;
-        minutes?: number;
-        seconds?: number;
-        milliseconds?: number;
-    }
-    
-    interface MomentInput {
-        /** Year */
-        years?: number;
-        /** Year */
-        year?: number;
-        /** Year */
-        y?: number;
-
-        /** Month */
-        months?: number;
-        /** Month */
-        month?: number;
-        /** Month */
-        M?: number;
-
-        /** Week */
-        weeks?: number;
-        /** Week */
-        week?: number;
-        /** Week */
-        w?: number;
-
-        /** Day/Date */
-        days?: number;
-        /** Day/Date */
-        day?: number;
-        /** Day/Date */
-        date?: number;
-        /** Day/Date */
-        d?: number;
-
-        /** Hour */
-        hours?: number;
-        /** Hour */
-        hour?: number;
-        /** Hour */
-        h?: number;
-
-        /** Minute */
-        minutes?: number;
-        /** Minute */
-        minute?: number;
-        /** Minute */
-        m?: number;
-
-        /** Second */
-        seconds?: number;
-        /** Second */
-        second?: number;
-        /** Second */
-        s?: number;
-
-        /** Millisecond */
-        milliseconds?: number;
-        /** Millisecond */
-        millisecond?: number;
-        /** Millisecond */
-        ms?: number;
-    }
-
-    interface Duration {
-        humanize(withSuffix?: boolean): string;
-
-        as(units: string): number;
-
-        milliseconds(): number;
-        asMilliseconds(): number;
-
-        seconds(): number;
-        asSeconds(): number;
-
-        minutes(): number;
-        asMinutes(): number;
-
-        hours(): number;
-        asHours(): number;
-
-        days(): number;
-        asDays(): number;
-
-        months(): number;
-        asMonths(): number;
-
-        years(): number;
-        asYears(): number;
-
-        add(n: number, p: string): Duration;
-        add(n: number): Duration;
-        add(d: Duration): Duration;
-
-        subtract(n: number, p: string): Duration;
-        subtract(n: number): Duration;
-        subtract(d: Duration): Duration;
-
-        toISOString(): string;
-        toJSON(): string;
-    }
-
-    interface Moment {
-        format(format: string): string;
-        format(): string;
-
-        fromNow(withoutSuffix?: boolean): string;
-
-        startOf(unitOfTime: string): Moment;
-        endOf(unitOfTime: string): Moment;
-
-        /**
-         * Mutates the original moment by adding time. (deprecated in 2.8.0)
-         *
-         * @param unitOfTime the unit of time you want to add (eg "years" / "hours" etc)
-         * @param amount the amount you want to add
-         */
-        add(unitOfTime: string, amount: number): Moment;
-        /**
-         * Mutates the original moment by adding time.
-         *
-         * @param amount the amount you want to add
-         * @param unitOfTime the unit of time you want to add (eg "years" / "hours" etc)
-         */
-        add(amount: number, unitOfTime: string): Moment;
-        /**
-         * Mutates the original moment by adding time. Note that the order of arguments can be flipped.
-         *
-         * @param amount the amount you want to add
-         * @param unitOfTime the unit of time you want to add (eg "years" / "hours" etc)
-         */
-        add(amount: string, unitOfTime: string): Moment;
-        /**
-         * Mutates the original moment by adding time.
-         *
-         * @param objectLiteral an object literal that describes multiple time units {days:7,months:1}
-         */
-        add(objectLiteral: MomentInput): Moment;
-        /**
-         * Mutates the original moment by adding time.
-         *
-         * @param duration a length of time
-         */
-        add(duration: Duration): Moment;
-
-        /**
-         * Mutates the original moment by subtracting time. (deprecated in 2.8.0)
-         *
-         * @param unitOfTime the unit of time you want to subtract (eg "years" / "hours" etc)
-         * @param amount the amount you want to subtract
-         */
-        subtract(unitOfTime: string, amount: number): Moment;
-        /**
-         * Mutates the original moment by subtracting time.
-         *
-         * @param unitOfTime the unit of time you want to subtract (eg "years" / "hours" etc)
-         * @param amount the amount you want to subtract
-         */
-        subtract(amount: number, unitOfTime: string): Moment;
-        /**
-         * Mutates the original moment by subtracting time. Note that the order of arguments can be flipped.
-         *
-         * @param amount the amount you want to add
-         * @param unitOfTime the unit of time you want to subtract (eg "years" / "hours" etc)
-         */
-        subtract(amount: string, unitOfTime: string): Moment;
-        /**
-         * Mutates the original moment by subtracting time.
-         *
-         * @param objectLiteral an object literal that describes multiple time units {days:7,months:1}
-         */
-        subtract(objectLiteral: MomentInput): Moment;
-        /**
-         * Mutates the original moment by subtracting time.
-         *
-         * @param duration a length of time
-         */
-        subtract(duration: Duration): Moment;
-
-        calendar(): string;
-        calendar(start: Moment): string;
-        calendar(start: Moment, formats: MomentCalendar): string;
-
-        clone(): Moment;
-
-        /**
-         * @return Unix timestamp, or milliseconds since the epoch.
-         */
-        valueOf(): number;
-
-        local(): Moment; // current date/time in local mode
-
-        utc(): Moment; // current date/time in UTC mode
-
-        isValid(): boolean;
-        invalidAt(): number;
-
-        year(y: number): Moment;
-        year(): number;
-        quarter(): number;
-        quarter(q: number): Moment;
-        month(M: number): Moment;
-        month(M: string): Moment;
-        month(): number;
-        day(d: number): Moment;
-        day(d: string): Moment;
-        day(): number;
-        date(d: number): Moment;
-        date(): number;
-        hour(h: number): Moment;
-        hour(): number;
-        hours(h: number): Moment;
-        hours(): number;
-        minute(m: number): Moment;
-        minute(): number;
-        minutes(m: number): Moment;
-        minutes(): number;
-        second(s: number): Moment;
-        second(): number;
-        seconds(s: number): Moment;
-        seconds(): number;
-        millisecond(ms: number): Moment;
-        millisecond(): number;
-        milliseconds(ms: number): Moment;
-        milliseconds(): number;
-        weekday(): number;
-        weekday(d: number): Moment;
-        isoWeekday(): number;
-        isoWeekday(d: number): Moment;
-        weekYear(): number;
-        weekYear(d: number): Moment;
-        isoWeekYear(): number;
-        isoWeekYear(d: number): Moment;
-        week(): number;
-        week(d: number): Moment;
-        weeks(): number;
-        weeks(d: number): Moment;
-        isoWeek(): number;
-        isoWeek(d: number): Moment;
-        isoWeeks(): number;
-        isoWeeks(d: number): Moment;
-        weeksInYear(): number;
-        isoWeeksInYear(): number;
-        dayOfYear(): number;
-        dayOfYear(d: number): Moment;
-
-        from(f: Moment | string | number | Date | number[], suffix?: boolean): string;
-        to(f: Moment | string | number | Date | number[], suffix?: boolean): string;
-        toNow(withoutPrefix?: boolean): string;
-
-        diff(b: Moment): number;
-        diff(b: Moment, unitOfTime: string): number;
-        diff(b: Moment, unitOfTime: string, round: boolean): number;
-
-        toArray(): number[];
-        toDate(): Date;
-        toISOString(): string;
-        toJSON(): string;
-        unix(): number;
-
-        isLeapYear(): boolean;
-        zone(): number;
-        zone(b: number): Moment;
-        zone(b: string): Moment;
-        utcOffset(): number;
-        utcOffset(b: number): Moment;
-        utcOffset(b: string): Moment;
-        daysInMonth(): number;
-        isDST(): boolean;
-
-        isBefore(): boolean;
-        isBefore(b: Moment | string | number | Date | number[], granularity?: string): boolean;
-
-        isAfter(): boolean;
-        isAfter(b: Moment | string | number | Date | number[], granularity?: string): boolean;
-
-        isSame(b: Moment | string | number | Date | number[], granularity?: string): boolean;
-        isBetween(a: Moment | string | number | Date | number[], b: Moment | string | number | Date | number[], granularity?: string): boolean;
-
-        // Deprecated as of 2.8.0.
-        lang(language: string): Moment;
-        lang(reset: boolean): Moment;
-        lang(): MomentLanguage;
-
-        locale(language: string): Moment;
-        locale(reset: boolean): Moment;
-        locale(): string;
-
-        localeData(language: string): Moment;
-        localeData(reset: boolean): Moment;
-        localeData(): MomentLanguage;
-
-        // Deprecated as of 2.7.0.
-        max(date: Moment | string | number | Date | any[]): Moment;
-        max(date: string, format: string): Moment;
-
-        // Deprecated as of 2.7.0.
-        min(date: Moment | string | number | Date | any[]): Moment;
-        min(date: string, format: string): Moment;
-
-        get(unit: string): number;
-        set(unit: string, value: number): Moment;
-        set(objectLiteral: MomentInput): Moment;
-        
-        /*This returns an object containing year, month, day-of-month, hour, minute, seconds, milliseconds.*/
-        //Works with version 2.10.5+
-        toObject(): MomentDateObject;
-    }
-
-    type formatFunction = () => string;
-
-    interface MomentCalendar {
-        lastDay?: string | formatFunction;
-        sameDay?: string | formatFunction;
-        nextDay?: string | formatFunction;
-        lastWeek?: string | formatFunction;
-        nextWeek?: string | formatFunction;
-        sameElse?: string | formatFunction;
-    }
-
-    interface BaseMomentLanguage {
-        months?: any;
-        monthsShort?: any;
-        weekdays?: any;
-        weekdaysShort?: any;
-        weekdaysMin?: any;
-        relativeTime?: MomentRelativeTime;
-        meridiem?: (hour: number, minute: number, isLowercase: boolean) => string;
-        calendar?: MomentCalendar;
-        ordinal?: (num: number) => string;
-    }
-
-    interface MomentLanguage extends BaseMomentLanguage {
-        longDateFormat?: MomentLongDateFormat;
-    }
-
-    interface MomentLanguageData extends BaseMomentLanguage {
-        /**
-         * @param formatType should be L, LL, LLL, LLLL.
-         */
-        longDateFormat(formatType: string): string;
-    }
-
-    interface MomentLongDateFormat {
-        L: string;
-        LL: string;
-        LLL: string;
-        LLLL: string;
-        LT: string;
-        LTS: string;
-        l?: string;
-        ll?: string;
-        lll?: string;
-        llll?: string;
-        lt?: string;
-        lts?: string;
-    }
-
-    interface MomentRelativeTime {
-        future: any;
-        past: any;
-        s: any;
-        m: any;
-        mm: any;
-        h: any;
-        hh: any;
-        d: any;
-        dd: any;
-        M: any;
-        MM: any;
-        y: any;
-        yy: any;
-    }
-
-    interface MomentStatic {
-        version: string;
-        fn: Moment;
-
-        (): Moment;
-        (date: number): Moment;
-        (date: number[]): Moment;
-        (date: string, format?: string, strict?: boolean): Moment;
-        (date: string, format?: string, language?: string, strict?: boolean): Moment;
-        (date: string, formats: string[], strict?: boolean): Moment;
-        (date: string, formats: string[], language?: string, strict?: boolean): Moment;
-        (date: string, specialFormat: () => void, strict?: boolean): Moment;
-        (date: string, specialFormat: () => void, language?: string, strict?: boolean): Moment;
-        (date: string, formatsIncludingSpecial: any[], strict?: boolean): Moment;
-        (date: string, formatsIncludingSpecial: any[], language?: string, strict?: boolean): Moment;
-        (date: Date): Moment;
-        (date: Moment): Moment;
-        (date: Object): Moment;
-
-        utc(): Moment;
-        utc(date: number): Moment;
-        utc(date: number[]): Moment;
-        utc(date: string, format?: string, strict?: boolean): Moment;
-        utc(date: string, format?: string, language?: string, strict?: boolean): Moment;
-        utc(date: string, formats: string[], strict?: boolean): Moment;
-        utc(date: string, formats: string[], language?: string, strict?: boolean): Moment;
-        utc(date: Date): Moment;
-        utc(date: Moment): Moment;
-        utc(date: Object): Moment;
-
-        unix(timestamp: number): Moment;
-
-        invalid(parsingFlags?: Object): Moment;
-        isMoment(): boolean;
-        isMoment(m: any): boolean;
-        isDate(m: any): boolean;
-        isDuration(): boolean;
-        isDuration(d: any): boolean;
-
-        // Deprecated in 2.8.0.
-        lang(language?: string): string;
-        lang(language?: string, definition?: MomentLanguage): string;
-
-        locale(language?: string): string;
-        locale(language?: string[]): string;
-        locale(language?: string, definition?: MomentLanguage): string;
-
-        localeData(language?: string): MomentLanguageData;
-
-        longDateFormat: any;
-        relativeTime: any;
-        meridiem: (hour: number, minute: number, isLowercase: boolean) => string;
-        calendar: any;
-        ordinal: (num: number) => string;
-
-        duration(milliseconds: Number): Duration;
-        duration(num: Number, unitOfTime: string): Duration;
-        duration(input: MomentInput): Duration;
-        duration(object: any): Duration;
-        duration(): Duration;
-
-        parseZone(date: string): Moment;
-
-        months(): string[];
-        months(index: number): string;
-        months(format: string): string[];
-        months(format: string, index: number): string;
-        monthsShort(): string[];
-        monthsShort(index: number): string;
-        monthsShort(format: string): string[];
-        monthsShort(format: string, index: number): string;
-
-        weekdays(): string[];
-        weekdays(index: number): string;
-        weekdays(format: string): string[];
-        weekdays(format: string, index: number): string;
-        weekdaysShort(): string[];
-        weekdaysShort(index: number): string;
-        weekdaysShort(format: string): string[];
-        weekdaysShort(format: string, index: number): string;
-        weekdaysMin(): string[];
-        weekdaysMin(index: number): string;
-        weekdaysMin(format: string): string[];
-        weekdaysMin(format: string, index: number): string;
-
-        min(...moments: Moment[]): Moment;
-        max(...moments: Moment[]): Moment;
-
-        normalizeUnits(unit: string): string;
-        relativeTimeThreshold(threshold: string): number | boolean;
-        relativeTimeThreshold(threshold: string, limit: number): boolean;
-
-        /**
-         * Constant used to enable explicit ISO_8601 format parsing.
-         */
-        ISO_8601(): void;
-
-        defaultFormat: string;
-    }
-
-}
-
-declare module 'moment' {
-    var moment: moment.MomentStatic;
-    export = moment;
-}
diff --git a/typings/moment/moment.d.ts b/typings/moment/moment.d.ts
deleted file mode 100644
index 78b09016..00000000
--- a/typings/moment/moment.d.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-// Type definitions for Moment.js 2.8.0
-// Project: https://github.com/timrwood/moment
-// Definitions by: Michael Lakerveld <https://github.com/Lakerfield>, Aaron King <https://github.com/kingdango>, Hiroki Horiuchi <https://github.com/horiuchi>, Dick van den Brink <https://github.com/DickvdBrink>, Adi Dahiya <https://github.com/adidahiya>, Matt Brooks <https://github.com/EnableSoftware>
-// Definitions: https://github.com/borisyankov/DefinitelyTyped
-
-/// <reference path="moment-node.d.ts" />
-
-declare var moment: moment.MomentStatic;
diff --git a/typings/node.d.ts b/typings/node.d.ts
deleted file mode 100644
index 5ed7730b..00000000
--- a/typings/node.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-/// <reference path="../node_modules/vscode/typings/node.d.ts" />
\ No newline at end of file
diff --git a/typings/vscode-typings.d.ts b/typings/vscode-typings.d.ts
deleted file mode 100644
index 61430b1c..00000000
--- a/typings/vscode-typings.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-/// <reference path="../node_modules/vscode/typings/index.d.ts" />
diff --git a/vsc-extension-quickstart.md b/vsc-extension-quickstart.md
deleted file mode 100644
index 4dfd9da2..00000000
--- a/vsc-extension-quickstart.md
+++ /dev/null
@@ -1,33 +0,0 @@
-# Welcome to your first VS Code Extension
-
-## What's in the folder
-* This folder contains all of the files necessary for your extension
-* `package.json` - this is the manifest file in which you declare your extension and command. 
-The sample plugin registers a command and defines its title and command name. With this information 
-VS Code can show the command in the command palette. It doesn’t yet need to load the plugin. 
-* `src/extension.ts` - this is the main file where you will provide the implementation of your command. 
-The file exports one function, `activate`, which is called the very first time your extension is 
-activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`. 
-We pass the function containing the implementation of the command as the second parameter to 
-`registerCommand`.
-
-## Get up and running straight away 
-* press `F5` to open a new window with your extension loaded
-* run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`
-* set breakpoints in your code inside `src/extension.ts` to debug your extension
-* find output from your extension in the debug console
-
-## Make changes
-* you can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`
-* you can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes
-
-## Explore the API
-* you can open the full set of our API when you open the file `node_modules/vscode/vscode.d.ts`
-
-## Run tests
-* open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the launch configuration dropdown pick `Launch Tests`
-* press `F5` to run the tests in a new window with your extension loaded
-* see the output of the test result in the debug console
-* make changes to `test/extension.test.ts` or create new test files inside the `test` folder
-	* by convention, the test runner will only consider files matching the name pattern `**.test.ts`
-	* you can create folders inside the `test` folder to structure your tests any way you want
\ No newline at end of file
