Repository: clarkio/vscode-twitch-highlighter
Branch: main
Commit: 6569f8b9a260
Files: 60
Total size: 99.8 KB
Directory structure:
gitextract_oxdixv9m/
├── .all-contributorsrc
├── .editorconfig
├── .gitattributes
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ └── bug_report.md
│ └── workflows/
│ ├── ci-pipeline.yml
│ ├── codeql-analysis.yml
│ └── deploy.yml
├── .gitignore
├── .vscode/
│ ├── extensions.json
│ ├── launch.json
│ ├── settings.json
│ └── tasks.json
├── .vscodeignore
├── CHANGELOG.md
├── README.md
├── build.yml
├── package.json
├── src/
│ ├── api.ts
│ ├── app.ts
│ ├── common/
│ │ ├── index.ts
│ │ └── keytar.ts
│ ├── constants.ts
│ ├── credentialManager.ts
│ ├── enums/
│ │ ├── AppContexts.ts
│ │ ├── Commands.ts
│ │ ├── Configuration.ts
│ │ ├── InternalCommands.ts
│ │ ├── KeytarKeys.ts
│ │ ├── LogLevel.ts
│ │ ├── Settings.ts
│ │ ├── TwitchKeys.ts
│ │ └── index.ts
│ ├── extension.ts
│ ├── highlight/
│ │ ├── Highlight.ts
│ │ ├── HighlightManager.ts
│ │ ├── index.ts
│ │ └── treeView/
│ │ ├── HighlightTreeDataProvider.ts
│ │ ├── HighlightTreeItem.ts
│ │ └── index.ts
│ ├── index.ts
│ ├── logger.ts
│ ├── test/
│ │ ├── runTest.ts
│ │ └── suite/
│ │ ├── extension.test.ts
│ │ ├── index.ts
│ │ └── utils.test.ts
│ ├── ttvchat/
│ │ ├── AuthenticationService.ts
│ │ ├── ChatClient.ts
│ │ ├── TwitchChatService.ts
│ │ ├── api/
│ │ │ ├── API.ts
│ │ │ └── index.ts
│ │ ├── index.ts
│ │ └── login/
│ │ └── index.htm
│ └── utils/
│ ├── getNodeModule.ts
│ ├── index.ts
│ ├── isEnum.ts
│ ├── naturalCompare.ts
│ └── parseMessage.ts
├── tsconfig.json
├── tslint.json
└── webpack.config.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .all-contributorsrc
================================================
{
"files": [
"README.md"
],
"imageSize": 100,
"commit": false,
"contributors": [
{
"login": "parithon",
"name": "Anthony Conrad (parithon)",
"avatar_url": "https://avatars.githubusercontent.com/u/8602418?v=4",
"profile": "https://github.com/parithon",
"contributions": [
"code"
]
},
{
"login": "MatthewKosloski",
"name": "Matthew Kosloski",
"avatar_url": "https://avatars.githubusercontent.com/u/1219553?v=4",
"profile": "https://matthewkosloski.me/",
"contributions": [
"code"
]
},
{
"login": "yoannfleurydev",
"name": "Yoann Fleury",
"avatar_url": "https://avatars.githubusercontent.com/u/3920615?v=4",
"profile": "http://blog.yoannfleury.dev",
"contributions": [
"code"
]
},
{
"login": "Technickel-Dev",
"name": "Technickel",
"avatar_url": "https://avatars.githubusercontent.com/u/22779812?v=4",
"profile": "https://www.technickel.dev/",
"contributions": [
"code",
"test"
]
}
],
"contributorsPerLine": 7,
"projectName": "vscode-twitch-highlighter",
"projectOwner": "clarkio",
"repoType": "github",
"repoHost": "https://github.com",
"skipCi": true,
"commitConvention": "angular"
}
================================================
FILE: .editorconfig
================================================
# http://editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
# Use 4 spaces for the Python files
[*.py]
indent_size = 4
max_line_length = 80
# The JSON files contain newlines inconsistently
[*.json]
insert_final_newline = ignore
# Minified JavaScript files shouldn't be changed
[**.min.js]
indent_style = ignore
insert_final_newline = ignore
# Makefiles always use tabs for indentation
[Makefile]
indent_style = tab
# Batch files use tabs for indentation
[*.bat]
indent_style = tab
[*.md]
trim_trailing_whitespace = false
================================================
FILE: .gitattributes
================================================
# Set default behavior to automatically normalize line endings.
* text=auto
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**VSCode Version**
<!-- copy-paste the Help > About section -->
**Additional context**
Add any other context about the problem here.
================================================
FILE: .github/workflows/ci-pipeline.yml
================================================
name: "Build and Test"
on:
pull_request:
branches:
- vnext
- main
jobs:
build:
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 16.x
- run: npm install
- run: xvfb-run -a npm test
if: runner.os == 'Linux'
- run: npm test
if: runner.os != 'Linux'
================================================
FILE: .github/workflows/codeql-analysis.yml
================================================
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ vnext, main ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ vnext ]
schedule:
- cron: '34 9 * * 5'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
================================================
FILE: .github/workflows/deploy.yml
================================================
name: "Deploy to Registries"
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 16
- run: npm ci
- name: Publish to Open VSX Registry
uses: HaaLeo/publish-vscode-extension@v1
with:
pat: ${{ secrets.OPEN_VSX_TOKEN }}
- name: Publish to Visual Studio Marketplace
uses: HaaLeo/publish-vscode-extension@v1
with:
pat: ${{ secrets.VSCE_PAT }}
registryUrl: https://marketplace.visualstudio.com
================================================
FILE: .gitignore
================================================
out
node_modules
types
.vscode-test/
*.vsix
.dccache
================================================
FILE: .vscode/extensions.json
================================================
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
"eg2.tslint"
]
}
================================================
FILE: .vscode/launch.json
================================================
// A launch configuration that compiles the extension and then opens it inside a new window
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
{
"version": "0.2.0",
"configurations": [
{
"name": "Extension",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}",
],
"outFiles": ["${workspaceFolder}/out/**/*.js"],
"preLaunchTask": "npm: watch"
},
{
"name": "Extension Disable Other Extensions",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}",
"--disable-extensions"
],
"outFiles": ["${workspaceFolder}/out/**/*.js"],
"preLaunchTask": "npm: watch"
},
{
"type": "node",
"request": "attach",
"name": "Attach to Server",
"port": 6009,
"timeout": 10000,
"restart": true,
"outFiles": ["${workspaceRoot}/out/**/*.js"]
},
{
"name": "Extension Tests",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}",
"--extensionTestsPath=${workspaceFolder}/out/test/suite/index",
"--disable-extensions"
],
"outFiles": ["${workspaceFolder}/out/test/**/*.js"],
"preLaunchTask": "npm: watch"
}
],
"compounds": [
{
"name": "Extension + LSP",
"configurations": ["Attach to Server", "Extension"]
}
]
}
================================================
FILE: .vscode/settings.json
================================================
// 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
"node_modules": false
},
"search.exclude": {
"out": true, // set this to false to include "out" folder in search results
"node_modules": true
},
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
"typescript.tsc.autoDetect": "off",
"typescript.tsdk": "node_modules\\typescript\\lib",
"prettier.singleQuote": true
}
================================================
FILE: .vscode/tasks.json
================================================
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "watch",
"problemMatcher": "$tsc-watch",
"isBackground": true,
"presentation": {
"reveal": "never"
},
"group": {
"kind": "build",
"isDefault": true
}
}
]
}
================================================
FILE: .vscodeignore
================================================
.github/**
.vscode/**
.vscode-test/**
out/test/**
out/**/*.map
src/**
.gitignore
build.yml
tsconfig.json
vsc-extension-quickstart.md
tslint.json
node_modules/
webpack.config.js
================================================
FILE: CHANGELOG.md
================================================
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [Released]
## [1.0.6]
### Added
- Support to publish to Open VSX Registry
## [1.0.5]
### Changed
- To ignore .dccache file created in a project when using the Snyk extension or CLI for Snyk Code in [#171](https://github.com/clarkio/vscode-twitch-highlighter/pull/171)
- To newest npm package lock version in [#171](https://github.com/clarkio/vscode-twitch-highlighter/pull/171)
- Default branch to be `main` to be used for production releases and will continue to use `vnext` as the active development branch. Feature branches will still be used with PRs for them opened against `vnext` and then when we're ready to push a new release we'll open a PR from `vnext` to `main`.
### Fixed
- Use of login page to run on expected port of 5001 for OAuth redirect flows between VS Code and a browser when authenticating with Twitch in [#171](https://github.com/clarkio/vscode-twitch-highlighter/pull/171)
## [1.0.4]
### Changed
- Testing from vscode to @vscode/test-electron in [#153](https://github.com/clarkio/vscode-twitch-highlighter/pull/153)
## [1.0.3]
### Changed
- To new Twitch App Client Id
## [1.0.2]
### Security
- Addressed dependency from security audit for package `bl`: [bl npm advisory](https://npmjs.com/advisories/1555)
## [1.0.1]
### Fixed
- Reading of extension settings by name: [commit](https://github.com/clarkio/vscode-twitch-highlighter/commit/5b6844bd999d5eefb5ec186f76510b0a62704d4e)
## [1.0.0]
First full release and no longer in preview!
### Added
- Export an API for others to create an extension that ties into YouTube, Mixer, etc.
- Ability to remove highlights using a context-menu.
- Functionality for Highlights to adjust when editing the text around the highlight
### Changed
- We now use Twitch's OAuth endpoint to generate an access_token and saving the token into your credential manager.
### Removed
- Need to manually generate an OAuth token that you enter into a prompt when logging in for this extension
### Security
- [Address vulnerability for dependency 'elliptic'](https://github.com/clarkio/vscode-twitch-highlighter/commit/9150f06a4290402a562c1404001ce7fa289efce3)
## [0.5.1]
### Added
- Context menu support to remove highlights. You can now remove highlights by right-clicking on them. Additionally, you can remove all highlights by right-clicking the editor tab.
## [0.5.0]
### Rewrite
This is a complete rewrite of the extension. The extension now has an API that others may use to build add-on extensions to extend to other chat services like Youtube or Mixer.
## [0.2.3]
### Fixed
- Issue where highlights were not visible in tree views ([110](https://github.com/clarkio/vscode-twitch-highlighter/pull/110) thanks @parithon)
## [0.2.2]
### Added
- Bot response to just `!line` in chat with instructions on how to use it ([96](https://github.com/clarkio/vscode-twitch-highlighter/pull/96) thanks @parithon)
### Changed
- Replaced all UI text and naming in code for "password" with "token" as that is more accurate to its use ([92](https://github.com/clarkio/vscode-twitch-highlighter/pull/92) thanks @parithon)
## [0.2.1]
### Added
- Template for issues ([90](https://github.com/clarkio/vscode-twitch-highlighter/pull/90) thanks @yoannfleurydev)
- Functionality to listen for ban events and remove highlights from banned users in Twitch chat ([99](https://github.com/clarkio/vscode-twitch-highlighter/pull/99) thanks @mpjme)
### Changed
- Updated README to remove duplicate animated gif ([85](https://github.com/clarkio/vscode-twitch-highlighter/pull/85) thanks @parithon)
- Switched from twitch-js library to tmi.js ([97](https://github.com/clarkio/vscode-twitch-highlighter/pull/97) thanks @parithon)
### Fixed
- Inaccurate status in status bar when a token is not provided on initial start up ([91](https://github.com/clarkio/vscode-twitch-highlighter/pull/91) thanks @parithon)
## [0.2.0]
### Added
- Highlights tree view in explorer and debug views ([83](https://github.com/clarkio/vscode-twitch-highlighter/pull/83) thanks @parithon)
- More tests using test theory inputs ([79](https://github.com/clarkio/vscode-twitch-highlighter/pull/79) thanks @parithon)
### Changed
- Location of the button to connect/disconnect the highlight from the chat channel to the left ([83](https://github.com/clarkio/vscode-twitch-highlighter/pull/83) thanks @parithon). This was moved to follow the convention that most extensions which are app based add to the left side of the status bar and the right side is reserved more for editor formatting type actions.
- Icon in activity to be hidden but can be shown via the setting `twitchHighlighter.showHighlightsInActivityBar` ([83](https://github.com/clarkio/vscode-twitch-highlighter/pull/83) thanks @parithon)
## [0.1.5]
### Added
- Ability to unhighlight on disconnect ([#67](https://github.com/clarkio/vscode-twitch-highlighter/pull/67) thanks @parithon)
- Option to highlight multiple lines via one command as well as a comment message ([#68](https://github.com/clarkio/vscode-twitch-highlighter/pull/68) thanks @parithon)
- DevOps badges to show status of development and production builds. Plus media to demonstrate the extension in use and a multi-line highlight example ([#75](https://github.com/clarkio/vscode-twitch-highlighter/pull/75) thanks @parithon)
## [0.1.4]
### Added
- A setting to allow changes to font color within highlights ([#55](https://github.com/clarkio/vscode-twitch-highlighter/pull/55))
- An .editorconfig file to keep styling consistent. ([#54](https://github.com/clarkio/vscode-twitch-highlighter/pull/54) thanks @parithon)
- Arbitrary tests to get things started in that area ([#69](https://github.com/clarkio/vscode-twitch-highlighter/pull/69) thanks @parithon)
- Use of webpack to significantly reduce the size of the extension and improve the install speed ([#66](https://github.com/clarkio/vscode-twitch-highlighter/pull/66) thanks @parithon)
### Changed
- Client code with some refactoring to clean it up ([#57](https://github.com/clarkio/vscode-twitch-highlighter/pull/57) thanks @parithon)
- Commands to use categories for grouping of them ([#63](https://github.com/clarkio/vscode-twitch-highlighter/pull/63) thanks @matthewkosloski)
### Removed
- Use of Twitch Glitch logo in the VS Code marketplace and in the activity bar icon. See PR for more details on why ([#65](https://github.com/clarkio/vscode-twitch-highlighter/pull/65) thanks @parithon)
## [0.1.3] - 2019-02-07
### Changed
- Setting names to be camelCase ([#48](https://github.com/clarkio/vscode-twitch-highlighter/pull/48))
- README with better instructions to get started ([#43](https://github.com/clarkio/vscode-twitch-highlighter/pull/43) thanks @FletcherCodes)
- Icon used in the VS Code Marketplace for better contrast/visibility ([#50](https://github.com/clarkio/vscode-twitch-highlighter/pull/50) thanks @parithon)
## [0.1.2] - 2019-02-03
### Fixed
- Issues where commands were not registering. The cause was from node_modules not being included in the package.
## 0.1.0 - 2019-02-01
- Pre-release version to gather feedback from the community and help identify gaps.
[1.0.6]: https://github.com/clarkio/vscode-twitch-highlighter/compare/1.0.5...1.0.6
[1.0.5]: https://github.com/clarkio/vscode-twitch-highlighter/compare/1.0.4...1.0.5
[1.0.4]: https://github.com/clarkio/vscode-twitch-highlighter/compare/1.0.2...1.0.4
[1.0.2]: https://github.com/clarkio/vscode-twitch-highlighter/compare/1.0.1...1.0.2
[1.0.1]: https://github.com/clarkio/vscode-twitch-highlighter/compare/1.0.0...1.0.1
[1.0.0]: https://github.com/clarkio/vscode-twitch-highlighter/compare/0.5.1...1.0.0
[0.5.1]: https://github.com/clarkio/vscode-twitch-highlighter/compare/0.5.0...0.5.1
[0.5.0]: https://github.com/clarkio/vscode-twitch-highlighter/compare/0.2.3...0.5.0
[0.2.3]: https://github.com/clarkio/vscode-twitch-highlighter/compare/0.2.2...0.2.3
[0.2.2]: https://github.com/clarkio/vscode-twitch-highlighter/compare/0.2.1...0.2.2
[0.2.1]: https://github.com/clarkio/vscode-twitch-highlighter/compare/0.2.0...0.2.1
[0.2.0]: https://github.com/clarkio/vscode-twitch-highlighter/compare/0.1.5...0.2.0
[0.1.5]: https://github.com/clarkio/vscode-twitch-highlighter/compare/0.1.4...0.1.5
[0.1.4]: https://github.com/clarkio/vscode-twitch-highlighter/compare/0.1.3...0.1.4
[0.1.3]: https://github.com/clarkio/vscode-twitch-highlighter/compare/0.1.2...0.1.3
[0.1.2]: https://github.com/clarkio/vscode-twitch-highlighter/compare/b28e5041ac...0.1.2
================================================
FILE: README.md
================================================
<div align="center">
# Twitch Line Highlighter VS Code Extension
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->
[](https://snyk.io/test/github/clarkio/vscode-twitch-highlighter)
[](https://discord.gg/xB95beJ)
[](https://twitch.tv/clarkio)
[](https://github.com/clarkio/vscode-twitch-highlighter/actions/workflows/ci-pipeline.yml)
<br>
[](https://twitter.com/intent/follow?screen_name=_clarkio)
</div>
A VS Code extension to allow your Twitch viewers to help in spotting bugs, typos, etc. by sending a command in chat that will highlight the line of code they want you to check.

<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->
## Clarkio
This VS Code extension was built with 💙 live on stream with the programming community. Come and hang out with us over on Twitch!
> https://twitch.tv/clarkio
## Requirements
In order to use this extension you will need the following things before going to the [Getting Started](#getting-started) section:
- An installed version of [VS Code](https://code.visualstudio.com/?WT.mc_id=academic-0000-brcl)
- A Twitch account for yourself or a separate one to be used as a chat bot ([sign up here](https://www.twitch.tv/signup))
## Getting Started
1. Install the extension from the [marketplace](https://marketplace.visualstudio.com/items?itemName=clarkio.twitch-highlighter&WT.mc_id=academic-0000-brcl)
2. Open your VS Code settings
- Keyboard shortcut: `CTRL/CMD + ,`
3. Type in "twitch" into the search bar
4. Find the `Twitch Highlighter: Channels` setting and enter the name of the channel(s) to which you'd like the extension to connect. Example: `clarkio` If you'd like to connect to more than one channel separate them by commas `,`. Example: `clarkio,parithon`
5. Save your changes and close that tab. Go back to the Settings UI tab.
6. Find the `Nickname` setting. If you are using your own account for the chat bot then enter your account username as the value here. If you created a separate account use that username. Save your changes.
7. Make sure you're logged in to the Twitch account you wish to authorize the highlighter bot to access in your default browser.
8. In the status bar, click the "Twitch" button. After clicking it, you'll see a notification that the extension wants to open a URL.
9. Choose the "Open" option which should open a new tab of your default browser.
10. Read through the permissions that are being requested for use of this bot/extension and choose "Authorize"
11. You should then be notified that you can close the browser/tab
12. Go back to VS Code and you should now see "Disconnected" in the status bar. Click on it to Connect the bot to chat and start listening for highlight commands.
## Twitch Commands
To highlight a line, use:
!highlight <LineNumber> OR !line <LineNumber>
To unhighlight a line, use:
!line !<LineNumber>
To highlight multiple lines, use the same syntax as above but include a range of lines to highlight:
!line <StartLineNumber>-<EndLineNumber>
Additionally, you can also include comments:
!line <LineNumber> This is a comment

## Extension Settings
- `twitchHighlighter.channels`: A comma separated list of channel name(s) to connect to on Twitch. Example: 'clarkio', Another Example: 'clarkio, parithon'
- `twitchHighlighter.nickname`: The username the bot should use when joining a Twitch channel.
> Note: this is required if you'd like to have the bot send join/leave messages in your chat. It also needs to match the Twitch username with which you generated the OAuth token.
- `twitchHighlighter.highlightColor`: Background color of the decoration (default: green). Use rgba() and define transparent background colors to play well with other decorations.
Example: green
- `twitchHighlighter.highlightFontColor`: Font color of the decoration (default: white). Use rgba() and define transparent background colors to play well with other decorations.
Example: white
* `twitchHighlighter.highlightBorder`: CSS styling property that will be applied to text enclosed by a decoration.
* `twitchHighlighter.announceBot`: Whether or not the bot should announce its joining or leaving the chat room.
* `twitchHighlighter.joinMessage`: The message the bot will say when joining a chat room
* `twitchHighlighter.leaveMessage`: The message the bot will say when leaving a chat room
* `twitchHighlighter.showHighlightsInActivityBar`: Show the Highlights icon in the activity bar to display the tree view.
* `twitchHighlighter.usageTip`: A tip shared by the bot when a user chats: '!line'.
## Attribution
Some of the code in this extension has been adapted from the [twitchlint extension](https://github.com/irth/twitchlint) built by [@irth](https://github.com/irth)
## Known Issues
- Extension doesn't allow specifying the file to put the highlight in. This is a work in progress.
## Release Notes
See [CHANGELOG.md](CHANGELOG.md)
## Contributors ✨
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tbody>
<tr>
<td align="center"><a href="https://github.com/parithon"><img src="https://avatars.githubusercontent.com/u/8602418?v=4?s=100" width="100px;" alt="Anthony Conrad (parithon)"/><br /><sub><b>Anthony Conrad (parithon)</b></sub></a><br /><a href="https://github.com/clarkio/vscode-twitch-highlighter/commits?author=parithon" title="Code">💻</a></td>
<td align="center"><a href="https://matthewkosloski.me/"><img src="https://avatars.githubusercontent.com/u/1219553?v=4?s=100" width="100px;" alt="Matthew Kosloski"/><br /><sub><b>Matthew Kosloski</b></sub></a><br /><a href="https://github.com/clarkio/vscode-twitch-highlighter/commits?author=MatthewKosloski" title="Code">💻</a></td>
<td align="center"><a href="http://blog.yoannfleury.dev"><img src="https://avatars.githubusercontent.com/u/3920615?v=4?s=100" width="100px;" alt="Yoann Fleury"/><br /><sub><b>Yoann Fleury</b></sub></a><br /><a href="https://github.com/clarkio/vscode-twitch-highlighter/commits?author=yoannfleurydev" title="Code">💻</a></td>
<td align="center"><a href="https://www.technickel.dev/"><img src="https://avatars.githubusercontent.com/u/22779812?v=4?s=100" width="100px;" alt="Technickel"/><br /><sub><b>Technickel</b></sub></a><br /><a href="https://github.com/clarkio/vscode-twitch-highlighter/commits?author=Technickel-Dev" title="Code">💻</a> <a href="https://github.com/clarkio/vscode-twitch-highlighter/commits?author=Technickel-Dev" title="Tests">⚠️</a></td>
</tr>
</tbody>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
================================================
FILE: build.yml
================================================
jobs:
- job: Windows
pool:
name: Hosted VS2017
demands: npm
steps:
- task: NodeTool@0
displayName: 'Use Node 8.x'
inputs:
versionSpec: 8.x
- task: Npm@1
displayName: 'Install dependencies'
inputs:
verbose: false
- task: Npm@1
displayName: 'Compile sources'
inputs:
command: custom
verbose: false
customCommand: 'run compile'
- script: 'node node_modules/vscode/bin/test'
displayName: 'Run tests'
- job: macOS
pool:
name: Hosted macOS
demands: npm
steps:
- task: NodeTool@0
displayName: 'Use Node 8.x'
inputs:
versionSpec: 8.x
- task: Npm@1
displayName: 'Install dependencies'
inputs:
verbose: false
- task: Npm@1
displayName: 'Compile sources'
inputs:
command: custom
verbose: false
customCommand: 'run compile'
- script: 'node node_modules/vscode/bin/test'
displayName: 'Run tests'
- job: Linux
pool:
name: Hosted Ubuntu 1604
demands: npm
steps:
- task: NodeTool@0
displayName: 'Use Node 8.x'
inputs:
versionSpec: 8.x
- task: Npm@1
displayName: 'Install dependencies'
inputs:
verbose: false
- task: Npm@1
displayName: 'Compile sources'
inputs:
command: custom
verbose: false
customCommand: 'run compile'
- script: |
set -e
/usr/bin/Xvfb :10 -ac >> /tmp/Xvfb.out 2>&1 &
disown -ar
displayName: 'Start xvfb'
- script: 'node node_modules/vscode/bin/test'
displayName: 'Run tests'
env:
DISPLAY: :10
================================================
FILE: package.json
================================================
{
"name": "twitch-highlighter",
"displayName": "Twitch Highlighter",
"description": "Allow your Twitch viewers to help in spotting bugs, typos, etc. by sending a command in chat that will highlight the line of code they want you to check.",
"version": "1.0.6",
"preview": false,
"publisher": "clarkio",
"engines": {
"vscode": "^1.31.0"
},
"icon": "resources/highlighterIcon.png",
"categories": [
"Other"
],
"activationEvents": [
"*"
],
"repository": {
"type": "git",
"url": "https://github.com/clarkio/vscode-twitch-highlighter.git"
},
"license": "MIT",
"main": "./out/extension.js",
"contributes": {
"commands": [
{
"command": "twitchHighlighter.refreshTreeView",
"title": "Refresh"
},
{
"command": "twitchHighlighter.highlight",
"title": "Highlight Line",
"category": "Twitch Highlighter"
},
{
"command": "twitchHighlighter.unhighlight",
"title": "Remove Highlight"
},
{
"command": "twitchHighlighter.unhighlightSpecific",
"title": "Unhighlight by File and Line",
"category": "Twitch Highlighter"
},
{
"command": "twitchHighlighter.unhighlightAll",
"title": "Remove All Highlights",
"category": "Twitch Highlighter"
},
{
"command": "twitchHighlighter.gotoHighlight",
"title": "Goto Highlight"
},
{
"command": "twitchHighlighter.requestHighlight",
"title": "Request a Highlight from the Twitch Chat Client",
"category": "Twitch Highlighter"
},
{
"command": "twitchHighlighter.requestUnhighlight",
"title": "Request an Unhighlight from the Twitch Chat Client",
"category": "Twitch Highlighter"
},
{
"command": "twitchHighlighter.requestUnhighlightAll",
"title": "Request to unhighlight all highlights requested by the Twitch Chat Client",
"category": "Twitch Highlighter"
},
{
"command": "twitchHighlighter.signIn",
"title": "Sign-in to Twitch",
"category": "Twitch Highlighter"
},
{
"command": "twitchHighlighter.signOut",
"title": "Sign-out of Twitch",
"category": "Twitch Highlighter"
},
{
"command": "twitchHighlighter.connect",
"title": "Start Listening to Chat",
"category": "Twitch Highlighter"
},
{
"command": "twitchHighlighter.disconnect",
"title": "Stop Listening to Chat",
"category": "Twitch Highlighter"
},
{
"command": "twitchHighlighter.context.unhighlight",
"title": "Remove Highlight",
"category": "Twitch Highlighter"
}
],
"menus": {
"commandPalette": [
{
"command": "twitchHighlighter.refreshTreeView",
"when": "false"
},
{
"command": "twitchHighlighter.unhighlight",
"when": "false"
},
{
"command": "twitchHighlighter.gotoHighlight",
"when": "false"
},
{
"command": "twitchHighlighter.requestHighlight",
"when": "false"
},
{
"command": "twitchHighlighter.requestUnhighlight",
"when": "false"
},
{
"command": "twitchHighlighter.requestUnhighlightAll",
"when": "false"
},
{
"command": "twitchHighlighter.context.unhighlight",
"when": "false"
}
],
"view/title": [
{
"command": "twitchHighlighter.refreshTreeView",
"when": "view == twitchHighlighterTreeView || view == twitchHighlighterTreeView-explorer || view == twitchHighlighterTreeView-debug",
"group": "navigation"
}
],
"view/item/context": [
{
"command": "twitchHighlighter.unhighlight",
"when": "view == twitchHighlighterTreeView || view == twitchHighlighterTreeView-explorer || view == twitchHighlighterTreeView-debug",
"group": "edit"
}
],
"editor/context": [
{
"command": "twitchHighlighter.context.unhighlight",
"group": "1_modification",
"when": "editorHasHighlights"
}
],
"editor/title/context": [
{
"command": "twitchHighlighter.unhighlightAll",
"group": "3_open",
"when": "editorHasHighlights"
}
]
},
"viewsContainers": {
"activitybar": [
{
"id": "twitchHighlighter-explorer",
"icon": "resources/highlighterIcon.svg",
"title": "Twitch Highlighter"
}
]
},
"views": {
"explorer": [
{
"id": "twitchHighlighterTreeView-explorer",
"name": "Highlights"
}
],
"debug": [
{
"id": "twitchHighlighterTreeView-debug",
"name": "Highlights"
}
],
"twitchHighlighter-explorer": [
{
"id": "twitchHighlighterTreeView",
"name": "Highlights",
"when": "config.twitchHighlighter.showHighlightsInActivityBar"
}
]
},
"configuration": {
"type": "object",
"title": "Twitch Highlighter",
"properties": {
"twitchHighlighter.channels": {
"type": "string",
"default": "",
"description": "A comma separated list of channel name(s) to connect to on Twitch. Example: 'clarkio', Another Example: 'clarkio, parithon'"
},
"twitchHighlighter.nickname": {
"type": "string",
"default": "",
"description": "The username the bot should use when joining a Twitch channel."
},
"twitchHighlighter.highlightColor": {
"type": "string",
"default": "green",
"markdownDescription": "Background color of the decoration. Use rgba() and define transparent background colors to play well with other decorations. Example: green"
},
"twitchHighlighter.highlightFontColor": {
"type": "string",
"default": "white",
"markdownDescription": "Font color of the decoration. Use rgba() and define transparent background colors to play well with other decorations. Example: white"
},
"twitchHighlighter.highlightBorder": {
"type": "string",
"default": "2px solid white",
"description": "CSS styling property that will be applied to text enclosed by a decoration."
},
"twitchHighlighter.announceBot": {
"title": "Twitch Highlighter",
"type": "boolean",
"default": true,
"description": "Whether or not the bot should announce its joining or leaving the chat room"
},
"twitchHighlighter.joinMessage": {
"type": "string",
"default": "Twitch Highlighter in the house!",
"description": "The message the bot will say when joining a chat room"
},
"twitchHighlighter.leaveMessage": {
"type": "string",
"default": "Twitch Highlighter has left the building!",
"description": "The message the bot will say when leaving a chat room"
},
"twitchHighlighter.requiredBadges": {
"type": "array",
"default": [],
"markdownDescription": "A list of badges required to use the highlighter command. The use must have at least one of these badges to use the command. Leave blank for no requirement. Example: moderator, subscriber, vip.",
"items": {
"type": "string",
"pattern": "(admin|bits|broadcaster|global_mod|moderator|subscriber|staff|turbo|premium|follower)*",
"errorMessage": "Expected one of the following: admin, bits, broadcaster, global_admin, moderator, subscriber, staff, turbo, premium, follower"
}
},
"twitchHighlighter.unhighlightOnDisconnect": {
"type": "boolean",
"default": false,
"description": "Unhighlight all lines when disconnected from the chat service."
},
"twitchHighlighter.showHighlightsInActivityBar": {
"type": "boolean",
"default": false,
"description": "Show the Highlights icon in the activity bar to display the tree view."
},
"twitchHighlighter.usageTip": {
"type": "string",
"default": "💡 To use the !line command, use the following format: !line <number> --or-- multiple lines: !line <start>-<end> --or-- with a comment: !line <number> <comment>",
"description": "A tip shared by the bot when a user chats: '!line'."
}
}
}
},
"scripts": {
"vscode:prepublish": "webpack --config webpack.config.js --mode production",
"compile": "copyfiles -au 1 ./src/**/*.htm out/ && tsc -p ./",
"watch": "copyfiles -au 1 ./src/**/*.htm out/ && tsc --watch -p ./",
"test": "npm run compile && node ./out/test/runTest.js"
},
"devDependencies": {
"@types/keytar": "^4.0.1",
"@types/mocha": "^2.2.42",
"@types/node": "^8.10.62",
"@types/request": "^2.48.3",
"@types/tmi.js": "^1.4.0",
"@types/uuid": "^3.4.5",
"@types/vscode": "^1.31.0",
"@vscode/test-electron": "^1.6.1",
"bufferutil": "^4.0.1",
"clean-webpack-plugin": "^3.0.0",
"copy-webpack-plugin": "^6.0.1",
"copyfiles": "^2.3.0",
"glob": "^8.0.3",
"mocha": "^4.1.0",
"mocha-junit-reporter": "^1.18.0",
"spec-xunit-file": "0.0.1-3",
"ts-loader": "^5.3.3",
"tslint": "^5.8.0",
"typescript": "^5.0.4",
"utf-8-validate": "^5.0.2",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.11"
},
"dependencies": {
"request": "^2.88.0",
"tmi.js": "^1.4.2",
"url": "^0.11.1",
"uuid": "^3.3.3"
}
}
================================================
FILE: src/api.ts
================================================
export interface HighlighterAPI {
/**
* Call this function to request a new highlight within the open, active text document.
* @param service The name of the service making the request, for example: twitch.
* @param userName The user requesting the highlight.
* @param startLine The start line used to highlight.
* @param endLine The end line of the highlight (only if highlighting multiple lines).
* @param comments The comment to add to the highlight.
*/
requestHighlight(service: string, userName: string, startLine: number, endLine?: number, comments?: string): void;
/**
* Call this function to request a highlight to be removed from the open, active text document.
* @param service The name of the service making the request, for example: twitch.
* @param userName The user requesting the unhighlight.
* @param lineNumber A line number where the highlight exists.
*/
requestUnhighlight(service: string, userName: string, lineNumber: number): void;
/**
* Call this function to request that all the highlights requested from the service are removed.
* @param service The name of the service making the request, for example: twitch.
*/
requestUnhighlightAll(service: string): void;
}
================================================
FILE: src/app.ts
================================================
import * as vscode from 'vscode';
import { HighlighterAPI } from './api';
import { AppContexts, Commands, Configuration, LogLevel, Settings } from './enums';
import {
HighlightManager,
HighlightTreeDataProvider, HighlightTreeItem
} from './highlight';
import { log, Logger } from './logger';
export class App implements vscode.Disposable {
private readonly _highlightManager: HighlightManager;
private readonly _highlightTreeDataProvider: HighlightTreeDataProvider;
private log: log;
private highlightDecorationType: vscode.TextEditorDecorationType;
private currentDocument?: vscode.TextDocument;
private config?: vscode.WorkspaceConfiguration;
constructor(outputChannel?: vscode.OutputChannel) {
this.log = new Logger(outputChannel).log;
this.config = vscode.workspace.getConfiguration(Configuration.sectionIdentifier);
this.highlightDecorationType = this.createTextEditorDecorationType();
this._highlightManager = new HighlightManager();
this._highlightTreeDataProvider = new HighlightTreeDataProvider(this._highlightManager.GetHighlightCollection.bind(this._highlightManager));
}
public intialize(context: vscode.ExtensionContext) {
this.log('Initializing line highlighter...');
context.subscriptions.push(
this._highlightManager.onHighlightChanged(this.onHighlightChangedHandler, this),
// @ts-ignore
vscode.window.onDidChangeVisibleTextEditors(this.onDidChangeVisibleTextEditorsHandler, this),
vscode.window.onDidChangeActiveTextEditor(this.onDidChangeActiveTextEditorHandler, this),
vscode.workspace.onDidChangeTextDocument(this.onDidChangeTextDocumentHandler, this),
vscode.workspace.onDidChangeConfiguration(this.onDidChangeConfigurationHandler, this),
vscode.window.registerTreeDataProvider('twitchHighlighterTreeView-explorer', this._highlightTreeDataProvider),
vscode.window.registerTreeDataProvider('twitchHighlighterTreeView-debug', this._highlightTreeDataProvider),
vscode.window.registerTreeDataProvider('twitchHighlighterTreeView', this._highlightTreeDataProvider),
vscode.commands.registerCommand(Commands.refreshTreeView, this.refreshTreeviewHandler, this),
vscode.commands.registerCommand(Commands.highlight, this.highlightHandler, this),
vscode.commands.registerCommand(Commands.unhighlight, this.unhighlightHandler, this),
vscode.commands.registerCommand(Commands.unhighlightSpecific, this.unhighlightSpecificHandler, this),
vscode.commands.registerCommand(Commands.unhighlightAll, this.unhighlightAllHandler, this),
vscode.commands.registerCommand(Commands.gotoHighlight, this.gotoHighlightHandler, this),
vscode.commands.registerCommand(Commands.requestHighlight, this.requestHighlightHandler, this),
vscode.commands.registerCommand(Commands.requestUnhighlight, this.requestUnhighlightHandler, this),
vscode.commands.registerCommand(Commands.requestUnhighlightAll, this.requestUnhighlightAllHandler, this),
vscode.commands.registerCommand(Commands.contextMenuUnhighlight, this.contextMenuUnhighlightHandler, this)
);
this.log('Initialized line highlighter.');
}
public API: HighlighterAPI = {
requestHighlight(service: string, userName: string, startLine: number, endLine?: number, comments?: string) {
vscode.commands.executeCommand(Commands.requestHighlight,
service,
userName,
startLine,
endLine,
comments
);
},
requestUnhighlight(service: string, userName: string, lineNumber: number) {
vscode.commands.executeCommand(Commands.requestUnhighlight,
service,
userName,
lineNumber
);
},
requestUnhighlightAll(service: string) {
vscode.commands.executeCommand(Commands.requestUnhighlightAll, service);
}
};
public async dispose() {
}
private onDidChangeTextDocumentHandler(event: vscode.TextDocumentChangeEvent): void {
if (event.document.languageId === 'Log') {
return;
}
// Determine if the change occured on a highlighted line, if it did then adjust the highlight.
event.contentChanges.forEach(valueChanged => {
this._highlightManager.UpdateHighlight(event.document, valueChanged);
});
// Determine if we changed the fileName of the currently active open document.
if (this.currentDocument && event.document.fileName !== this.currentDocument.fileName) {
this._highlightManager.Rename(this.currentDocument.fileName, event.document.fileName);
this.currentDocument = event.document;
}
}
private onDidChangeConfigurationHandler(event: vscode.ConfigurationChangeEvent): void {
if (!event.affectsConfiguration(Configuration.sectionIdentifier)) {
return;
}
this.config = vscode.workspace.getConfiguration(Configuration.sectionIdentifier);
this.highlightDecorationType = this.createTextEditorDecorationType();
this.refresh();
}
private onDidChangeVisibleTextEditorsHandler(editors: Array<vscode.TextEditor>): void {
if (editors.length > 0) {
editors.forEach(te => {
te.setDecorations(
this.highlightDecorationType,
this._highlightManager.GetDecorations(te.document.fileName)
);
});
}
}
/**
* Sets the 'editorHasHighlights' to true or false.
* The 'editorHasHighlights' context is used to determine if the
* 'Remove Highlight' and 'Remove All Highlights' context menu items
* are visible or not.
*/
private setEditorHasHighlightsContext() {
if (vscode.window.activeTextEditor) {
const editor = vscode.window.activeTextEditor;
if (this._highlightManager.GetDecorations(editor.document.fileName).length > 0) {
vscode.commands.executeCommand('setContext', AppContexts.editorHasHighlights, true);
} else {
vscode.commands.executeCommand('setContext', AppContexts.editorHasHighlights, false);
}
}
}
private onDidChangeActiveTextEditorHandler(editor?: vscode.TextEditor): void {
if (editor) {
this.currentDocument = editor.document;
}
else {
this.currentDocument = undefined;
}
this.setEditorHasHighlightsContext();
}
private refreshTreeviewHandler(): void {
this._highlightTreeDataProvider.refresh();
}
private createTextEditorDecorationType(): vscode.TextEditorDecorationType {
const configuration = vscode.workspace.getConfiguration(Configuration.sectionIdentifier);
if (this.highlightDecorationType) {
this.highlightDecorationType.dispose();
}
return vscode.window.createTextEditorDecorationType({
backgroundColor: configuration.get<string>(Settings.highlightColor) || 'green',
border: configuration.get<string>(Settings.highlightBorder) || '2px solid white',
color: configuration.get<string>(Settings.highlightFontColor) || 'white'
});
}
private refresh(): void {
this.setEditorHasHighlightsContext();
vscode.window.visibleTextEditors.forEach(te => {
te.setDecorations(
this.highlightDecorationType,
this._highlightManager.GetDecorations(te.document.fileName)
);
});
this._highlightTreeDataProvider.refresh();
}
private onHighlightChangedHandler(): void {
this.refresh();
}
private get isActiveTextEditor(): boolean {
const editor = vscode.window.activeTextEditor;
return (editor !== undefined &&
editor.document.languageId !== 'log' &&
editor.document.getText().length > 0);
}
private async highlightHandler(): Promise<void> {
const editor = vscode.window.activeTextEditor;
if (!this.isActiveTextEditor) {
vscode.window.showInformationMessage('The current open, and active text editor is either empty or not a valid target to highlight a line.');
return;
}
try {
const options: vscode.InputBoxOptions = {
ignoreFocusOut: true,
prompt: "Enter a line number"
};
const value = await vscode.window.showInputBox(options);
if (value) {
this._highlightManager.Add(editor!.document, 'self', +(value || 0));
}
}
catch (err) {
this.log(LogLevel.Error, err);
}
}
private async unhighlightHandler(treeItem?: HighlightTreeItem): Promise<void> {
if (treeItem) {
const fileName = treeItem.fileName;
const highlightLines = treeItem.highlights.map(h => h.startLine);
highlightLines.forEach(line => {
this._highlightManager.Remove(fileName, 'self', line, true);
});
this._highlightManager.Refresh();
}
else {
try {
const options: vscode.QuickPickOptions = {
ignoreFocusOut: true
};
const value = await vscode.window.showQuickPick(this._highlightManager.GetHighlightDetails(), options);
if (value) {
const fileNameAndLineNumber = value.split(": ");
const fileName = fileNameAndLineNumber[0];
const lineNumber = fileNameAndLineNumber[1];
this._highlightManager.Remove(fileName, 'self', +(lineNumber));
}
}
catch (err) {
this.log(LogLevel.Error, err);
}
}
}
private async unhighlightSpecificHandler(): Promise<void> {
if (this._highlightManager.GetHighlightCollection().length === 0) {
vscode.window.showInformationMessage(
'There are no highlights to unhighlight'
);
return;
}
let pickerOptions: Array<string> = new Array<string>();
const highlights = this._highlightManager.GetHighlightDetails();
highlights.forEach(highlight => {
pickerOptions = [...pickerOptions, highlight];
});
try {
const pickedOption = await vscode.window.showQuickPick(pickerOptions);
if (!pickedOption) {
this.log('A valid highlight was not selected.');
return;
}
const [pickedFile, lineNumber] = pickedOption.split(': ');
this._highlightManager.Remove(pickedFile, 'self', +(lineNumber));
}
catch (err) {
this.log(LogLevel.Error, err);
}
}
private unhighlightAllHandler(): void {
this._highlightManager.Clear();
}
private async gotoHighlightHandler(lineNumber: number, fileName: string): Promise<void> {
const document = await vscode.workspace.openTextDocument(fileName);
if (document) {
vscode.window.showTextDocument(document).then(editor => {
lineNumber = lineNumber < 3 ? 2 : lineNumber;
editor.revealRange(document.lineAt(lineNumber - 2).range);
});
}
}
private requestHighlightHandler(service: string, userName: string, startLine: number, endLine?: number, comments?: string): void {
const editor = vscode.window.activeTextEditor;
if (!this.isActiveTextEditor) {
this.log(LogLevel.Warning, `Could not highlight the line requested by ${service}:${userName}`);
this.log(LogLevel.Warning, 'The current open, and active text editor is either empty or not a valid target to highlight a line.');
return;
}
this._highlightManager.Add(editor!.document, `${service}:${userName}`, startLine, endLine || startLine, comments);
}
private requestUnhighlightHandler(service: string, userName: string, lineNumber: number): void {
const editor = vscode.window.activeTextEditor;
if (!this.isActiveTextEditor) {
this.log(LogLevel.Warning, `Could not unhighlight the line requested by ${service}:${userName}`);
this.log(LogLevel.Warning, 'The current open, and active text editor is either empty or not a valid target to highlight a line.');
return;
}
this._highlightManager.Remove(editor!.document, `${service}:${userName}`, lineNumber);
}
private requestUnhighlightAllHandler(service: string): void {
this._highlightManager.Clear(service);
}
private contextMenuUnhighlightHandler() {
if (vscode.window.activeTextEditor) {
const lineNumber = vscode.window.activeTextEditor.selection.active.line;
this._highlightManager.Remove(vscode.window.activeTextEditor.document, 'self', lineNumber + 1);
}
}
}
================================================
FILE: src/common/index.ts
================================================
export * from './keytar';
================================================
FILE: src/common/keytar.ts
================================================
import * as keytartype from 'keytar';
import { getNodeModule } from '../utils';
export const keytar: typeof keytartype | undefined = getNodeModule<typeof keytartype>('keytar');
================================================
FILE: src/constants.ts
================================================
export const extensionId = 'clarkio.twitch-highlighter';
export const extSuffix = 'twitchHighlighter';
================================================
FILE: src/credentialManager.ts
================================================
import * as keytartype from 'keytar';
import { env } from 'vscode';
declare const __webpack_require__: typeof require;
declare const __non_webpack_require__: typeof require;
function getNodeModule<T>(moduleName: string): T | undefined {
const r = typeof __webpack_require__ === 'function' ? __non_webpack_require__ : require;
try {
return r(`${env.appRoot}/node_modules.asar/${moduleName}`);
}
catch (err) {
// Not in ASAR.
}
try {
return r(`${env.appRoot}/node_modules/${moduleName}`);
}
catch (err) {
// Not available
}
return undefined;
}
export class CredentialManager {
private static service: string = "vscode-twitch-highlighter";
private static clientIdIdentifier: string = "twitchClientId";
private static passwordIdentifier: string = "twitchToken";
private static keytar: typeof keytartype | undefined = getNodeModule<typeof keytartype>('keytar');
/**
* @deprecated Included only so people can remove their previous Client ID.
*/
public static async deleteTwitchClientId(): Promise<boolean> {
if (CredentialManager.keytar) {
return await CredentialManager.keytar.deletePassword(CredentialManager.service, CredentialManager.clientIdIdentifier);
}
return false;
}
public static async setPassword(value: string): Promise<void> {
if (CredentialManager.keytar && value !== null) {
await CredentialManager.keytar.setPassword(CredentialManager.service, CredentialManager.passwordIdentifier, value);
}
}
public static async deleteTwitchToken(): Promise<boolean> {
if (CredentialManager.keytar) {
return await CredentialManager.keytar.deletePassword(CredentialManager.service, CredentialManager.passwordIdentifier);
}
return false;
}
public static getTwitchToken(): Promise<string | null> {
return new Promise<string | null>(async resolve => {
const password = await CredentialManager.getPassword(CredentialManager.passwordIdentifier);
resolve(password);
});
}
private static async getPassword(account: string): Promise<string | null> {
if (CredentialManager.keytar) {
return await CredentialManager.keytar.getPassword(CredentialManager.service, account);
}
return null;
}
}
export default CredentialManager;
================================================
FILE: src/enums/AppContexts.ts
================================================
export enum AppContexts {
'editorHasHighlights' = 'editorHasHighlights'
}
================================================
FILE: src/enums/Commands.ts
================================================
// export enum Commands {
// 'highlight' = 'twitchHighlighter.highlight',
// 'unhighlight' = 'twitchHighlighter.unhighlight',
// 'unhighlightAll' = 'twitchHighlighter.unhighlightAll',
// 'unhighlightSpecific' = 'twitchHighlighter.unhighlightSpecific',
// 'startChat' = 'twitchHighlighter.startChat',
// 'stopChat' = 'twitchHighlighter.stopChat',
// 'toggleChat' = 'twitchHighlighter.toggleChat',
// 'removeTwitchClientId' = 'twitchHighlighter.removeTwitchClientId',
// 'setTwitchToken' = 'twitchHighlighter.setTwitchToken',
// 'removeTwitchToken' = 'twitchHighlighter.removeTwitchToken',
// 'refreshTreeView' = 'twitchHighlighter.refreshTreeView',
// 'gotoHighlight' = 'twitchHighlighter.gotoHighlight',
// 'removeHighlight' = 'twitchHighlighter.removeHighlight',
// 'requestHighlight' = 'twitchHighlighter.requestHighlight',
// 'requestUnhighlight' = 'twitchHighlighter.requestUnhighlight',
// 'requestUnhighlightAll' = 'twitchHighlighter.requestUnhighlightAll',
// 'signIn' = 'twitchHighlighter.signIn',
// 'signOut' = 'twitchHighlighter.signOut',
// 'connect' = 'twitchHighlighter.connect',
// 'disconnect' = 'twitchHighlighter.disconnect'
// }
export enum Commands {
'refreshTreeView' = 'twitchHighlighter.refreshTreeView',
'highlight' = 'twitchHighlighter.highlight',
'unhighlight' = 'twitchHighlighter.unhighlight',
'unhighlightSpecific' = 'twitchHighlighter.unhighlightSpecific',
'unhighlightAll' = 'twitchHighlighter.unhighlightAll',
'gotoHighlight' = 'twitchHighlighter.gotoHighlight',
'requestHighlight' = 'twitchHighlighter.requestHighlight',
'requestUnhighlight' = 'twitchHighlighter.requestUnhighlight',
'requestUnhighlightAll' = 'twitchHighlighter.requestUnhighlightAll',
'signIn' = 'twitchHighlighter.signIn',
'signOut' = 'twitchHighlighter.signOut',
'connect' = 'twitchHighlighter.connect',
'disconnect' = 'twitchHighlighter.disconnect',
'contextMenuUnhighlight' = 'twitchHighlighter.context.unhighlight'
}
================================================
FILE: src/enums/Configuration.ts
================================================
export enum Configuration {
"sectionIdentifier" = "twitchHighlighter",
"highlightBackgroundColor" = "highlightBackgroundColor",
"highlightForegroundColor" = "highlightForegroundColor",
"highlightBorderStyle" = "highlightBorderStyle",
"showActivityBar" = "showActivityBar"
}
================================================
FILE: src/enums/InternalCommands.ts
================================================
export enum InternalCommands {
'removeBannedHighlights' = 'twitchHighlighter.removeBannedHighlights'
}
================================================
FILE: src/enums/KeytarKeys.ts
================================================
export enum KeytarKeys {
"service" = "vscode-twitch-highlighter-ttvchat",
"account" = "vscode-twitch-highlighter-ttvchat.account",
"userId" = "vscode-twitch-highlighter-ttvchat.userId",
"userLogin" = "vscode-twitch-highlighter-ttvchat.userLogin"
}
================================================
FILE: src/enums/LogLevel.ts
================================================
export enum LogLevel {
'Information' = 'info',
'Warning' = 'warn',
'Error' = 'error',
'Debug' = 'debug'
}
================================================
FILE: src/enums/Settings.ts
================================================
export enum Settings {
'channels' = 'channels',
'username' = 'nickname',
'highlightColor' = 'highlightColor',
'highlightBorder' = 'highlightBorder',
'highlightFontColor' = 'highlightFontColor',
'announceBot' = 'announceBot',
'joinMessage' = 'joinMessage',
'leaveMessage' = 'leaveMessage',
'unhighlightOnDisconnect' = 'unhighlightOnDisconnect',
'showHighlightsInActivityBar' = 'showHighlightsInActivityBar',
'usageTip' = 'usageTip',
'requiredBadges' = 'requiredBadges'
}
================================================
FILE: src/enums/TwitchKeys.ts
================================================
export enum TwitchKeys {
"clientId" = "83juwb58ggj9s7l7nf9ngyill70tem",
"scope" = "chat:read chat:edit",
}
================================================
FILE: src/enums/index.ts
================================================
export * from './Commands';
export * from './InternalCommands';
export * from './Settings';
export * from './LogLevel';
export * from './Configuration';
export * from './KeytarKeys';
export * from './TwitchKeys';
export * from './AppContexts';
================================================
FILE: src/extension.ts
================================================
'use strict';
// The module 'vscode' contains the VS Code extensibility API
// Import the module and reference it with the alias vscode in your code below
import * as vscode from 'vscode';
import { CredentialManager } from './credentialManager';
import { App } from './app';
import { TwitchChatService } from './ttvchat';
let app: App;
let ttvchat: TwitchChatService;
// this method is called when your extension is activated
// your extension is activated the very first time the command is executed
export function activate(context: vscode.ExtensionContext) {
// Remove the older credentials if they exist.
CredentialManager.deleteTwitchToken();
CredentialManager.deleteTwitchClientId();
const outputChannel = vscode.window.createOutputChannel('Twitch Line Highlighter');
app = new App(outputChannel);
ttvchat = new TwitchChatService(app.API, outputChannel);
app.intialize(context);
ttvchat.initialize(context);
return app.API;
}
export function deactivate() {
ttvchat.dispose();
}
export const editorHasDecorations = () => {
return true;
}
================================================
FILE: src/highlight/Highlight.ts
================================================
import { Range } from 'vscode';
export class Highlight {
private _userName: string;
private _range: Range;
private _comments?: string;
constructor(userName: string, range: Range, comments?: string) {
this._userName = userName;
this._range = range;
this._comments = comments;
}
public get range(): Range {
return this._range;
}
public get userName(): string {
return this._userName;
}
// The textEditor start line is zero indexed.
// We adjust to match the editors line numbers as shown to the user.
public get startLine(): number {
return this._range.start.line + 1;
}
// The textEditor end line is zero indexed.
// We adjust to match the editors line numbers as shown to the user.
public get endLine(): number {
return this._range.end.line + 1;
}
public get comments(): string | undefined {
return this._comments;
}
// The range should only be updated by the app
// when changes occur in the TextEditor, such as a newline
// is added above this highlight range.
public Update(newRange: Range): void {
this._range = newRange;
}
}
================================================
FILE: src/highlight/HighlightManager.ts
================================================
import {
TextDocument,
Range,
Position,
EventEmitter,
Event,
TextDocumentContentChangeEvent,
DecorationOptions
} from "vscode";
import { isString } from 'util';
import { Highlight } from './Highlight';
export interface HighlightCollection {
fileName: string;
highlights: Array<Highlight>;
}
export interface HighlightChangedEvent {
}
export class HighlightManager {
private readonly _onHighlightsChanged: EventEmitter<HighlightChangedEvent> = new EventEmitter();
private highlightCollection: Array<HighlightCollection> = [];
public get onHighlightChanged(): Event<HighlightChangedEvent> {
return this._onHighlightsChanged.event;
}
public GetHighlightCollection(): Array<HighlightCollection> {
return this.highlightCollection;
}
public GetHighlightDetails(): string[] {
if (this.highlightCollection.length > 0) {
return this.highlightCollection
.map(hc => hc.highlights.map(h => `${hc.fileName}: ${h.startLine}`))
.reduce(s => s)
.sort((hA, hB) => hB.localeCompare(hA));
}
return [];
}
public GetDecorations(fileName: string): DecorationOptions[] {
const idx = this.highlightCollection.findIndex(hc => hc.fileName === fileName);
if (idx > -1) {
return this.highlightCollection[idx].highlights.map<DecorationOptions>(h => {
return {
hoverMessage: `From ${h.userName === 'self'? 'You' : h.userName} ${h.comments !== undefined ? h.comments : ''}`,
range: h.range
};
});
}
return [];
}
public Add(document: TextDocument, userName: string, startLine: number, endLine?: number, comments?: string): Promise<void> {
return new Promise(resolve => {
if (!endLine) {
endLine = startLine;
}
const range = new Range(
new Position(--startLine, 0),
new Position(--endLine, document.lineAt(endLine).text.length)
);
const highlight = new Highlight(userName, range, comments);
const idx = this.highlightCollection.findIndex(h => h.fileName === document.fileName);
if (idx > -1) {
if (!this.highlightCollection[idx].highlights.some(h => (h.userName === userName || userName === 'self') && h.startLine <= startLine && h.endLine >= endLine!)) {
this.highlightCollection[idx].highlights.push(highlight);
}
}
else {
this.highlightCollection.push({
fileName: document.fileName,
highlights: [highlight]
});
}
// @ts-ignore
this._onHighlightsChanged.fire();
resolve();
});
}
public Remove(document: TextDocument, userName: string, lineNumber: number, deferRefresh?: boolean): Promise<void>;
public Remove(fileName: string, userName: string, lineNumber: number, deferRefresh?: boolean): Promise<void>;
public Remove(documentOrFileName: TextDocument | string, userName: string, lineNumber: number, deferRefresh: boolean = false): Promise<void> {
return new Promise<void>(resolve =>{
if (!isString(documentOrFileName)) {
documentOrFileName = documentOrFileName.fileName;
}
const idx = this.highlightCollection.findIndex(h => h.fileName === documentOrFileName);
if (idx > -1) {
const hidx = this.highlightCollection[idx].highlights.findIndex(h => (h.userName === userName || userName === 'self') && h.startLine <= lineNumber && h.endLine >= lineNumber);
if (hidx > -1) {
this.highlightCollection[idx].highlights.splice(hidx, 1);
}
if (!deferRefresh) {
// @ts-ignore
this._onHighlightsChanged.fire();
}
}
resolve();
});
}
public Refresh() {
// @ts-ignore
this._onHighlightsChanged.fire();
}
public Clear(service?: string): Promise<void> {
return new Promise<void>(resolve =>{
if (service) {
this.highlightCollection.forEach(hc => {
const highlightsToRemove = hc.highlights.filter(h => h.userName.indexOf(`${service}:`) > -1);
highlightsToRemove.forEach(h => {
this.Remove(hc.fileName, h.userName, h.startLine, true);
});
});
}
else {
this.highlightCollection = new Array<HighlightCollection>();
}
// @ts-ignore
this._onHighlightsChanged.fire();
});
}
public Rename(oldName: string, newName: string) {
const idx = this.highlightCollection.findIndex(hc => hc.fileName === oldName);
if (idx > -1) {
this.highlightCollection[idx].fileName = newName;
}
}
public UpdateHighlight(document: TextDocument, valueChanged: TextDocumentContentChangeEvent) {
const idx = this.highlightCollection.findIndex(hc => hc.fileName === document.fileName);
let updated = false;
if (idx > -1) {
// A carriage return was removed.
if (valueChanged.text.length === 0 && valueChanged.range.end.line === valueChanged.range.start.line + 1) {
let highlights = this.highlightCollection[idx].highlights.filter(h => h.range.start.line > valueChanged.range.end.line);
highlights.forEach(highlight => {
highlight.Update(new Range(
new Position(highlight.range.start.line - 1, highlight.range.start.character),
new Position(highlight.range.end.line, highlight.range.end.character)
));
updated = true;
});
highlights = this.highlightCollection[idx].highlights.filter(h => h.range.end.line >= valueChanged.range.end.line);
highlights.forEach(highlight => {
highlight.Update(new Range(
new Position(highlight.range.start.line, highlight.range.start.character),
new Position(highlight.range.end.line - 1, highlight.range.end.character)
));
updated = true;
});
}
else if (valueChanged.text.match('\n')) {
let highlights = this.highlightCollection[idx].highlights.filter(h => h.range.end.line >= valueChanged.range.start.line);
highlights.forEach(highlight => {
highlight.Update(new Range(
new Position(highlight.range.start.line, highlight.range.start.character),
new Position(highlight.range.end.line + 1, highlight.range.end.character)
));
updated = true;
});
highlights = this.highlightCollection[idx].highlights.filter(h => h.range.start.line > valueChanged.range.start.line);
highlights.forEach(highlight => {
highlight.Update(new Range(
new Position(highlight.range.start.line + 1, highlight.range.start.character),
new Position(highlight.range.end.line, highlight.range.end.character)
));
updated = true;
});
}
else {
const highlights = this.highlightCollection[idx].highlights.filter(h => h.range.contains(valueChanged.range));
highlights.forEach(h => {
if (valueChanged.text.length === 0) { // A character was deleted.
h.Update(new Range(
new Position(h.range.start.line, h.range.start.character),
new Position(h.range.end.line, h.range.end.character - 1)
));
updated = true;
}
else {
h.Update(new Range(
new Position(h.range.start.line, h.range.start.character),
new Position(h.range.end.line, h.range.end.character + valueChanged.text.length)
));
updated = true;
}
});
}
}
if (updated) {
// @ts-ignore
this._onHighlightsChanged.fire();
}
}
}
================================================
FILE: src/highlight/index.ts
================================================
export * from './Highlight';
export * from './HighlightManager';
export * from './treeView';
================================================
FILE: src/highlight/treeView/HighlightTreeDataProvider.ts
================================================
import {
TreeDataProvider,
EventEmitter,
Event,
TreeItem,
TreeItemCollapsibleState
} from "vscode";
import{ basename } from 'path';
import { HighlightTreeItem } from "./HighlightTreeItem";
import { HighlightCollection } from "../HighlightManager";
import { naturalCompare } from '../../utils';
export class HighlightTreeDataProvider implements TreeDataProvider<HighlightTreeItem> {
private readonly _onDidChangeTreeData: EventEmitter<HighlightTreeItem | undefined>;
constructor(private getHighlightCollections = (): HighlightCollection[] => []) {
this._onDidChangeTreeData = new EventEmitter();
}
public get onDidChangeTreeData(): Event<HighlightTreeItem | undefined> {
return this._onDidChangeTreeData.event;
}
public refresh(): void {
// @ts-ignore
this._onDidChangeTreeData.fire();
}
public getTreeItem(element: HighlightTreeItem): TreeItem {
return element;
}
public getChildren(element?: HighlightTreeItem): Thenable<HighlightTreeItem[]> {
if (element) {
return Promise.resolve(element.HighlightTreeItems.sort((highlightA, highlightB) => naturalCompare(highlightA.label, highlightB.label)));
}
let highlightTreeItems = new Array<HighlightTreeItem>();
const currentHighlightCollections = this.getHighlightCollections().filter(hc => hc.highlights.length > 0);
currentHighlightCollections.forEach(hc => {
const highlights = hc.highlights;
const label = basename(hc.fileName);
highlightTreeItems.push(new HighlightTreeItem(label, hc.fileName, highlights, TreeItemCollapsibleState.Expanded));
});
highlightTreeItems = highlightTreeItems
.sort((highlightTreeItemA, highlightTreeItemB) => highlightTreeItemB.label.localeCompare(highlightTreeItemA.label));
return Promise.resolve(highlightTreeItems.sort((highlightTreeItemA, highlightTreeItemB) => naturalCompare(highlightTreeItemA.label, highlightTreeItemB.label)));
}
}
================================================
FILE: src/highlight/treeView/HighlightTreeItem.ts
================================================
import { TreeItem, TreeItemCollapsibleState, Command } from "vscode";
import { Highlight } from "../Highlight";
import { Commands } from "../../enums";
export class HighlightTreeItem extends TreeItem {
constructor(
public readonly label: string,
public readonly fileName: string,
public readonly highlights: Highlight[] = [],
public readonly collapsibleState: TreeItemCollapsibleState,
public readonly command?: Command
) {
super(label, collapsibleState);
}
// @ts-ignore
public get description(): string {
if (this.highlights.length > 0) {
return `Highlights: ${this.highlights.length}`;
}
return '';
}
public get HighlightTreeItems(): HighlightTreeItem[] {
const children = new Array<HighlightTreeItem>();
this.highlights.forEach(highlight => {
const label = `Line: ${highlight.endLine > highlight.startLine ? `${highlight.startLine} - ${highlight.endLine}` : `${highlight.startLine}`}`;
const existingItem = children.find(item => item.label === label);
if (existingItem) {
existingItem.highlights.push(highlight);
}
else {
const command: Command = {
command: Commands.gotoHighlight,
title: '',
arguments: [highlight.startLine, this.fileName]
};
children.push(new HighlightTreeItem(label, this.fileName, [highlight], TreeItemCollapsibleState.None, command));
}
});
return children;
}
contextValue = 'highlightTreeItem';
}
================================================
FILE: src/highlight/treeView/index.ts
================================================
export * from './HighlightTreeItem';
export * from './HighlightTreeDataProvider';
================================================
FILE: src/index.ts
================================================
export * from './api';
================================================
FILE: src/logger.ts
================================================
import { OutputChannel } from 'vscode';
import { LogLevel } from './enums';
import { isEnum } from './utils';
export type log = {
(message: string, ...optionalParams: any[]): void;
(level: LogLevel, message?: string, ...optionalParams: any[]): void;
};
export class Logger {
private readonly _channel?: OutputChannel;
constructor(outputChannel?: OutputChannel, thisArgs?: any) {
this._channel = outputChannel;
this.log = this.log.bind(thisArgs || this);
}
public log(message: string, ...optionalParams: any[]): void;
public log(levelOrMessage: LogLevel | string, message?: string, ...optionalParams: any[]): void {
const captains: any = console;
let level;
if (isEnum(levelOrMessage, LogLevel)) {
level = levelOrMessage;
}
else {
level = LogLevel.Information;
message = levelOrMessage;
}
const getTime = (): {
hours: string,
minutes: string,
seconds: string
} => {
const date = new Date();
const hours = date.getHours();
const minutes = date.getMinutes();
const seconds = date.getSeconds();
const prefix = (value: number): string => {
return value < 10 ? `0${value}` : `${value}`;
};
return {
hours: prefix(hours),
minutes: prefix(minutes),
seconds: prefix(seconds)
};
};
const { hours, minutes, seconds } = getTime();
const log = `[${hours}:${minutes}:${seconds}] ${message}`;
captains[level](log, ...optionalParams);
if (this._channel && level !== LogLevel.Debug) {
this._channel.appendLine(log);
}
}
}
================================================
FILE: src/test/runTest.ts
================================================
import * as path from "path";
import { runTests } from "@vscode/test-electron";
async function main() {
try {
// The folder containing the Extension Manifest package.json
// Passed to `--extensionDevelopmentPath`
const extensionDevelopmentPath = path.resolve(__dirname, "../../");
// The path to the extension test runner script
// Passed to --extensionTestsPath
const extensionTestsPath = path.resolve(__dirname, "./suite/index");
// Download VS Code, unzip it and run the integration test
await runTests({ extensionDevelopmentPath, extensionTestsPath });
} catch (err) {
console.error(err);
console.error("Failed to run tests");
process.exit(1);
}
}
main();
================================================
FILE: src/test/suite/extension.test.ts
================================================
import { extensionId, extSuffix } from '../../constants';
import { Settings } from "../../enums";
import { Commands } from "../../enums";
//
// 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 '../extension';
interface ICommand {
title: string;
command: string;
category: string;
}
interface IConfiguration {
type: string;
title: string;
properties: any;
}
export type EnumIndexer = {
[Key: string]: string
};
// Defines a Mocha test suite to group tests of similar kind together
suite("Extension Tests", function () {
let extension: vscode.Extension<any>;
setup(function () {
const ext = vscode.extensions.getExtension(extensionId);
if (!ext) {
throw new Error('Extension was not found.');
}
if (ext) { extension = ext; }
});
/**
* Because we are waiting on a process to complete in the background
* we use the `done` function to inform mocha that this test run is
* complete.
*/
test("Extension loads in VSCode and is active", function (done) {
// Hopefully a 200ms timeout will allow the extension to activate within Windows
// otherwise we get a false result.
setTimeout(function () {
assert.equal(extension.isActive, true);
done();
}, 200);
});
test("constants.Commands exist in package.json", function () {
const commandCollection: ICommand[] = extension.packageJSON.contributes.commands;
for (let command in Commands) {
const result = commandCollection.some(c => c.command === (Commands as any)[command]);
assert.ok(result);
}
});
test("constants.Settings exist in package.json", function () {
const config: IConfiguration = extension.packageJSON.contributes.configuration;
const properties = Object.keys(config.properties);
for (let setting in Settings) {
const result = properties.some(property => property === `${extSuffix}.${(Settings as any)[setting]}`);
assert.ok(result);
}
});
test('package.json commands registered in extension', function (done) {
const commandStrings: string[] = extension.packageJSON.contributes.commands.map((c: ICommand) => c.command);
vscode.commands.getCommands(true)
.then((allCommands: string[]) => {
const commands: string[] = allCommands.filter(c => c.startsWith(`${extSuffix}.`));
commands.forEach(command => {
const result = commandStrings.some(c => c === command);
assert.ok(result);
});
done();
});
});
});
================================================
FILE: src/test/suite/index.ts
================================================
import * as path from "path";
import * as Mocha from "mocha";
import * as glob from "glob";
export function run(): Promise<void> {
// Create the mocha test
const mocha = new Mocha({
ui: "tdd",
});
mocha.useColors(true);
const testsRoot = path.resolve(__dirname, "..");
return new Promise((c, e) => {
glob("**/**.test.js", { cwd: testsRoot }, (err, files) => {
if (err) {
return e(err);
}
// Add files to the test suite
files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f)));
try {
// Run the mocha test
mocha.run((failures) => {
if (failures > 0) {
e(new Error(`${failures} tests failed.`));
} else {
c();
}
});
} catch (err) {
console.error(err);
e(err);
}
});
});
}
================================================
FILE: src/test/suite/utils.test.ts
================================================
import * as assert from 'assert';
import { parseMessage } from '../../utils';
interface Theory {
message: string;
startLine: number;
endLine: number;
fileName?: string;
comment?: string;
}
suite('Utils Tests', function() {
test('Ensure parseMessage returns expected results', () => {
const theories: Theory[] = [
{
message: '!line 5',
startLine: 5,
endLine: 5
},
{
message: '!line settings.js 5',
startLine: 5,
endLine: 5,
fileName: 'settings.js'
},
{
message: '!line settings 5',
startLine: 5,
endLine: 5,
fileName: 'settings'
},
{
message: '!line 5 settings.js',
startLine: 5,
endLine: 5,
fileName: 'settings.js'
},
{
message: '!line 5 settings',
startLine: 5,
endLine: 5,
comment: 'settings'
},
{
message: '!line 5-15',
startLine: 5,
endLine: 15
},
{
message: '!line 5-15 comment',
startLine: 5,
endLine: 15,
comment: 'comment'
},
{
message: '!line settings.js 5-15 comment',
startLine: 5,
endLine: 15,
fileName: 'settings.js',
comment: 'comment'
},
{
message: '!line 5-15 settings.js comment',
startLine: 5,
endLine: 15,
fileName: 'settings.js',
comment: 'comment'
}
];
theories.forEach(({message, startLine, endLine, fileName, comment}) => {
const result = parseMessage(message);
assert.ok(result);
if (result) {
assert.equal(result.startLine, startLine);
assert.equal(result.endLine, endLine);
assert.equal(result.fileName, fileName);
assert.equal(result.comments, comment);
}
});
});
});
================================================
FILE: src/ttvchat/AuthenticationService.ts
================================================
import { readFileSync } from 'fs';
import * as http from 'http';
import * as path from 'path';
import * as url from 'url';
import { v4 } from 'uuid';
import { env, Event, EventEmitter, extensions, Uri, window } from 'vscode';
import { keytar } from '../common';
import { extensionId } from '../constants';
import { KeytarKeys, LogLevel, TwitchKeys } from '../enums';
import { log } from '../logger';
import { API } from './api';
export class AuthenticationService {
private readonly _onAuthStatusChanged: EventEmitter<boolean> = new EventEmitter();
public readonly onAuthStatusChanged: Event<boolean> = this._onAuthStatusChanged.event;
private port: number = 5001;
constructor(private log: log) { }
public async initialize() {
if (keytar) {
const accessToken = await keytar.getPassword(KeytarKeys.service, KeytarKeys.account);
const userLogin = await keytar.getPassword(KeytarKeys.service, KeytarKeys.userLogin);
if (accessToken && userLogin) {
// Twitch access tokens should be validated on a recurring interval.
await this.validateToken(accessToken);
return;
}
}
this._onAuthStatusChanged.fire(false);
}
// https://dev.twitch.tv/docs/authentication#validating-requests
public async validateToken(accessToken: string) {
await API.validateToken(accessToken);
this._onAuthStatusChanged.fire(true);
this.log("Twitch access token has been validated.");
const hour = 1000 * 60 * 60;
setInterval(this.validateToken, hour, accessToken); // Validate the token each hour
}
public async signInHandler() {
if (keytar) {
const accessToken = await keytar.getPassword(KeytarKeys.service, KeytarKeys.account);
if (!accessToken) {
const state = v4();
this.createServer(state);
env.openExternal(Uri.parse(`https://id.twitch.tv/oauth2/authorize?client_id=${TwitchKeys.clientId}` +
`&redirect_uri=http://localhost:${this.port}` +
`&response_type=token&scope=${TwitchKeys.scope}` +
`&force_verify=true` +
`&state=${state}`));
}
else {
const validResult = await API.validateToken(accessToken);
if (validResult.valid) {
this._onAuthStatusChanged.fire(true);
}
}
}
}
public async signOutHandler() {
if (keytar) {
const token = await keytar.getPassword(KeytarKeys.service, KeytarKeys.account);
if (token) {
const revoked = await API.revokeToken(token);
if (revoked) {
window.showInformationMessage('Twitch token revoked successfully');
}
}
keytar.deletePassword(KeytarKeys.service, KeytarKeys.account);
keytar.deletePassword(KeytarKeys.service, KeytarKeys.userLogin);
}
this._onAuthStatusChanged.fire(false);
}
private createServer(state: string) {
const filePath = path.join(extensions.getExtension(extensionId)!.extensionPath, 'out', 'ttvchat', 'login', 'index.htm');
this.log(LogLevel.Debug, `Starting login server using filePath: ${filePath}.`);
const file = readFileSync(filePath);
if (file) {
const server = http.createServer(async (req: any, res: any) => {
const mReq = url.parse(req.url!, true);
const mReqPath = mReq.pathname;
if (mReqPath === '/') {
res.writeHead(200, { 'Content-Type': 'text/html', 'Cache-Control': 'no-cache' });
res.end(file);
}
else if (mReqPath === '/oauth') {
const q: any = mReq.query;
if (q.state !== state) {
window.showErrorMessage('Error while logging in. State mismatch error.');
await API.revokeToken(q.access_token);
this._onAuthStatusChanged.fire(false);
res.writeHead(500, 'Error while logging in. State mismatch error.');
res.end();
return;
}
const validationResult = await API.validateToken(q.access_token);
if (keytar && validationResult.valid) {
keytar.setPassword(KeytarKeys.service, KeytarKeys.account, q.access_token);
keytar.setPassword(KeytarKeys.service, KeytarKeys.userLogin, validationResult.login);
this._onAuthStatusChanged.fire(true);
}
res.writeHead(200, { 'Content-Type': 'text/html', 'Cache-Control': 'no-cache' });
res.end(file);
}
else if (mReqPath === '/complete') {
res.writeHead(200, { 'Content-Type': 'text/html', 'Cache-Control': 'no-cache' });
res.end(file);
setTimeout(() => server.close(), 3000);
}
else if (mReqPath === '/favicon.ico') {
res.writeHead(204);
res.end();
}
});
server.listen(this.port, (err: any) => {
this.log(LogLevel.Error, err);
});
}
}
}
================================================
FILE: src/ttvchat/ChatClient.ts
================================================
import {
Badges, ChatUserstate, Client,
Options
} from "tmi.js";
import {
ConfigurationChangeEvent, Disposable, Event, EventEmitter,
ExtensionContext, workspace,
WorkspaceConfiguration
} from 'vscode';
import { keytar } from "../common";
import { Configuration, KeytarKeys, LogLevel, Settings } from "../enums";
import { log } from '../logger';
import { API } from './api';
interface IBadges extends Badges {
[key: string]: string | undefined;
follower: string;
}
export interface ChatClientMessageReceivedEvent {
userState: ChatUserstate;
message: string;
}
export class ChatClient implements Disposable {
private readonly _onChatClientConnected: EventEmitter<boolean> = new EventEmitter();
private readonly _onChatClientMessageReceived: EventEmitter<ChatClientMessageReceivedEvent> = new EventEmitter();
public readonly onChatClientConnected: Event<boolean> = this._onChatClientConnected.event;
public readonly onChatClientMessageReceived: Event<ChatClientMessageReceivedEvent> = this._onChatClientMessageReceived.event;
private config?: WorkspaceConfiguration;
private client?: Client;
private channel: string = "";
private announceBot: boolean = true;
private joinMessage: string = "";
private leaveMessage: string = "";
private requiredBadges: string[] = [];
constructor(private log: log) { }
public initialize(context: ExtensionContext) {
this.config = workspace.getConfiguration(Configuration.sectionIdentifier);
this.announceBot = this.config.get<boolean>(Settings.announceBot) || true;
this.joinMessage = this.config.get<string>(Settings.joinMessage) || "";
this.leaveMessage = this.config.get<string>(Settings.leaveMessage) || "";
this.requiredBadges = this.config.get<string[]>(Settings.requiredBadges) || [];
context.subscriptions.push(workspace.onDidChangeConfiguration(this.onDidChangeConfigurationHandler, this));
}
public async connect() {
if (keytar && this.config && !this.isConnected) {
const accessToken = await keytar.getPassword(KeytarKeys.service, KeytarKeys.account);
const login = await keytar.getPassword(KeytarKeys.service, KeytarKeys.userLogin);
if (accessToken && login) {
this.channel = this.config.get<string>(Settings.channels) || login;
const opts: Options = {
identity: {
username: login,
password: accessToken
},
channels: this.channel.split(', ').map(c => c.trim())
};
this.client = Client(opts);
this.client.on('connected', this.onConnectedHandler.bind(this));
this.client.on('message', this.onMessageHandler.bind(this));
this.client.on('join', this.onJoinHandler.bind(this));
const status = await this.client.connect();
this._onChatClientConnected.fire(true);
return status;
}
}
}
public async disconnect() {
if (this.isConnected) {
if (this.announceBot && this.leaveMessage.length > 0) {
await this.sendMessage(this.leaveMessage);
}
if (this.client) {
await this.client.disconnect();
this.client = undefined;
}
this._onChatClientConnected.fire(false);
}
}
public async dispose() {
await this.disconnect();
}
private get isConnected(): boolean {
return this.client ? this.client.readyState() === 'OPEN' : false;
}
private async sendMessage(message: string) {
if (this.isConnected && this.client) {
await this.client.say(this.channel, message);
}
}
private async onJoinHandler(channel: string, username: string, self: boolean) {
if (self && this.client && this.announceBot && this.joinMessage.length > 0) {
this.log(`Joined channel: ${channel} as ${username}`);
await this.sendMessage(this.joinMessage);
}
}
private onConnectedHandler(address: string, port: number) {
this.log(`Connected chat client to ${address} port ${port}`);
}
private async onMessageHandler(channel: string, userState: ChatUserstate, message: string, self: boolean) {
this.log(`Received '${message}' from ${userState["display-name"]}`);
if (self) {
return;
}
if (!message) {
return;
}
const badges = userState.badges as IBadges || {};
badges.follower = await API.isUserFollowingChannel(userState.id!, channel) === true ? '1' : '0';
if (this.requiredBadges.length > 0 && !badges.broadcaster) {
// Check to ensure the user has a required badge
const canContinue = this.requiredBadges.some(badge => badges[badge] === '1');
// Bail if the user does not have the required badge
if (!canContinue) {
this.log(LogLevel.Warning, `${userState.username} does not have any of the required badges to use the highlight command.`);
return;
}
}
message = message.toLocaleLowerCase().trim();
if (message.startsWith('!line') || message.startsWith('!highlight')) {
// message = message.replace('!line', '').replace('!highlight', '').trim();
if (message.length === 0) {
this.sendMessage('💡 To use the !line command, use the following format: !line <number> --or-- multiple lines: !line <start>-<end> --or-- with a comment: !line <number> <comment>');
return;
}
this._onChatClientMessageReceived.fire({
userState,
message
});
}
}
private async onDidChangeConfigurationHandler(event: ConfigurationChangeEvent) {
if (event.affectsConfiguration(Configuration.sectionIdentifier) && this.isConnected) {
await this.disconnect();
await this.connect();
}
}
}
================================================
FILE: src/ttvchat/TwitchChatService.ts
================================================
import * as vscode from 'vscode';
import { Commands, Configuration, Settings } from '../enums';
import { HighlighterAPI } from '../index';
import { log, Logger } from '../logger';
import { parseMessage } from '../utils';
import { AuthenticationService } from './AuthenticationService';
import { ChatClient, ChatClientMessageReceivedEvent } from './ChatClient';
export class TwitchChatService implements vscode.Disposable {
private readonly _api: HighlighterAPI;
private readonly _authenticationService: AuthenticationService;
private log: log;
private loginStatusBarItem: vscode.StatusBarItem;
private chatClientStatusBarItem: vscode.StatusBarItem;
private chatClient: ChatClient;
private config?: vscode.WorkspaceConfiguration;
constructor(api: HighlighterAPI, outputChannel: vscode.OutputChannel) {
this.log = new Logger(outputChannel).log;
this.chatClient = new ChatClient(this.log);
this.chatClient.disconnect.bind(this.chatClient);
this.config = vscode.workspace.getConfiguration(Configuration.sectionIdentifier);
this._api = api;
this._authenticationService = new AuthenticationService(this.log);
this._authenticationService.signOutHandler.bind(this._authenticationService);
this.loginStatusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left);
this.loginStatusBarItem.text = `$(sign-in) Twitch`;
this.loginStatusBarItem.command = Commands.signIn;
this.loginStatusBarItem.tooltip = 'Twitch Line Highlighter Login';
this.chatClientStatusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left);
this.chatClientStatusBarItem.text = `$(plug) Disconnected`;
this.chatClientStatusBarItem.command = Commands.connect;
this.chatClientStatusBarItem.tooltip = 'Twitch Line Highlighter Chat Bot';
}
public async initialize(context: vscode.ExtensionContext): Promise<void> {
this.log('ttvchat initializing...');
context.subscriptions.push(
this.loginStatusBarItem,
this.chatClientStatusBarItem,
vscode.workspace.onDidChangeConfiguration(this.onDidChangeConfigurationHandler, this),
this._authenticationService.onAuthStatusChanged(this.onAuthStatusChangedHandler, this),
this.chatClient.onChatClientConnected(this.onChatClientConnectedHandler, this),
this.chatClient.onChatClientMessageReceived(this.onChatClientMessageReceivedHandler, this),
vscode.commands.registerCommand(Commands.signIn, this._authenticationService.signInHandler, this._authenticationService),
vscode.commands.registerCommand(Commands.signOut, this.onSignOutHandler, this),
vscode.commands.registerCommand(Commands.connect, this.chatClient.connect, this.chatClient),
vscode.commands.registerCommand(Commands.disconnect, this.chatClient.disconnect, this.chatClient)
);
this.chatClient.initialize(context);
await this._authenticationService.initialize();
this.log('ttvchat initialized.');
}
private onDidChangeConfigurationHandler(event: vscode.ConfigurationChangeEvent) {
if (!event.affectsConfiguration(Configuration.sectionIdentifier)) {
return;
}
this.config = vscode.workspace.getConfiguration(Configuration.sectionIdentifier);
}
private onAuthStatusChangedHandler(signedIn: boolean) {
if (signedIn) {
this.loginStatusBarItem.hide();
this.chatClientStatusBarItem.show();
}
else {
this.chatClient.disconnect();
this.loginStatusBarItem.show();
this.chatClientStatusBarItem.hide();
}
}
private onChatClientConnectedHandler(isConnected: boolean) {
if (isConnected) {
this.chatClientStatusBarItem.text = '$(plug) Connected';
this.chatClientStatusBarItem.command = Commands.disconnect;
this.chatClientStatusBarItem.tooltip = 'Line Highlighter is connected to the chat room. Click to disconnect.';
}
else {
this.chatClientStatusBarItem.text = '$(plug) Disconnected';
this.chatClientStatusBarItem.command = Commands.connect;
this.chatClientStatusBarItem.tooltip = 'Line Highlighter is not connected to the chat room. Click to connect.';
const unhighlightOnDisconnect = this.config!.get<boolean>(Settings.unhighlightOnDisconnect) || false;
if (unhighlightOnDisconnect) {
this._api.requestUnhighlightAll('twitch');
}
}
}
private onChatClientMessageReceivedHandler(event: ChatClientMessageReceivedEvent) {
const userName = event.userState.username!;
const result = parseMessage(event.message);
if (result) {
if (result.highlight) {
this._api.requestHighlight('twitch', userName, result.startLine, result.endLine, result.comments);
}
else {
this._api.requestUnhighlight('twitch', userName, result.startLine);
}
}
}
private onSignOutHandler() {
// Disconnect from the chat
this.chatClient.disconnect();
// Sign out of Twitch
this._authenticationService.signOutHandler();
}
public async dispose() {
await this.chatClient.disconnect();
}
}
================================================
FILE: src/ttvchat/api/API.ts
================================================
import * as request from 'request';
import { TwitchKeys } from '../../enums';
export class API {
public static async isUserFollowingChannel(userId: string, channel: string) {
const url = `https://api.twitch.tv/helix/users/followers?from_id=${userId}&to_name=${channel}`;
const result = await new Promise<boolean>((resolve, reject) => {
request.get(url, {
headers: {
'Accept': 'application/json'
}
}, (err: any, response: any, body: any) => {
if (err) {
reject(err);
}
else {
if (response.statusCode === 200) {
const json = JSON.parse(body);
resolve(true);
}
else {
resolve(false);
}
}
});
});
return result;
}
public static async validateToken(token: string) {
const url = `https://id.twitch.tv/oauth2/validate`;
const result = await new Promise<{valid: boolean, login: string}>((resolve, reject) => {
request.get(url, {
headers: {
'Authorization': `OAuth ${token}`
}
}, (err: any, response: any, body: any) => {
if (err) {
reject(err);
}
else {
if (response.statusCode === 200) {
const json = JSON.parse(body);
resolve({valid: true, login: json.login});
}
else {
resolve({valid: false, login: ''});
}
}
});
});
return result;
}
public static async revokeToken(token: string) {
const url = `https://id.twitch.tv/oauth2/revoke?` +
`client_id=${TwitchKeys.clientId}` +
`&token=${token}`;
const result = await new Promise<boolean>((resolve, reject) => {
request.post(url, {auth: { 'bearer': token }}, (err, response) => {
if (err) {
reject(err);
}
else {
resolve(response.statusCode === 200);
}
});
});
return result;
}
}
================================================
FILE: src/ttvchat/api/index.ts
================================================
export * from './API';
================================================
FILE: src/ttvchat/index.ts
================================================
export * from './ChatClient';
export * from './TwitchChatService';
================================================
FILE: src/ttvchat/login/index.htm
================================================
<html>
<head>
<script type="text/javascript">
window.onload = function () {
if (window.location.href.includes("/#")) {
window.location.href = 'oauth?' + window.location.hash.replace('#', '');
} else if (window.location.href.includes("oauth?")) {
window.location.href = 'complete'
}
};
</script>
</head>
<body>
<h1>You can close the window</h1>
</body>
</html>
================================================
FILE: src/utils/getNodeModule.ts
================================================
import { env } from 'vscode';
import * as keytartype from 'keytar';
declare const __webpack_require__: typeof require;
declare const __non_webpack_require__: typeof require;
export const getNodeModule = <T>(moduleName: string): T | undefined => {
const r = typeof __webpack_require__ === 'function' ? __non_webpack_require__ : require;
try {
return r(`${env.appRoot}/node_modules.asar/${moduleName}`);
}
catch ( err ) {
// Not in ASAR
}
try {
return r(`${env.appRoot}/node_modules/${moduleName}`);
}
catch ( err ) {
// Not available
}
return undefined;
};
================================================
FILE: src/utils/index.ts
================================================
export * from './isEnum';
export * from './naturalCompare';
export * from './getNodeModule';
export * from './parseMessage';
================================================
FILE: src/utils/isEnum.ts
================================================
import { isString, isNumber } from "util";
export const isEnum = (value: any, Enum: {}): boolean => {
if (value === undefined || !isString(value) && !isNumber(value)) {
return false;
}
return Object.values(Enum).includes(value);
};
================================================
FILE: src/utils/naturalCompare.ts
================================================
export const naturalCompare = (a: any, b: any) => {
let ax: any[] = [], bx: any[] = [];
a.replace(/(\d+)|(\D+)/g, function (_: any, $1: any, $2: any) {
ax.push([$1 || Infinity, $2 || ""]);
});
b.replace(/(\d+)|(\D+)/g, function (_: any, $1: any, $2: any) {
bx.push([$1 || Infinity, $2 || ""]);
});
while (ax.length && bx.length) {
let an = ax.shift();
let bn = bx.shift();
let nn = (an[0] - bn[0]) || an[1].localeCompare(bn[1]);
if (nn) {
return nn;
}
}
return ax.length - bx.length;
};
================================================
FILE: src/utils/parseMessage.ts
================================================
export interface ParseMessageResult {
highlight: boolean;
startLine: number;
endLine: number;
fileName?: string;
comments?: string;
}
export const parseMessage = (message: string) => {
/**
* Regex pattern to verify the command is a highlight command
* groups the different sections of the command.
*
* See `https://regexr.com/48gf0` for my tests on the pattern.
*
* Matches:
*
* !line 5
* !line !5
* !line 5-10
* !line !5-15
* !line settings.json 5 | !line 5 settings.json
* !line settings.json !5 | !line !5 settings.json
* !line settings.json 5-15 | !line 5-15 settings.json
* !line settings.json !5-15 | !line !5-15 settings.json
* !line settings.json 5 including a comment | !line 5 settings.json including a comment
* !line settings.json 5-15 including a comment | !line 5-15 settings.json including a comment
* !line settings.json 5 5 needs a comment | !line 5 settings.json 5 needs a comment
* !line 5 5 needs a comment
* !line 5-7 6 should be deleted
* !line settings.json 5-7 6 should be deleted
* !highlight 5
*
*/
const commandPattern = /\!(?:line|highlight) (?:((?:[\w]+)?\.?[\w]*) )?(\!)?(-?\d+)(?:(?:-{1}|\.{2})(-?\d+))?(?: ((?:[\w]+)?\.[\w]{1,}))?(?: (.+))?/i;
const cmdopts = commandPattern.exec(message);
if (!cmdopts) {
return undefined;
}
const highlight = cmdopts[2] === undefined;
const fileName = cmdopts[1] || cmdopts[5];
const startLine = +cmdopts[3];
const endLine = cmdopts[4] ? +cmdopts[4] : +cmdopts[3];
const comments = cmdopts[6];
const vStartLine = endLine < startLine ? endLine : startLine;
const vEndLine = endLine < startLine ? startLine : endLine;
const result: ParseMessageResult = {
highlight,
startLine: vStartLine,
endLine: vEndLine,
fileName,
comments: comments
};
return result;
};
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"outDir": "out",
"lib": [
"es6",
"es2017"
],
// "declaration": false,
// "declarationDir": "types",
"sourceMap": true,
"rootDir": "src",
/* Strict Type-Checking Option */
"strict": true, /* enable all strict type-checking options */
/* Additional Checks */
// "noUnusedLocals": true /* Report errors on unused locals. */,
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
"skipLibCheck": true /* Skip type checking of all declaration files */
},
"exclude": [
"node_modules",
".vscode-test",
"types",
"out"
]
}
================================================
FILE: tslint.json
================================================
{
"rules": {
"no-string-throw": true,
"no-unused-expression": true,
"no-duplicate-variable": true,
"curly": true,
"class-name": true,
"semicolon": [
true,
"always"
],
"triple-equals": true
},
"defaultSeverity": "warning",
"linterOptions": {
"exclude": [
"node_modules"
]
}
}
================================================
FILE: webpack.config.js
================================================
'use strict';
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin');
module.exports = {
target: 'node',
entry: {
extension: './src/extension.ts',
ttvchat: './src/ttvchat/index.ts',
},
devtool: 'source-map',
plugins: [
new CleanWebpackPlugin(),
new CopyPlugin({
patterns: [
{ from: 'src/ttvchat/login', to: 'ttvchat/login' }
]
})
],
module: {
rules: [
{
test: /\.ts$/,
exclude: /node_modules/,
use: 'ts-loader',
},
]
},
externals: {
// Exclude vscode from webpack since it is generated automatically
vscode: 'commonjs vscode',
},
resolve: {
extensions: ['.ts', '.js']
},
output: {
path: path.resolve(__dirname, 'out'),
filename: '[name].js',
libraryTarget: 'commonjs2',
devtoolModuleFilenameTemplate: '../[resource-path]'
}
};
gitextract_oxdixv9m/ ├── .all-contributorsrc ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ └── bug_report.md │ └── workflows/ │ ├── ci-pipeline.yml │ ├── codeql-analysis.yml │ └── deploy.yml ├── .gitignore ├── .vscode/ │ ├── extensions.json │ ├── launch.json │ ├── settings.json │ └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── README.md ├── build.yml ├── package.json ├── src/ │ ├── api.ts │ ├── app.ts │ ├── common/ │ │ ├── index.ts │ │ └── keytar.ts │ ├── constants.ts │ ├── credentialManager.ts │ ├── enums/ │ │ ├── AppContexts.ts │ │ ├── Commands.ts │ │ ├── Configuration.ts │ │ ├── InternalCommands.ts │ │ ├── KeytarKeys.ts │ │ ├── LogLevel.ts │ │ ├── Settings.ts │ │ ├── TwitchKeys.ts │ │ └── index.ts │ ├── extension.ts │ ├── highlight/ │ │ ├── Highlight.ts │ │ ├── HighlightManager.ts │ │ ├── index.ts │ │ └── treeView/ │ │ ├── HighlightTreeDataProvider.ts │ │ ├── HighlightTreeItem.ts │ │ └── index.ts │ ├── index.ts │ ├── logger.ts │ ├── test/ │ │ ├── runTest.ts │ │ └── suite/ │ │ ├── extension.test.ts │ │ ├── index.ts │ │ └── utils.test.ts │ ├── ttvchat/ │ │ ├── AuthenticationService.ts │ │ ├── ChatClient.ts │ │ ├── TwitchChatService.ts │ │ ├── api/ │ │ │ ├── API.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ └── login/ │ │ └── index.htm │ └── utils/ │ ├── getNodeModule.ts │ ├── index.ts │ ├── isEnum.ts │ ├── naturalCompare.ts │ └── parseMessage.ts ├── tsconfig.json ├── tslint.json └── webpack.config.js
SYMBOL INDEX (120 symbols across 26 files)
FILE: src/api.ts
type HighlighterAPI (line 1) | interface HighlighterAPI {
FILE: src/app.ts
class App (line 12) | class App implements vscode.Disposable {
method constructor (line 20) | constructor(outputChannel?: vscode.OutputChannel) {
method intialize (line 28) | public intialize(context: vscode.ExtensionContext) {
method requestHighlight (line 64) | requestHighlight(service: string, userName: string, startLine: number,...
method requestUnhighlight (line 73) | requestUnhighlight(service: string, userName: string, lineNumber: numb...
method requestUnhighlightAll (line 80) | requestUnhighlightAll(service: string) {
method dispose (line 85) | public async dispose() {
method onDidChangeTextDocumentHandler (line 88) | private onDidChangeTextDocumentHandler(event: vscode.TextDocumentChang...
method onDidChangeConfigurationHandler (line 104) | private onDidChangeConfigurationHandler(event: vscode.ConfigurationCha...
method onDidChangeVisibleTextEditorsHandler (line 113) | private onDidChangeVisibleTextEditorsHandler(editors: Array<vscode.Tex...
method setEditorHasHighlightsContext (line 130) | private setEditorHasHighlightsContext() {
method onDidChangeActiveTextEditorHandler (line 141) | private onDidChangeActiveTextEditorHandler(editor?: vscode.TextEditor)...
method refreshTreeviewHandler (line 151) | private refreshTreeviewHandler(): void {
method createTextEditorDecorationType (line 155) | private createTextEditorDecorationType(): vscode.TextEditorDecorationT...
method refresh (line 169) | private refresh(): void {
method onHighlightChangedHandler (line 180) | private onHighlightChangedHandler(): void {
method isActiveTextEditor (line 184) | private get isActiveTextEditor(): boolean {
method highlightHandler (line 191) | private async highlightHandler(): Promise<void> {
method unhighlightHandler (line 214) | private async unhighlightHandler(treeItem?: HighlightTreeItem): Promis...
method unhighlightSpecificHandler (line 242) | private async unhighlightSpecificHandler(): Promise<void> {
method unhighlightAllHandler (line 270) | private unhighlightAllHandler(): void {
method gotoHighlightHandler (line 274) | private async gotoHighlightHandler(lineNumber: number, fileName: strin...
method requestHighlightHandler (line 284) | private requestHighlightHandler(service: string, userName: string, sta...
method requestUnhighlightHandler (line 294) | private requestUnhighlightHandler(service: string, userName: string, l...
method requestUnhighlightAllHandler (line 304) | private requestUnhighlightAllHandler(service: string): void {
method contextMenuUnhighlightHandler (line 308) | private contextMenuUnhighlightHandler() {
FILE: src/credentialManager.ts
function getNodeModule (line 7) | function getNodeModule<T>(moduleName: string): T | undefined {
class CredentialManager (line 24) | class CredentialManager {
method deleteTwitchClientId (line 33) | public static async deleteTwitchClientId(): Promise<boolean> {
method setPassword (line 39) | public static async setPassword(value: string): Promise<void> {
method deleteTwitchToken (line 44) | public static async deleteTwitchToken(): Promise<boolean> {
method getTwitchToken (line 50) | public static getTwitchToken(): Promise<string | null> {
method getPassword (line 56) | private static async getPassword(account: string): Promise<string | nu...
FILE: src/enums/AppContexts.ts
type AppContexts (line 1) | enum AppContexts {
FILE: src/enums/Commands.ts
type Commands (line 24) | enum Commands {
FILE: src/enums/Configuration.ts
type Configuration (line 1) | enum Configuration {
FILE: src/enums/InternalCommands.ts
type InternalCommands (line 1) | enum InternalCommands {
FILE: src/enums/KeytarKeys.ts
type KeytarKeys (line 1) | enum KeytarKeys {
FILE: src/enums/LogLevel.ts
type LogLevel (line 1) | enum LogLevel {
FILE: src/enums/Settings.ts
type Settings (line 1) | enum Settings {
FILE: src/enums/TwitchKeys.ts
type TwitchKeys (line 1) | enum TwitchKeys {
FILE: src/extension.ts
function activate (line 16) | function activate(context: vscode.ExtensionContext) {
function deactivate (line 34) | function deactivate() {
FILE: src/highlight/Highlight.ts
class Highlight (line 3) | class Highlight {
method constructor (line 8) | constructor(userName: string, range: Range, comments?: string) {
method range (line 14) | public get range(): Range {
method userName (line 18) | public get userName(): string {
method startLine (line 24) | public get startLine(): number {
method endLine (line 30) | public get endLine(): number {
method comments (line 34) | public get comments(): string | undefined {
method Update (line 41) | public Update(newRange: Range): void {
FILE: src/highlight/HighlightManager.ts
type HighlightCollection (line 14) | interface HighlightCollection {
type HighlightChangedEvent (line 19) | interface HighlightChangedEvent {
class HighlightManager (line 22) | class HighlightManager {
method onHighlightChanged (line 26) | public get onHighlightChanged(): Event<HighlightChangedEvent> {
method GetHighlightCollection (line 30) | public GetHighlightCollection(): Array<HighlightCollection> {
method GetHighlightDetails (line 34) | public GetHighlightDetails(): string[] {
method GetDecorations (line 44) | public GetDecorations(fileName: string): DecorationOptions[] {
method Add (line 57) | public Add(document: TextDocument, userName: string, startLine: number...
method Remove (line 92) | public Remove(documentOrFileName: TextDocument | string, userName: str...
method Refresh (line 113) | public Refresh() {
method Clear (line 118) | public Clear(service?: string): Promise<void> {
method Rename (line 136) | public Rename(oldName: string, newName: string) {
method UpdateHighlight (line 143) | public UpdateHighlight(document: TextDocument, valueChanged: TextDocum...
FILE: src/highlight/treeView/HighlightTreeDataProvider.ts
class HighlightTreeDataProvider (line 14) | class HighlightTreeDataProvider implements TreeDataProvider<HighlightTre...
method constructor (line 17) | constructor(private getHighlightCollections = (): HighlightCollection[...
method onDidChangeTreeData (line 21) | public get onDidChangeTreeData(): Event<HighlightTreeItem | undefined> {
method refresh (line 25) | public refresh(): void {
method getTreeItem (line 30) | public getTreeItem(element: HighlightTreeItem): TreeItem {
method getChildren (line 34) | public getChildren(element?: HighlightTreeItem): Thenable<HighlightTre...
FILE: src/highlight/treeView/HighlightTreeItem.ts
class HighlightTreeItem (line 6) | class HighlightTreeItem extends TreeItem {
method constructor (line 7) | constructor(
method description (line 18) | public get description(): string {
method HighlightTreeItems (line 25) | public get HighlightTreeItems(): HighlightTreeItem[] {
FILE: src/logger.ts
type log (line 6) | type log = {
class Logger (line 11) | class Logger {
method constructor (line 14) | constructor(outputChannel?: OutputChannel, thisArgs?: any) {
method log (line 20) | public log(levelOrMessage: LogLevel | string, message?: string, ...opt...
FILE: src/test/runTest.ts
function main (line 5) | async function main() {
FILE: src/test/suite/extension.test.ts
type ICommand (line 18) | interface ICommand {
type IConfiguration (line 24) | interface IConfiguration {
type EnumIndexer (line 30) | type EnumIndexer = {
FILE: src/test/suite/index.ts
function run (line 5) | function run(): Promise<void> {
FILE: src/test/suite/utils.test.ts
type Theory (line 5) | interface Theory {
FILE: src/ttvchat/AuthenticationService.ts
class AuthenticationService (line 14) | class AuthenticationService {
method constructor (line 19) | constructor(private log: log) { }
method initialize (line 21) | public async initialize() {
method validateToken (line 36) | public async validateToken(accessToken: string) {
method signInHandler (line 44) | public async signInHandler() {
method signOutHandler (line 66) | public async signOutHandler() {
method createServer (line 81) | private createServer(state: string) {
FILE: src/ttvchat/ChatClient.ts
type IBadges (line 16) | interface IBadges extends Badges {
type ChatClientMessageReceivedEvent (line 21) | interface ChatClientMessageReceivedEvent {
class ChatClient (line 26) | class ChatClient implements Disposable {
method constructor (line 41) | constructor(private log: log) { }
method initialize (line 43) | public initialize(context: ExtensionContext) {
method connect (line 53) | public async connect() {
method disconnect (line 77) | public async disconnect() {
method dispose (line 90) | public async dispose() {
method isConnected (line 94) | private get isConnected(): boolean {
method sendMessage (line 98) | private async sendMessage(message: string) {
method onJoinHandler (line 104) | private async onJoinHandler(channel: string, username: string, self: b...
method onConnectedHandler (line 111) | private onConnectedHandler(address: string, port: number) {
method onMessageHandler (line 115) | private async onMessageHandler(channel: string, userState: ChatUsersta...
method onDidChangeConfigurationHandler (line 154) | private async onDidChangeConfigurationHandler(event: ConfigurationChan...
FILE: src/ttvchat/TwitchChatService.ts
class TwitchChatService (line 10) | class TwitchChatService implements vscode.Disposable {
method constructor (line 20) | constructor(api: HighlighterAPI, outputChannel: vscode.OutputChannel) {
method initialize (line 42) | public async initialize(context: vscode.ExtensionContext): Promise<voi...
method onDidChangeConfigurationHandler (line 70) | private onDidChangeConfigurationHandler(event: vscode.ConfigurationCha...
method onAuthStatusChangedHandler (line 77) | private onAuthStatusChangedHandler(signedIn: boolean) {
method onChatClientConnectedHandler (line 89) | private onChatClientConnectedHandler(isConnected: boolean) {
method onChatClientMessageReceivedHandler (line 106) | private onChatClientMessageReceivedHandler(event: ChatClientMessageRec...
method onSignOutHandler (line 119) | private onSignOutHandler() {
method dispose (line 126) | public async dispose() {
FILE: src/ttvchat/api/API.ts
class API (line 5) | class API {
method isUserFollowingChannel (line 6) | public static async isUserFollowingChannel(userId: string, channel: st...
method validateToken (line 30) | public static async validateToken(token: string) {
method revokeToken (line 55) | public static async revokeToken(token: string) {
FILE: src/utils/parseMessage.ts
type ParseMessageResult (line 1) | interface ParseMessageResult {
Condensed preview — 60 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (110K chars).
[
{
"path": ".all-contributorsrc",
"chars": 1329,
"preview": "{\n \"files\": [\n \"README.md\"\n ],\n \"imageSize\": 100,\n \"commit\": false,\n \"contributors\": [\n {\n \"login\": \"par"
},
{
"path": ".editorconfig",
"chars": 641,
"preview": "# http://editorconfig.org\nroot = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_tr"
},
{
"path": ".gitattributes",
"chars": 77,
"preview": "# Set default behavior to automatically normalize line endings.\n* text=auto\n\n"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 599,
"preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n---\n\n**Describe the bu"
},
{
"path": ".github/workflows/ci-pipeline.yml",
"chars": 541,
"preview": "name: \"Build and Test\"\n\non:\n pull_request:\n branches:\n - vnext\n - main\n\njobs:\n build:\n strategy:\n "
},
{
"path": ".github/workflows/codeql-analysis.yml",
"chars": 2357,
"preview": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# Y"
},
{
"path": ".github/workflows/deploy.yml",
"chars": 619,
"preview": "name: \"Deploy to Registries\"\n\non:\n push:\n branches:\n - main\n\njobs:\n deploy:\n runs-on: ubuntu-latest\n ste"
},
{
"path": ".gitignore",
"chars": 53,
"preview": "out\nnode_modules\ntypes\n.vscode-test/\n*.vsix\n.dccache\n"
},
{
"path": ".vscode/extensions.json",
"chars": 156,
"preview": "{\n\t// See http://go.microsoft.com/fwlink/?LinkId=827846\n\t// for the documentation about the extensions.json format\n\t\"rec"
},
{
"path": ".vscode/launch.json",
"chars": 1777,
"preview": "// A launch configuration that compiles the extension and then opens it inside a new window\n// Use IntelliSense to learn"
},
{
"path": ".vscode/settings.json",
"chars": 564,
"preview": "// Place your settings in this file to overwrite default and user settings.\n{\n \"files.exclude\": {\n \"out\": false, // "
},
{
"path": ".vscode/tasks.json",
"chars": 494,
"preview": "// See https://go.microsoft.com/fwlink/?LinkId=733558\n// for the documentation about the tasks.json format\n{\n \"versio"
},
{
"path": ".vscodeignore",
"chars": 177,
"preview": ".github/**\n.vscode/**\n.vscode-test/**\nout/test/**\nout/**/*.map\nsrc/**\n.gitignore\nbuild.yml\ntsconfig.json\nvsc-extension-q"
},
{
"path": "CHANGELOG.md",
"chars": 8760,
"preview": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Change"
},
{
"path": "README.md",
"chars": 7832,
"preview": "<div align=\"center\">\n\n# Twitch Line Highlighter VS Code Extension\n<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or m"
},
{
"path": "build.yml",
"chars": 1814,
"preview": "jobs:\n - job: Windows\n pool:\n name: Hosted VS2017\n demands: npm\n steps:\n - task: NodeTool@0\n "
},
{
"path": "package.json",
"chars": 9906,
"preview": "{\n \"name\": \"twitch-highlighter\",\n \"displayName\": \"Twitch Highlighter\",\n \"description\": \"Allow your Twitch viewers to "
},
{
"path": "src/api.ts",
"chars": 1243,
"preview": "export interface HighlighterAPI {\n /**\n * Call this function to request a new highlight within the open, active text "
},
{
"path": "src/app.ts",
"chars": 12062,
"preview": "import * as vscode from 'vscode';\nimport { HighlighterAPI } from './api';\nimport { AppContexts, Commands, Configuration,"
},
{
"path": "src/common/index.ts",
"chars": 26,
"preview": "export * from './keytar';\n"
},
{
"path": "src/common/keytar.ts",
"chars": 178,
"preview": "import * as keytartype from 'keytar';\nimport { getNodeModule } from '../utils';\n\nexport const keytar: typeof keytartype "
},
{
"path": "src/constants.ts",
"chars": 103,
"preview": "export const extensionId = 'clarkio.twitch-highlighter';\nexport const extSuffix = 'twitchHighlighter';\n"
},
{
"path": "src/credentialManager.ts",
"chars": 2274,
"preview": "import * as keytartype from 'keytar';\nimport { env } from 'vscode';\n\ndeclare const __webpack_require__: typeof require;\n"
},
{
"path": "src/enums/AppContexts.ts",
"chars": 76,
"preview": "export enum AppContexts {\n 'editorHasHighlights' = 'editorHasHighlights'\n}\n"
},
{
"path": "src/enums/Commands.ts",
"chars": 1992,
"preview": "// export enum Commands {\n// 'highlight' = 'twitchHighlighter.highlight',\n// 'unhighlight' = 'twitchHighlighter.unhi"
},
{
"path": "src/enums/Configuration.ts",
"chars": 284,
"preview": "export enum Configuration {\n \"sectionIdentifier\" = \"twitchHighlighter\",\n \"highlightBackgroundColor\" = \"highlightBackgr"
},
{
"path": "src/enums/InternalCommands.ts",
"chars": 105,
"preview": "export enum InternalCommands {\n 'removeBannedHighlights' = 'twitchHighlighter.removeBannedHighlights'\n}\n"
},
{
"path": "src/enums/KeytarKeys.ts",
"chars": 256,
"preview": "export enum KeytarKeys {\n \"service\" = \"vscode-twitch-highlighter-ttvchat\",\n \"account\" = \"vscode-twitch-highlighter-ttv"
},
{
"path": "src/enums/LogLevel.ts",
"chars": 114,
"preview": "export enum LogLevel {\n 'Information' = 'info',\n 'Warning' = 'warn',\n 'Error' = 'error',\n 'Debug' = 'debug'\n}\n"
},
{
"path": "src/enums/Settings.ts",
"chars": 494,
"preview": "export enum Settings {\n 'channels' = 'channels',\n 'username' = 'nickname',\n 'highlightColor' = 'highlightColor',\n 'h"
},
{
"path": "src/enums/TwitchKeys.ts",
"chars": 111,
"preview": "export enum TwitchKeys {\n \"clientId\" = \"83juwb58ggj9s7l7nf9ngyill70tem\",\n \"scope\" = \"chat:read chat:edit\",\n}\n"
},
{
"path": "src/enums/index.ts",
"chars": 244,
"preview": "export * from './Commands';\nexport * from './InternalCommands';\nexport * from './Settings';\nexport * from './LogLevel';\n"
},
{
"path": "src/extension.ts",
"chars": 1077,
"preview": "'use strict';\n// The module 'vscode' contains the VS Code extensibility API\n// Import the module and reference it with t"
},
{
"path": "src/highlight/Highlight.ts",
"chars": 1122,
"preview": "import { Range } from 'vscode';\n\nexport class Highlight {\n private _userName: string;\n private _range: Range;\n privat"
},
{
"path": "src/highlight/HighlightManager.ts",
"chars": 7591,
"preview": "import {\n TextDocument,\n Range,\n Position,\n EventEmitter,\n Event,\n TextDocumentContentChangeEvent,\n DecorationOpt"
},
{
"path": "src/highlight/index.ts",
"chars": 93,
"preview": "export * from './Highlight';\nexport * from './HighlightManager';\nexport * from './treeView';\n"
},
{
"path": "src/highlight/treeView/HighlightTreeDataProvider.ts",
"chars": 1940,
"preview": "import {\n TreeDataProvider,\n EventEmitter,\n Event,\n TreeItem,\n TreeItemCollapsibleState\n} from \"vscode\";\nimport{ ba"
},
{
"path": "src/highlight/treeView/HighlightTreeItem.ts",
"chars": 1503,
"preview": "import { TreeItem, TreeItemCollapsibleState, Command } from \"vscode\";\n\nimport { Highlight } from \"../Highlight\";\nimport "
},
{
"path": "src/highlight/treeView/index.ts",
"chars": 82,
"preview": "export * from './HighlightTreeItem';\nexport * from './HighlightTreeDataProvider';\n"
},
{
"path": "src/index.ts",
"chars": 22,
"preview": "export * from './api';"
},
{
"path": "src/logger.ts",
"chars": 1617,
"preview": "import { OutputChannel } from 'vscode';\n\nimport { LogLevel } from './enums';\nimport { isEnum } from './utils';\n\nexport t"
},
{
"path": "src/test/runTest.ts",
"chars": 715,
"preview": "import * as path from \"path\";\n\nimport { runTests } from \"@vscode/test-electron\";\n\nasync function main() {\n try {\n //"
},
{
"path": "src/test/suite/extension.test.ts",
"chars": 2850,
"preview": "import { extensionId, extSuffix } from '../../constants';\nimport { Settings } from \"../../enums\";\nimport { Commands } fr"
},
{
"path": "src/test/suite/index.ts",
"chars": 852,
"preview": "import * as path from \"path\";\nimport * as Mocha from \"mocha\";\nimport * as glob from \"glob\";\n\nexport function run(): Prom"
},
{
"path": "src/test/suite/utils.test.ts",
"chars": 1885,
"preview": "import * as assert from 'assert';\n\nimport { parseMessage } from '../../utils';\n\ninterface Theory {\n message: string;\n "
},
{
"path": "src/ttvchat/AuthenticationService.ts",
"chars": 4833,
"preview": "import { readFileSync } from 'fs';\nimport * as http from 'http';\nimport * as path from 'path';\nimport * as url from 'url"
},
{
"path": "src/ttvchat/ChatClient.ts",
"chars": 5631,
"preview": "import {\n Badges, ChatUserstate, Client,\n Options\n} from \"tmi.js\";\nimport {\n ConfigurationChangeEvent, Disposable, Ev"
},
{
"path": "src/ttvchat/TwitchChatService.ts",
"chars": 5066,
"preview": "import * as vscode from 'vscode';\nimport { Commands, Configuration, Settings } from '../enums';\nimport { HighlighterAPI "
},
{
"path": "src/ttvchat/api/API.ts",
"chars": 1986,
"preview": "import * as request from 'request';\n\nimport { TwitchKeys } from '../../enums';\n\nexport class API {\n public static async"
},
{
"path": "src/ttvchat/api/index.ts",
"chars": 24,
"preview": "export * from './API';\n"
},
{
"path": "src/ttvchat/index.ts",
"chars": 67,
"preview": "export * from './ChatClient';\nexport * from './TwitchChatService';\n"
},
{
"path": "src/ttvchat/login/index.htm",
"chars": 459,
"preview": "<html>\n\n<head>\n <script type=\"text/javascript\">\n window.onload = function () {\n if (window.location"
},
{
"path": "src/utils/getNodeModule.ts",
"chars": 595,
"preview": "import { env } from 'vscode';\nimport * as keytartype from 'keytar';\n\n\ndeclare const __webpack_require__: typeof require;"
},
{
"path": "src/utils/index.ts",
"chars": 125,
"preview": "export * from './isEnum';\nexport * from './naturalCompare';\nexport * from './getNodeModule';\nexport * from './parseMessa"
},
{
"path": "src/utils/isEnum.ts",
"chars": 243,
"preview": "import { isString, isNumber } from \"util\";\n\nexport const isEnum = (value: any, Enum: {}): boolean => {\n if (value === u"
},
{
"path": "src/utils/naturalCompare.ts",
"chars": 541,
"preview": "export const naturalCompare = (a: any, b: any) => {\n let ax: any[] = [], bx: any[] = [];\n\n a.replace(/(\\d+)|(\\D+)/g, f"
},
{
"path": "src/utils/parseMessage.ts",
"chars": 1864,
"preview": "export interface ParseMessageResult {\n highlight: boolean;\n startLine: number;\n endLine: number;\n fileName?: string;"
},
{
"path": "tsconfig.json",
"chars": 903,
"preview": "{\n \"compilerOptions\": {\n \"module\": \"commonjs\",\n \"target\": \"es6\",\n \"outDir\": \"out\",\n \"lib\": [\n \"es6\",\n "
},
{
"path": "tslint.json",
"chars": 343,
"preview": "{\n \"rules\": {\n \"no-string-throw\": true,\n \"no-unused-expression\": true,\n \"no-duplicate-variable\": true,\n \"cu"
},
{
"path": "webpack.config.js",
"chars": 958,
"preview": "'use strict';\n\nconst path = require('path');\nconst { CleanWebpackPlugin } = require('clean-webpack-plugin');\nconst CopyP"
}
]
About this extraction
This page contains the full source code of the clarkio/vscode-twitch-highlighter GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 60 files (99.8 KB), approximately 26.2k tokens, and a symbol index with 120 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.