Browse Source

Adds commit details webview

- adds lit components
- exposes webview in WebviewViewBase
- adds pointer cursor to enabled buttons
- adds custom events support to DOM utility
main
Keith Daulton 2 years ago
parent
commit
18d0ec586e
30 changed files with 3691 additions and 8 deletions
  1. +23
    -0
      package.json
  2. +8
    -0
      src/commands/git/search.ts
  3. +4
    -0
      src/commands/gitCommands.actions.ts
  4. +2
    -0
      src/commands/quickCommand.steps.ts
  5. +2
    -0
      src/commands/searchCommits.ts
  6. +1
    -0
      src/constants.ts
  7. +22
    -0
      src/container.ts
  8. +10
    -0
      src/quickpicks/items/commits.ts
  9. +4
    -1
      src/webviews/apps/.eslintrc.json
  10. +94
    -0
      src/webviews/apps/commitDetails/commitDetails.html
  11. +342
    -0
      src/webviews/apps/commitDetails/commitDetails.scss
  12. +392
    -0
      src/webviews/apps/commitDetails/commitDetails.ts
  13. +4
    -0
      src/webviews/apps/shared/base.scss
  14. +1505
    -0
      src/webviews/apps/shared/components/codicon.ts
  15. +67
    -0
      src/webviews/apps/shared/components/commit/commit-identity.ts
  16. +42
    -0
      src/webviews/apps/shared/components/commit/commit-stats.ts
  17. +233
    -0
      src/webviews/apps/shared/components/commit/file-change-item.ts
  18. +12
    -0
      src/webviews/apps/shared/components/converters/date-converter.ts
  19. +19
    -0
      src/webviews/apps/shared/components/formatted-date.ts
  20. +77
    -0
      src/webviews/apps/shared/components/rich/issue-pull-request.ts
  21. +57
    -0
      src/webviews/apps/shared/components/skeleton-loader.ts
  22. +97
    -0
      src/webviews/apps/shared/components/webview-pane.ts
  23. +7
    -1
      src/webviews/apps/shared/dom.ts
  24. +198
    -0
      src/webviews/commitDetails/commitDetailsWebview.ts
  25. +347
    -0
      src/webviews/commitDetails/commitDetailsWebviewView.ts
  26. +49
    -0
      src/webviews/commitDetails/protocol.ts
  27. +1
    -1
      src/webviews/webviewViewBase.ts
  28. +1
    -1
      tsconfig.base.json
  29. +5
    -3
      webpack.config.js
  30. +66
    -1
      yarn.lock

+ 23
- 0
package.json View File

@ -63,8 +63,10 @@
"onView:gitlens.views.contributors",
"onView:gitlens.views.searchAndCompare",
"onView:gitlens.views.worktrees",
"onView:gitlens.views.commitDetails",
"onWebviewPanel:gitlens.welcome",
"onWebviewPanel:gitlens.settings",
"onWebviewPanel:gitlens.commitDetails",
"onCommand:gitlens.plus.learn",
"onCommand:gitlens.plus.loginOrSignUp",
"onCommand:gitlens.plus.logout",
@ -99,6 +101,7 @@
"onCommand:gitlens.showStashesView",
"onCommand:gitlens.showTagsView",
"onCommand:gitlens.showTimelineView",
"onCommand:gitlens.showCommitDetailsPage",
"onCommand:gitlens.showWorktreesView",
"onCommand:gitlens.compareWith",
"onCommand:gitlens.compareHeadWith",
@ -3925,6 +3928,11 @@
"category": "GitLens"
},
{
"command": "gitlens.showCommitDetailsPage",
"title": "Show Commit Details View",
"category": "GitLens"
},
{
"command": "gitlens.showWorktreesView",
"title": "Show Worktrees View",
"category": "GitLens"
@ -6224,6 +6232,10 @@
"when": "gitlens:enabled"
},
{
"command": "gitlens.showCommitDetailsPage",
"when": "gitlens:enabled"
},
{
"command": "gitlens.showWorktreesView",
"when": "gitlens:enabled && !gitlens:hasVirtualFolders"
},
@ -10934,6 +10946,15 @@
],
"scm": [
{
"type": "webview",
"id": "gitlens.views.commitDetails",
"name": "Commit Details",
"when": "!gitlens:disabled",
"contextualTitle": "GitLens",
"icon": "images/views/commits.svg",
"visibility": "hidden"
},
{
"id": "gitlens.views.commits",
"name": "Commits",
"when": "!gitlens:disabled",
@ -11263,6 +11284,7 @@
"chroma-js": "2.4.2",
"https-proxy-agent": "5.0.1",
"iconv-lite": "0.6.3",
"lit": "^2.2.7",
"lodash-es": "4.17.21",
"md5.js": "1.3.5",
"node-fetch": "2.6.7",
@ -11297,6 +11319,7 @@
"eslint-import-resolver-typescript": "3.4.0",
"eslint-plugin-anti-trojan-source": "1.1.0",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-lit": "^1.6.1",
"fork-ts-checker-webpack-plugin": "6.5.2",
"glob": "8.0.3",
"html-loader": "4.1.0",

+ 8
- 0
src/commands/git/search.ts View File

@ -36,6 +36,7 @@ interface Context {
interface State extends Required<SearchPattern> {
repo: string | Repository;
showResultsInSideBar: boolean | SearchResultsNode;
showResultsInDetails?: boolean;
}
export interface SearchGitCommandArgs {
@ -242,6 +243,13 @@ export class SearchGitCommand extends QuickCommand {
context.commit = result;
}
if (state.showResultsInDetails) {
void this.container.commitDetailsWebviewView.show({
commit: context.commit,
});
break;
}
const result = yield* getSteps(
this.container,
{

+ 4
- 0
src/commands/gitCommands.actions.ts View File

@ -583,6 +583,10 @@ export namespace GitActions {
}
}
export async function openDetails(commit: GitCommit): Promise<void> {
void (await Container.instance.commitDetailsWebviewView.show({ commit: commit }));
}
export async function openFiles(commit: GitCommit): Promise<void>;
export async function openFiles(files: GitFile[], repoPath: string, ref: string): Promise<void>;
export async function openFiles(

+ 2
- 0
src/commands/quickCommand.steps.ts View File

@ -41,6 +41,7 @@ import {
CommitOpenChangesCommandQuickPickItem,
CommitOpenChangesWithDiffToolCommandQuickPickItem,
CommitOpenChangesWithWorkingCommandQuickPickItem,
CommitOpenDetailsCommandQuickPickItem,
CommitOpenDirectoryCompareCommandQuickPickItem,
CommitOpenDirectoryCompareWithWorkingCommandQuickPickItem,
CommitOpenFileCommandQuickPickItem,
@ -1783,6 +1784,7 @@ async function getShowCommitOrStashStepItems<
new CommitOpenAllChangesWithWorkingCommandQuickPickItem(state.reference),
new CommitOpenAllChangesWithDiffToolCommandQuickPickItem(state.reference),
QuickPickSeparator.create(),
new CommitOpenDetailsCommandQuickPickItem(state.reference),
new CommitOpenFilesCommandQuickPickItem(state.reference),
new CommitOpenRevisionsCommandQuickPickItem(state.reference),
);

+ 2
- 0
src/commands/searchCommits.ts View File

@ -13,6 +13,7 @@ export interface SearchCommitsCommandArgs {
prefillOnly?: boolean;
showResultsInDetails?: boolean;
showResultsInSideBar?: boolean;
}
@ -53,6 +54,7 @@ export class SearchCommitsCommand extends Command {
...args?.search,
showResultsInSideBar:
configuration.get('gitCommands.search.showResultsInSideBar') ?? args?.showResultsInSideBar,
showResultsInDetails: args?.showResultsInDetails ?? false,
},
}));
}

+ 1
- 0
src/constants.ts View File

@ -199,6 +199,7 @@ export const enum Commands {
ShowTagsView = 'gitlens.showTagsView',
ShowWorktreesView = 'gitlens.showWorktreesView',
RefreshTimelinePage = 'gitlens.refreshTimelinePage',
ShowCommitDetailsPage = 'gitlens.showCommitDetailsPage',
ShowTimelinePage = 'gitlens.showTimelinePage',
ShowTimelineView = 'gitlens.showTimelineView',
ShowWelcomePage = 'gitlens.showWelcomePage',

+ 22
- 0
src/container.ts View File

@ -50,6 +50,8 @@ import { ViewCommands } from './views/viewCommands';
import { ViewFileDecorationProvider } from './views/viewDecorationProvider';
import { WorktreesView } from './views/worktreesView';
import { VslsController } from './vsls/vsls';
import { CommitDetailsWebview } from './webviews/commitDetails/commitDetailsWebview';
import { CommitDetailsWebviewView } from './webviews/commitDetails/commitDetailsWebviewView';
import { HomeWebviewView } from './webviews/home/homeWebviewView';
import { RebaseEditorProvider } from './webviews/rebase/rebaseEditor';
import { SettingsWebview } from './webviews/settings/settingsWebview';
@ -175,6 +177,8 @@ export class Container {
context.subscriptions.push((this._timelineWebview = new TimelineWebview(this)));
context.subscriptions.push((this._welcomeWebview = new WelcomeWebview(this)));
context.subscriptions.push((this._rebaseEditor = new RebaseEditorProvider(this)));
context.subscriptions.push((this._commitDetailsWebview = new CommitDetailsWebview(this)));
context.subscriptions.push((this._commitDetailsWebviewView = new CommitDetailsWebviewView(this)));
context.subscriptions.push(new ViewFileDecorationProvider());
@ -286,6 +290,24 @@ export class Container {
return this._commitsView;
}
private _commitDetailsWebview: CommitDetailsWebview | undefined;
get commitDetailsWebview() {
if (this._commitDetailsWebview == null) {
this._context.subscriptions.push((this._commitDetailsWebview = new CommitDetailsWebview(this)));
}
return this._commitDetailsWebview;
}
private _commitDetailsWebviewView: CommitDetailsWebviewView | undefined;
get commitDetailsWebviewView() {
if (this._commitDetailsWebviewView == null) {
this._context.subscriptions.push((this._commitDetailsWebviewView = new CommitDetailsWebviewView(this)));
}
return this._commitDetailsWebviewView;
}
private readonly _context: ExtensionContext;
get context() {
return this._context;

+ 10
- 0
src/quickpicks/items/commits.ts View File

@ -251,6 +251,16 @@ export class CommitOpenDirectoryCompareWithWorkingCommandQuickPickItem extends C
}
}
export class CommitOpenDetailsCommandQuickPickItem extends CommandQuickPickItem {
constructor(private readonly commit: GitCommit, item?: QuickPickItem) {
super(item ?? '$(files) Open in Commit Details');
}
override execute(_options: { preserveFocus?: boolean; preview?: boolean }): Promise<void> {
return GitActions.Commit.openDetails(this.commit);
}
}
export class CommitOpenFilesCommandQuickPickItem extends CommandQuickPickItem {
constructor(private readonly commit: GitCommit, item?: QuickPickItem) {
super(item ?? '$(files) Open Files');

+ 4
- 1
src/webviews/apps/.eslintrc.json View File

@ -1,8 +1,11 @@
{
"extends": ["../../../.eslintrc.base.json"],
"extends": ["../../../.eslintrc.base.json", "plugin:lit/recommended"],
"env": {
"browser": true
},
"rules": {
"import/extensions": ["error", "never", { "js": "always" }]
},
"parserOptions": {
"project": "src/webviews/apps/tsconfig.json"
}

+ 94
- 0
src/webviews/apps/commitDetails/commitDetails.html View File

@ -0,0 +1,94 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
</head>
<body class="preload">
<div class="commit-detail-panel">
<!-- commitDetails:header -->
<!-- commitDetails:choices -->
<div class="commit-detail-panel__none" id="empty" aria-hidden="true">
<p><code-icon icon="warning"></code-icon> <span>No commit selected</span></p>
<p>
<button class="button button--full" type="button" data-action="pick-commit">Pick Commit...</button>
</p>
<!-- <p><button class="button button--full" type="button">Follow Open Files</button></p> -->
</div>
<main class="commit-details commit-detail-panel__main" id="main" tabindex="-1">
<div class="commit-details__commit">
<div class="commit-details__top">
<ul class="commit-details__authors" aria-label="Authors">
<li class="commit-details__author" data-region="author">
<skeleton-loader></skeleton-loader>
</li>
</ul>
<a
class="commit-details__commit-action"
href="#"
data-action="commit-show-actions"
aria-label="Show Commit Actions"
title="Show Commit Actions"
><code-icon icon="kebab-vertical"></code-icon
></a>
</div>
<div class="commit-details__message">
<p class="commit-details__message-text" data-region="message">
<skeleton-loader></skeleton-loader>
</p>
</div>
</div>
<webview-pane collapsable expanded data-region="rich-pane">
<span slot="title">Autolinks</span>
<div class="commit-details__rich" data-region="rich-info" aria-hidden="true">
<p>
<code-icon icon="info"></code-icon> Use autolinks to identify external references, like Jira
issues or Zendesk tickets, in your commit messages and convert them to clickable links.
</p>
<p>
<a href="#">Configure autolinks in settings</a>
</p>
</div>
<div class="commit-details__rich" data-region="autolinks">
<section
class="commit-details__pull-request"
aria-label="Pull request"
data-region="pull-request"
>
<skeleton-loader lines="2"></skeleton-loader>
</section>
<section class="commit-details__issue" aria-label="Issue" data-region="issue">
<skeleton-loader lines="2"></skeleton-loader>
</section>
</div>
</webview-pane>
<webview-pane class="commit-details__files" collapsable expanded>
<span slot="title"
>Files changed
<span class="commit-details__stats" data-region="stats"></span>
</span>
<ul class="change-list" data-region="files">
<li class="change-list__item commit-details__item-skeleton">
<skeleton-loader></skeleton-loader>
</li>
<li class="change-list__item commit-details__item-skeleton">
<skeleton-loader></skeleton-loader>
</li>
<li class="change-list__item commit-details__item-skeleton">
<skeleton-loader></skeleton-loader>
</li>
</ul>
</webview-pane>
</main>
</div>
#{endOfBody}
<style nonce="#{cspNonce}">
@font-face {
font-family: 'codicon';
src: url('#{webroot}/codicon.ttf?404cbc4fe3a64b9a93064eef76704c79') format('truetype');
}
</style>
</body>
</html>

+ 342
- 0
src/webviews/apps/commitDetails/commitDetails.scss View File

@ -0,0 +1,342 @@
// @import '../shared/base';
// @import '../shared/buttons';
// @import '../shared/icons';
:root {
--gitlens-gutter-width: 20px;
--gitlens-scrollbar-gutter-width: 10px;
--gitlens-view-background-color: #fafafa;
}
// generic resets
html {
// height: 100%;
font-size: 62.5%;
box-sizing: border-box;
font-family: var(--font-family);
}
* {
&,
&::before,
&::after {
box-sizing: inherit;
}
}
body {
// height: 100%;
font-family: var(--font-family);
font-size: var(--font-size);
color: var(--color-foreground);
padding: 0;
}
ul {
list-style: none;
margin: 0;
padding: 0;
}
::-webkit-scrollbar-corner {
background-color: transparent !important;
}
.button {
--button-foreground: var(--vscode-button-foreground);
--button-background: var(--vscode-button-background);
--button-hover-background: var(--vscode-button-hoverBackground);
display: inline-block;
border: none;
padding: 0.4rem;
font-size: inherit;
line-height: inherit;
text-align: center;
text-decoration: none;
user-select: none;
background: var(--button-background);
color: var(--button-foreground);
cursor: pointer;
&:hover {
background: var(--button-hover-background);
}
&:focus {
outline: 1px solid var(--vscode-focusBorder);
outline-offset: 0.2rem;
}
&--full {
width: 100%;
}
}
// webview-specific styles
.change-list {
list-style: none;
&__item {
// & + & {
// margin-top: 0.25rem;
// }
}
&__link {
width: 100%;
color: inherit;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
&__type {}
&__filename {}
&__path {}
&__actions {
flex: none;
}
&__action {}
}
.pull-request,
.issue {
display: grid;
gap: 0.25rem 0.5rem;
justify-content: start;
&__icon {
grid-column: 1;
grid-row: 1 / 3;
color: var(--vscode-gitlens-mergedPullRequestIconColor);
}
&__title {
grid-column: 2;
grid-row: 1;
margin: 0;
font-size: 1.5rem;
}
&__date {
grid-column: 2;
grid-row: 2;
margin: 0;
font-size: 1.2rem;
}
}
.commit-author {
display: grid;
gap: 0.25rem 0.5rem;
justify-content: start;
&__avatar {
grid-column: 1;
grid-row: 1 / 3;
}
&__name {
grid-column: 2;
grid-row: 1;
font-size: 1.5rem;
}
&__date {
grid-column: 2;
grid-row: 2;
font-size: 1.2rem;
}
}
.commit-details {
&__commit {
padding: { // 1.5rem
left: var( --gitlens-gutter-width);
right: var( --gitlens-scrollbar-gutter-width);
}
// background-color: var(--color-background--lighten-05);
margin-bottom: 1.75rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
&__top {
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: space-between;
gap: 0.5rem;
}
&__message {
font-size: 1.5rem;
border: 1px solid var(--color-background--lighten-15);
padding: 0.5rem;
}
&__message-text {
flex: 1;
margin: 0;
display: block;
@supports (-webkit-line-clamp: 6) {
display: -webkit-box;
-webkit-line-clamp: 6;
-webkit-box-orient: vertical;
overflow: hidden;
}
}
&__commit-action {
display: inline-flex;
justify-content: center;
align-items: center;
width: 20px;
height: 20px;
border-radius: 0.25em;
color: inherit;
padding: 2px;
vertical-align: text-bottom;
text-decoration: none;
> * {
pointer-events: none;
}
&:hover {
color: var(--vscode-foreground);
background-color: var(--color-background--lighten-15);
}
}
&__authors {
flex-basis: 100%;
}
&__author {
& + & {
margin-top: 0.5rem;
}
}
&__rich {
padding: 0.5rem var( --gitlens-scrollbar-gutter-width) 1rem var( --gitlens-gutter-width);
> :last-child {
margin-top: 0.5rem;
}
}
&__pull-request {}
&__issue {
> :not(:first-child) {
margin-top: 0.5rem;
}
}
&__stats {
margin-left: 0.5rem;
}
// &__files {
// border-top: 1px solid var(--color-background--lighten-075);
// padding: {
// top: 1.75rem;
// }
// }
&__file {
padding: {
left: var( --gitlens-gutter-width);
right: var( --gitlens-scrollbar-gutter-width);
top: 1px;
bottom: 1px;
}
}
&__item-skeleton {
padding: {
left: var( --gitlens-gutter-width);
right: var( --gitlens-scrollbar-gutter-width);
top: 1px;
bottom: 1px;
}
}
}
.commit-detail-panel {
$block: &;
max-height: 100vh;
overflow: auto;
scrollbar-gutter: stable;
[aria-hidden="true"] {
display: none;
}
&__none {
padding: {
left: var( --gitlens-gutter-width);
right: var( --gitlens-scrollbar-gutter-width);
}
}
&__header {
margin: {
top: 1rem;
bottom: 1.5rem;
}
}
&__title {
font-size: 2.4rem;
// FIXME: specificity hack
&-icon {
// color: var(--vscode-banner-iconForeground);
font-size: inherit !important;
}
}
&__nav {
border: 1px solid var(--color-button-secondary-background);
padding: 0.5rem;
margin: {
top: 1rem;
bottom: 1.5rem;
}
}
&__commit-count {
margin: {
top: 0;
bottom: 0.5rem;
}
}
&__commits {}
&__commit {
& + & {
margin-top: 0.5rem;
}
}
&__commit-button {
appearance: none;
text-decoration: none;
border: none;
color: var(--color-button-foreground);
background-color: var(--color-button-secondary-background);
padding: 0.5rem;
display: flex;
align-items: center;
width: 100%;
gap: 0.5rem;
> :last-child {
margin-left: auto;
}
&[aria-current="true"] {
background-color: var(--color-button-background);
}
}
&__main {
padding: {
top: 1rem;
bottom: 1rem;
}
}
}
@import '../shared/codicons';
@import '../shared/utils';

+ 392
- 0
src/webviews/apps/commitDetails/commitDetails.ts View File

@ -0,0 +1,392 @@
/*global*/
import { IpcMessage, onIpc } from '../../../webviews/protocol';
import {
CommitActionsCommandType,
CommitSummary,
DidChangeNotificationType,
FileComparePreviousCommandType,
FileCompareWorkingCommandType,
FileMoreActionsCommandType,
OpenFileCommandType,
OpenFileOnRemoteCommandType,
PickCommitCommandType,
RichContentNotificationType,
State,
} from '../../commitDetails/protocol';
import { App } from '../shared/appBase';
import { FileChangeItem, FileChangeItemEventDetail } from '../shared/components/commit/file-change-item';
import { DOM } from '../shared/dom';
import './commitDetails.scss';
import '../shared/components/codicon';
import '../shared/components/commit/commit-identity';
import '../shared/components/formatted-date';
import '../shared/components/rich/issue-pull-request';
import '../shared/components/skeleton-loader';
import '../shared/components/commit/commit-stats';
// eslint-disable-next-line @typescript-eslint/no-duplicate-imports
import '../shared/components/commit/file-change-item';
import '../shared/components/webview-pane';
export class CommitDetailsApp extends App<State> {
constructor() {
super('CommitDetailsApp');
console.log('CommitDetailsApp', this.state);
}
override onInitialize() {
console.log('CommitDetailsApp onInitialize', this.state);
this.renderContent();
}
override onBind() {
const disposables = [
DOM.on<FileChangeItem, FileChangeItemEventDetail>('file-change-item', 'file-open-on-remote', e =>
this.onOpenFileOnRemote(e.detail),
),
DOM.on<FileChangeItem, FileChangeItemEventDetail>('file-change-item', 'file-open', e =>
this.onOpenFile(e.detail),
),
DOM.on<FileChangeItem, FileChangeItemEventDetail>('file-change-item', 'file-compare-working', e =>
this.onCompareFileWithWorking(e.detail),
),
DOM.on<FileChangeItem, FileChangeItemEventDetail>('file-change-item', 'file-compare-previous', e =>
this.onCompareFileWithPrevious(e.detail),
),
DOM.on<FileChangeItem, FileChangeItemEventDetail>('file-change-item', 'file-more-actions', e =>
this.onFileMoreActions(e.detail),
),
DOM.on('[data-action="commit-show-actions"]', 'click', e => this.onCommitMoreActions(e)),
DOM.on('[data-action="pick-commit"]', 'click', e => this.onPickCommit(e)),
];
return disposables;
}
onPickCommit(e: MouseEvent) {
this.sendCommand(PickCommitCommandType, undefined);
}
onOpenFileOnRemote(e: FileChangeItemEventDetail) {
this.sendCommand(OpenFileOnRemoteCommandType, e);
}
onOpenFile(e: FileChangeItemEventDetail) {
this.sendCommand(OpenFileCommandType, e);
}
onCompareFileWithWorking(e: FileChangeItemEventDetail) {
this.sendCommand(FileCompareWorkingCommandType, e);
}
onCompareFileWithPrevious(e: FileChangeItemEventDetail) {
this.sendCommand(FileComparePreviousCommandType, e);
}
onFileMoreActions(e: FileChangeItemEventDetail) {
this.sendCommand(FileMoreActionsCommandType, e);
}
onCommitMoreActions(e: MouseEvent) {
e.preventDefault();
if (this.state.selected === undefined) {
e.stopPropagation();
return;
}
this.sendCommand(CommitActionsCommandType, undefined);
}
protected override onMessageReceived(e: MessageEvent) {
const msg = e.data as IpcMessage;
switch (msg.method) {
case RichContentNotificationType.method:
onIpc(RichContentNotificationType, msg, params => {
const newState = { ...this.state };
if (params.formattedMessage != null) {
newState.selected.message = params.formattedMessage;
}
if (params.pullRequest != null) {
newState.pullRequest = params.pullRequest;
}
if (params.formattedMessage != null) {
newState.issues = params.issues;
}
this.state = newState;
this.renderRichContent();
});
break;
case DidChangeNotificationType.method:
onIpc(DidChangeNotificationType, msg, params => {
this.state = { ...this.state, ...params.state };
this.renderContent();
});
break;
}
}
renderCommit() {
const hasSelection = this.state.selected !== undefined;
const $empty = document.getElementById('empty');
const $main = document.getElementById('main');
$empty?.setAttribute('aria-hidden', hasSelection ? 'true' : 'false');
$main?.setAttribute('aria-hidden', hasSelection ? 'false' : 'true');
return hasSelection;
}
renderRichContent() {
if (!this.renderCommit()) {
return;
}
this.renderMessage();
this.renderAutolinks();
}
renderContent() {
if (!this.renderCommit()) {
return;
}
this.renderMessage();
this.renderAuthor();
this.renderStats();
this.renderFiles();
if (this.state.includeRichContent) {
this.renderAutolinks();
}
}
renderChoices() {
// <nav class="commit-detail-panel__nav" aria-label="list of selected commits" data-region="choices">
// <p class="commit-detail-panel__commit-count">
// Selected commits: <span data-region="choices-count">2</span>
// </p>
// <ul class="commit-detail-panel__commits" data-region="choices-list">
// <li class="commit-detail-panel__commit">
// <skeleton-loader></skeleton-loader>
// </li>
// <li class="commit-detail-panel__commit">
// <skeleton-loader></skeleton-loader>
// </li>
// </ul>
// </nav>
const $el = document.querySelector<HTMLElement>('[data-region="choices"]');
if ($el == null) {
return;
}
if (this.state.commits?.length) {
const $count = $el.querySelector<HTMLElement>('[data-region="choices-count"]');
if ($count != null) {
$count.innerHTML = `${this.state.commits.length}`;
}
const $list = $el.querySelector<HTMLElement>('[data-region="choices-list"]');
if ($list != null) {
$list.innerHTML = this.state.commits
.map(
(item: CommitSummary) => `
<li class="commit-detail-panel__commit">
<button class="commit-detail-panel__commit-button" type="button" ${
item.sha === this.state.selected.sha ? 'aria-current="true"' : ''
}>
<img src="${item.avatar}" alt="${item.author.name}" />
<span>${item.message}</span>
<span>${item.shortSha}</span>
</button>
</li>
`,
)
.join('');
}
$el.setAttribute('aria-hidden', 'false');
} else {
$el.setAttribute('aria-hidden', 'true');
$el.innerHTML = '';
}
}
renderStats() {
const $el = document.querySelector<HTMLElement>('[data-region="stats"]');
if ($el == null) {
return;
}
if (this.state.selected.stats?.changedFiles !== undefined) {
const { added, deleted, changed } = this.state.selected.stats.changedFiles;
$el.innerHTML = `
<commit-stats added="${added}" modified="${changed}" removed="${deleted}"></commit-stats>
`;
} else {
$el.innerHTML = '';
}
}
renderFiles() {
const $el = document.querySelector<HTMLElement>('[data-region="files"]');
if ($el == null) {
return;
}
if (this.state.selected.files?.length > 0) {
$el.innerHTML = this.state.selected.files
.map(
(file: Record<string, any>) => `
<li class="change-list__item">
<file-change-item class="commit-details__file" status="${file.status}" path="${file.path}" repo-path="${file.repoPath}" icon="${file.icon.dark}"></file-change-item>
</li>
`,
)
.join('');
$el.setAttribute('aria-hidden', 'false');
} else {
$el.innerHTML = '';
}
}
renderAuthor() {
const $el = document.querySelector<HTMLElement>('[data-region="author"]');
if ($el == null) {
return;
}
if (this.state.selected.author != null) {
$el.innerHTML = `
<commit-identity
name="${this.state.selected.author.name}"
email="${this.state.selected.author.email}"
date="${this.state.selected.author.date}"
avatar="${this.state.selected.author.avatar}"
></commit-identity>
`;
$el.setAttribute('aria-hidden', 'false');
} else {
$el.innerHTML = '';
$el.setAttribute('aria-hidden', 'true');
}
}
renderCommitter() {
// <li class="commit-details__author" data-region="committer">
// <skeleton-loader></skeleton-loader>
// </li>
const $el = document.querySelector<HTMLElement>('[data-region="committer"]');
if ($el == null) {
return;
}
if (this.state.selected.committer != null) {
$el.innerHTML = `
<commit-identity
name="${this.state.selected.committer.name}"
email="${this.state.selected.committer.email}"
date="${this.state.selected.committer.date}"
avatar="${this.state.selected.committer.avatar}"
committer
></commit-identity>
`;
$el.setAttribute('aria-hidden', 'false');
} else {
$el.innerHTML = '';
$el.setAttribute('aria-hidden', 'true');
}
}
renderTitle() {
// <header class="commit-detail-panel__header" role="banner" aria-hidden="true">
// <h1 class="commit-detail-panel__title">
// <span class="codicon codicon-git-commit commit-detail-panel__title-icon"></span>
// Commit: <span data-region="commit-title"></span>
// </h1>
// </header>
const $el = document.querySelector<HTMLElement>('[data-region="commit-title"]');
if ($el != null) {
$el.innerHTML = this.state.selected.shortSha;
}
}
renderMessage() {
const $el = document.querySelector<HTMLElement>('[data-region="message"]');
if ($el == null) {
return;
}
const [headline, ...lines] = this.state.selected.message.split('\n');
if (lines.length > 1) {
$el.innerHTML = `<strong>${headline}</strong><br>${lines.join('<br>')}`;
} else {
$el.innerHTML = `<strong>${headline}</strong>`;
}
}
renderAutolinks() {
const $el = document.querySelector<HTMLElement>('[data-region="autolinks"]');
if ($el == null) {
return;
}
const $info = document.querySelector<HTMLElement>('[data-region="rich-info"]');
if (this.state.pullRequest != null || this.state.issues?.length > 0) {
$el.setAttribute('aria-hidden', 'false');
$info?.setAttribute('aria-hidden', 'true');
this.renderPullRequest();
this.renderIssues();
} else {
$el.setAttribute('aria-hidden', 'true');
$info?.setAttribute('aria-hidden', 'false');
}
}
renderPullRequest() {
const $el = document.querySelector<HTMLElement>('[data-region="pull-request"]');
if ($el == null) {
return;
}
if (this.state.pullRequest != null) {
$el.innerHTML = `
<issue-pull-request
name="${this.state.pullRequest.title}"
url="${this.state.pullRequest.url}"
key="${this.state.pullRequest.id}"
status="${this.state.pullRequest.state}"
date="${this.state.pullRequest.date}"
></issue-pull-request>
`;
$el.setAttribute('aria-hidden', 'false');
} else {
$el.innerHTML = '';
$el.setAttribute('aria-hidden', 'true');
}
}
renderIssues() {
const $el = document.querySelector<HTMLElement>('[data-region="issue"]');
if ($el == null) {
return;
}
if (this.state.issues?.length > 0) {
$el.innerHTML = this.state.issues
.map(
(issue: Record<string, any>) => `
<issue-pull-request
name="${issue.title}"
url="${issue.url}"
key="${issue.id}"
status="${(issue.closed as boolean) ? 'closed' : 'opened'}"
date="${(issue.closed as boolean) ? issue.closedDate : issue.date}"
></issue-pull-request>
`,
)
.join('');
$el.setAttribute('aria-hidden', 'false');
} else {
$el.innerHTML = '';
$el.setAttribute('aria-hidden', 'true');
}
}
}
new CommitDetailsApp();

+ 4
- 0
src/webviews/apps/shared/base.scss View File

@ -77,6 +77,10 @@ textarea {
margin: 0;
}
button:not(:disabled) {
cursor: pointer;
}
input[type='checkbox'] {
background: none;
border: none;

+ 1505
- 0
src/webviews/apps/shared/components/codicon.ts
File diff suppressed because it is too large
View File


+ 67
- 0
src/webviews/apps/shared/components/commit/commit-identity.ts View File

@ -0,0 +1,67 @@
import { css, html, LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import '../formatted-date';
@customElement('commit-identity')
export class CommitIdentity extends LitElement {
static override styles = css`
:host {
display: grid;
gap: 0.25rem 0.5rem;
justify-content: start;
}
a {
color: var(--color-link-foreground);
text-decoration: none;
}
.avatar {
grid-column: 1;
grid-row: 1 / 3;
width: 36px;
}
.thumb {
width: 100%;
height: auto;
border-radius: 0.4rem;
}
.name {
grid-column: 2;
grid-row: 1;
font-size: 1.5rem;
}
.date {
grid-column: 2;
grid-row: 2;
font-size: 1.2rem;
}
`;
@property()
name = '';
@property()
email = '';
@property()
date = '';
@property()
avatar = 'https://www.gravatar.com/avatar/?s=16&d=robohash';
@property({ type: Boolean, reflect: true })
committer = false;
override render() {
const largerUrl = this.avatar.replace('s=32', 's=64');
return html`
<a class="avatar" href="${this.email ? `mailto:${this.email}` : '#'}"
><img class="thumb" lazy src="${largerUrl}" alt="${this.name}"
/></a>
<a class="name" href="${this.email ? `mailto:${this.email}` : '#'}">${this.name}</a>
<span class="date"
>${this.committer === true ? 'committed' : 'authored'}
<formatted-date date="${this.date}"></formatted-date
></span>
`;
}
}

+ 42
- 0
src/webviews/apps/shared/components/commit/commit-stats.ts View File

@ -0,0 +1,42 @@
import { css, html, LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import '../codicon';
@customElement('commit-stats')
export class CommitStats extends LitElement {
static override styles = css`
:host {
display: inline-flex;
flex-direction: row;
gap: 0.5rem;
}
.added {
color: var(--vscode-gitDecoration-addedResourceForeground);
}
.modified {
color: var(--vscode-gitDecoration-modifiedResourceForeground);
}
.deleted {
color: var(--vscode-gitDecoration-deletedResourceForeground);
}
}
`;
@property({ type: Number })
added = 0;
@property({ type: Number })
modified = 0;
@property({ type: Number })
removed = 0;
override render() {
return html`
<span class="added"><code-icon icon="diff-added"></code-icon> ${this.added}</span>
<span class="modified"><code-icon icon="diff-modified"></code-icon> ${this.modified}</span>
<span class="deleted"><code-icon icon="diff-removed"></code-icon> ${this.removed}</span>
`;
}
}

+ 233
- 0
src/webviews/apps/shared/components/commit/file-change-item.ts View File

@ -0,0 +1,233 @@
import { css, html, LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import '../codicon';
export interface FileChangeItemEventDetail {
path: string;
repoPath: string;
}
// TODO: use the model version
const statusTextMap: Record<string, string> = {
'.': 'Unchanged',
'!': 'Ignored',
'?': 'Untracked',
A: 'Added',
D: 'Deleted',
M: 'Modified',
R: 'Renamed',
C: 'Copied',
AA: 'Conflict',
AU: 'Conflict',
UA: 'Conflict',
DD: 'Conflict',
DU: 'Conflict',
UD: 'Conflict',
UU: 'Conflict',
T: 'Modified',
U: 'Updated but Unmerged',
};
// TODO: use the model version
const statusCodiconsMap: Record<string, string | undefined> = {
'.': undefined,
'!': 'diff-ignored',
'?': 'diff-added',
A: 'diff-added',
D: 'diff-removed',
M: 'diff-modified',
R: 'diff-renamed',
C: 'diff-added',
AA: 'warning',
AU: 'warning',
UA: 'warning',
DD: 'warning',
DU: 'warning',
UD: 'warning',
UU: 'warning',
T: 'diff-modified',
U: 'diff-modified',
};
@customElement('file-change-item')
export class FileChangeItem extends LitElement {
static override styles = css`
:host {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
font-size: var(--vscode-font-size);
line-height: 2rem;
color: var(--vscode-foreground);
}
:host(:hover),
:host(:focus-within) {
background-color: var(--vscode-list-hoverBackground);
}
:host(:focus-within) {
outline: 1px solid var(--vscode-focusBorder);
outline-offset: -1px;
}
* {
box-sizing: border-box;
}
.change-list__link {
width: 100%;
color: inherit;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
text-decoration: none;
outline: none;
}
.change-list__status {
margin-right: 0.33em;
}
.change-list__status-icon {
width: 16px;
aspect-ratio: 1;
vertical-align: text-bottom;
}
.change-list__path {
color: var(--color-background--lighten-50);
}
.change-list__actions {
flex: none;
user-select: none;
display: flex;
align-items: center;
}
:host(:not(:hover):not(:focus-within)) .change-list__actions {
display: none;
}
.change-list__action {
display: inline-flex;
justify-content: center;
align-items: center;
width: 2rem;
height: 2rem;
border-radius: 0.25em;
color: inherit;
padding: 2px;
vertical-align: text-bottom;
text-decoration: none;
}
.change-list__action:hover {
background-color: var(--color-background--lighten-15);
}
`;
@property()
status = '';
@property()
path = '';
@property({ attribute: 'repo-path' })
repoPath = '';
@property()
icon = '';
private renderIcon() {
if (this.icon !== '') {
return html`<img class="change-list__status-icon" src="${this.icon}" />`;
}
const statusIcon = (this.status !== '' && statusCodiconsMap[this.status]) ?? 'file';
return html` <code-icon icon="${statusIcon}"></code-icon> `;
}
override render() {
const statusName = this.status !== '' ? statusTextMap[this.status] : '';
const pathIndex = this.path.lastIndexOf('/');
const fileName = pathIndex > -1 ? this.path.substring(pathIndex + 1) : this.path;
const filePath = pathIndex > -1 ? this.path.substring(0, pathIndex) : '';
return html`
<a class="change-list__link" @click=${this.onComparePrevious} href="#">
<span class="change-list__status" aria-label="${statusName}">${this.renderIcon()}</span
><span class="change-list__filename">${fileName}</span>
<small class="change-list__path">${filePath}</small>
</a>
<nav class="change-list__actions">
<a
class="change-list__action"
@click=${this.onOpenFile}
href="#"
title="Open file"
aria-label="Open file"
><code-icon icon="go-to-file"></code-icon></a
><a
class="change-list__action"
@click=${this.onCompareWorking}
href="#"
title="Compare"
aria-label="Compare"
><code-icon icon="git-compare"></code-icon></a
><a
class="change-list__action"
@click=${this.onOpenFileOnRemote}
href="#"
title="Open on remote"
aria-label="Open on remote"
><code-icon icon="globe"></code-icon></a
><a
class="change-list__action"
@click=${this.onMoreActions}
href="#"
title="Show more actions"
aria-label="Show more actions"
><code-icon icon="ellipsis"></code-icon
></a>
</nav>
`;
}
private onOpenFile(e: Event) {
e.preventDefault();
this.fireEvent('file-open');
}
private onOpenFileOnRemote(e: Event) {
e.preventDefault();
this.fireEvent('file-open-on-remote');
}
private onCompareWorking(e: Event) {
e.preventDefault();
this.fireEvent('file-compare-working');
}
private onComparePrevious(e: Event) {
e.preventDefault();
this.fireEvent('file-compare-previous');
}
private onMoreActions(e: Event) {
e.preventDefault();
this.fireEvent('file-more-actions');
}
private fireEvent(eventName: string) {
const event = new CustomEvent<FileChangeItemEventDetail>(eventName, {
detail: {
path: this.path,
repoPath: this.repoPath,
},
bubbles: true,
composed: true,
});
this.dispatchEvent(event);
}
}

+ 12
- 0
src/webviews/apps/shared/components/converters/date-converter.ts View File

@ -0,0 +1,12 @@
import type { ComplexAttributeConverter } from 'lit';
export const dateConverter = (locale?: string): ComplexAttributeConverter<Date> => {
return {
toAttribute: (date: Date) => {
return date.toLocaleDateString(locale);
},
fromAttribute: (value: string) => {
return new Date(value);
},
};
};

+ 19
- 0
src/webviews/apps/shared/components/formatted-date.ts View File

@ -0,0 +1,19 @@
import { html, LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { formatDate, fromNow } from '../../../../system/date';
import { dateConverter } from './converters/date-converter';
@customElement('formatted-date')
export class FormattedDate extends LitElement {
@property()
format = 'MMMM Do, YYYY h:mma';
@property({ converter: dateConverter(navigator.language), reflect: true })
date = new Date();
override render() {
return html`<time datetime="${this.date}" title="${formatDate(this.date, this.format ?? 'MMMM Do, YYYY h:mma')}"
>${fromNow(this.date)}</time
>`;
}
}

+ 77
- 0
src/webviews/apps/shared/components/rich/issue-pull-request.ts View File

@ -0,0 +1,77 @@
import { css, html, LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import '../formatted-date';
import '../codicon';
@customElement('issue-pull-request')
export class IssuePullRequest extends LitElement {
static override styles = css`
:host {
display: grid;
gap: 0.25rem 0.5rem;
justify-content: start;
}
a {
color: var(--color-link-foreground);
text-decoration: none;
}
.icon {
grid-column: 1;
grid-row: 1 / 3;
color: var(--vscode-gitlens-mergedPullRequestIconColor);
width: 32px;
text-align: center;
}
.title {
grid-column: 2;
grid-row: 1;
margin: 0;
font-size: 1.5rem;
}
.date {
grid-column: 2;
grid-row: 2;
margin: 0;
font-size: 1.2rem;
}
`;
@property()
url = '';
@property()
name = '';
@property()
date = '';
@property()
status = 'merged';
@property()
key = '#1999';
override render() {
const icon =
this.status.toLowerCase() === 'merged'
? 'git-merge'
: this.status.toLowerCase() === 'closed'
? 'pass'
: 'issues';
return html`
<span class="icon"><code-icon icon=${icon}></code-icon></span>
<p class="title">
<a href="${this.url}">${this.name}</a>
</p>
<p class="date">
${this.key} ${this.status}
<formatted-date date="${this.date}"></formatted-date>
</p>
`;
}
}

+ 57
- 0
src/webviews/apps/shared/components/skeleton-loader.ts View File

@ -0,0 +1,57 @@
import { css, html, LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';
// height: calc(1em * var(--skeleton-line-height, 1.2) * var(--skeleton-lines, 1));
// background-color: var(--color-background--lighten-30);
@customElement('skeleton-loader')
export class SkeletonLoader extends LitElement {
static override styles = css`
:host {
--skeleton-line-height: 1.2;
--skeleton-lines: 1;
}
.skeleton {
position: relative;
display: block;
overflow: hidden;
border-radius: 0.25em;
width: 100%;
height: calc(1em * var(--skeleton-line-height, 1.2) * var(--skeleton-lines, 1));
background-color: var(--color-background--lighten-15);
}
.skeleton::before {
content: '';
position: absolute;
display: block;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-image: linear-gradient(
to right,
transparent 0%,
var(--color-background--lighten-15) 20%,
var(--color-background--lighten-30) 60%,
transparent 100%
);
transform: translateX(-100%);
animation: skeleton-loader 2s ease-in-out infinite;
}
@keyframes skeleton-loader {
100% {
transform: translateX(100%);
}
}
`;
@property({ type: Number })
lines = 1;
override render() {
const style = `--skeleton-lines: ${this.lines};`;
return html`<div class="skeleton" style=${style}></div>`;
}
}

+ 97
- 0
src/webviews/apps/shared/components/webview-pane.ts View File

@ -0,0 +1,97 @@
import { css, html, LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import './codicon';
@customElement('webview-pane')
export class WebviewPane extends LitElement {
static override styles = css`
:host {
display: flex;
flex-direction: column;
background-color: var(--color-view-background);
color: var(--color-view-foreground);
}
* {
box-sizing: border-box;
}
.header {
flex: none;
display: flex;
background-color: var(--color-view-background);
color: var(--color-view-header-foreground);
border-top: 1px solid var(--vscode-panel-border);
}
.header:focus-within {
outline: 1px solid var(--vscode-focusBorder);
outline-offset: -1px;
}
.title {
appearance: none;
width: 100%;
padding: 0;
border: none;
text-align: left;
line-height: 2.2rem;
font-weight: bold;
background: transparent;
color: inherit;
cursor: pointer;
outline: none;
}
.icon {
font-weight: normal;
margin: 0 0.2rem;
}
.content {
overflow: auto;
/* scrollbar-gutter: stable; */
box-shadow: #000000 0 0.6rem 0.6rem -0.6rem inset;
padding-top: 0.6rem;
}
:host([collapsable]:not([expanded])) .content {
display: none;
}
`;
@property({ type: Boolean, reflect: true })
collapsable = false;
@property({ type: Boolean, reflect: true })
expanded = false;
renderTitle() {
if (!this.collapsable) {
return html`<div class="title">${this.title}</div>`;
}
return html`<button
type="button"
class="title"
aria-controls="content"
aria-expanded=${this.expanded}
@click="${this.toggleExpanded}"
>
<code-icon class="icon" icon=${this.expanded ? 'chevron-down' : 'chevron-right'}></code-icon
><slot name="title">Section</slot>
</button>`;
}
override render() {
return html`
<header class="header">${this.renderTitle()}</header>
<div id="content" role="region" class="content">
<slot></slot>
</div>
`;
}
private toggleExpanded() {
this.expanded = !this.expanded;
}
}

+ 7
- 1
src/webviews/apps/shared/dom.ts View File

@ -29,10 +29,16 @@ export namespace DOM {
listener: (e: DocumentEventMap[K], target: T) => void,
options?: boolean | AddEventListenerOptions,
): Disposable;
export function on<T extends HTMLElement, K>(
selector: string,
name: string,
listener: (e: CustomEvent<K>, target: T) => void,
options?: boolean | AddEventListenerOptions,
): Disposable;
export function on<K extends keyof (DocumentEventMap | WindowEventMap), T extends Document | Element | Window>(
sourceOrSelector: string | Window | Document | Element,
name: K,
listener: (e: (DocumentEventMap | WindowEventMap)[K], target: T) => void,
listener: (e: (DocumentEventMap | WindowEventMap)[K] | CustomEvent<K>, target: T) => void,
options?: boolean | AddEventListenerOptions,
): Disposable {
let disposed = false;

+ 198
- 0
src/webviews/commitDetails/commitDetailsWebview.ts View File

@ -0,0 +1,198 @@
import { ProgressLocation, window } from 'vscode';
import { Commands } from '../../constants';
import type { Container } from '../../container';
import { GitCommit, GitRemote, IssueOrPullRequest } from '../../git/models';
import { RichRemoteProvider } from '../../git/remotes/provider';
import { debug } from '../../system/decorators/log';
import { WebviewBase } from '../webviewBase';
import type { CommitDetails, CommitSummary, ShowCommitDetailsPageCommandArgs, State } from './protocol';
export class CommitDetailsWebview extends WebviewBase<State> {
private shaList: string[] = [
'7224b547bbaa3a643e89ceb515dfb7cbad83aa26',
'f55b2ad418a05a51c381c667e5e87d0435883cfc',
];
private selectedSha: string | undefined = 'f55b2ad418a05a51c381c667e5e87d0435883cfc';
constructor(container: Container) {
super(
container,
'gitlens.commitDetails',
'commitDetails.html',
'images/gitlens-icon.png',
'Commit Details',
Commands.ShowCommitDetailsPage,
);
}
private updateShaList(refs?: string[]) {
let refsList;
if (refs?.length && refs.length > 0) {
refsList = refs;
} else {
// TODO: replace with quick pick for a commit
refsList = ['7224b547bbaa3a643e89ceb515dfb7cbad83aa26', 'f55b2ad418a05a51c381c667e5e87d0435883cfc'];
}
this.shaList = refsList;
if (this.selectedSha && !this.shaList.includes(this.selectedSha)) {
// TODO: maybe make a quick pick for the list of commits?
this.selectedSha = this.shaList[0];
}
}
protected override onShowCommand(refs?: ShowCommitDetailsPageCommandArgs): void {
// TODO: get args from command
this.updateShaList(refs);
super.onShowCommand();
}
private async getLinkedIssuesAndPullRequests(
message: string,
remote: GitRemote<RichRemoteProvider>,
): Promise<IssueOrPullRequest[] | undefined> {
try {
const issueSearch = await this.container.autolinks.getLinkedIssuesAndPullRequests(message, remote);
console.log('CommitDetailsWebview getLinkedIssuesAndPullRequests', issueSearch);
if (issueSearch != null) {
const filteredIssues = Array.from(issueSearch.values()).filter(
value => value != null,
) as IssueOrPullRequest[];
return filteredIssues;
}
return undefined;
} catch (e) {
console.error(e);
return undefined;
}
}
private async getRichContent(selected: GitCommit): Promise<Record<string, any>> {
const pullRequest = selected != null ? await selected.getAssociatedPullRequest() : undefined;
console.log('CommitDetailsWebview pullRequest', pullRequest);
const issues: Record<string, any>[] = [];
let formattedMessage;
if (selected?.message !== undefined && typeof selected.message === 'string') {
const remote = await this.container.git.getBestRemoteWithRichProvider(selected.repoPath);
console.log('CommitDetailsWebview remote', remote);
if (remote != null) {
formattedMessage = this.container.autolinks.linkify(selected.message, true, [remote]);
const issueSearch = await this.getLinkedIssuesAndPullRequests(selected.message, remote);
console.log('CommitDetailsWebview issueSearch', issueSearch);
if (issueSearch !== undefined) {
issues.push(...issueSearch);
}
}
}
return {
formattedMessage: formattedMessage,
pullRequest: pullRequest,
issues: issues?.length ? issues : undefined,
};
}
@debug({ args: false })
protected async getState(init = false): Promise<State> {
const repo = this.container.git.openRepositories?.[0];
console.log('CommitDetailsWebview repo', repo);
if (repo === undefined) {
return {
commits: [],
};
}
const commitPromises = this.shaList.map(sha => repo.getCommit(sha));
const results = await Promise.all(commitPromises);
console.log('CommitDetailsWebview results', results);
const commits = results.filter(commit => commit !== undefined) as GitCommit[];
const selected = commits.find(commit => commit.sha === this.selectedSha);
console.log('CommitDetailsWebview selected', selected);
// const pullRequest = selected != null ? await selected.getAssociatedPullRequest() : undefined;
// console.log('CommitDetailsWebview pullRequest', pullRequest);
// const issues: Record<string, any>[] = [];
// let formattedMessage;
// if (selected?.message !== undefined && typeof selected.message === 'string') {
// const remote = await this.container.git.getBestRemoteWithRichProvider(selected.repoPath);
// console.log('CommitDetailsWebview remote', remote);
// if (remote != null) {
// formattedMessage = this.container.autolinks.linkify(selected.message, true, [remote]);
// const issueSearch = await this.getLinkedIssuesAndPullRequests(selected.message, remote);
// console.log('CommitDetailsWebview issueSearch', issueSearch);
// if (issueSearch !== undefined) {
// issues.push(...issueSearch);
// }
// }
// }
const richContent = !init && selected != null ? await this.getRichContent(selected) : undefined;
let formattedCommit;
if (selected !== undefined) {
formattedCommit = await getDetailsModel(selected, richContent?.formattedMessage);
}
const commitChoices = await Promise.all(commits.map(async commit => summaryModel(commit)));
return {
// TODO: keep state of the selected commit
commits: commitChoices,
selected: formattedCommit,
pullRequest: richContent?.pullRequest,
issues: richContent?.issues,
};
}
protected override async includeBootstrap() {
return window.withProgress({ location: ProgressLocation.Window, title: 'Loading webview...' }, () =>
this.getState(true),
);
}
}
async function summaryModel(commit: GitCommit): Promise<CommitSummary> {
return {
sha: commit.sha,
shortSha: commit.shortSha,
summary: commit.summary,
message: commit.message,
author: commit.author,
avatar: (await commit.getAvatarUri())?.toString(true),
};
}
async function getDetailsModel(commit: GitCommit, formattedMessage?: string): Promise<CommitDetails | undefined> {
if (commit === undefined) {
return;
}
const authorAvatar = await commit.author?.getAvatarUri(commit);
const committerAvatar = await commit.committer?.getAvatarUri(commit);
return {
sha: commit.sha,
shortSha: commit.shortSha,
summary: commit.summary,
message: formattedMessage ?? commit.message,
author: { ...commit.author, avatar: authorAvatar?.toString(true) },
committer: { ...commit.committer, avatar: committerAvatar?.toString(true) },
files: commit.files?.map(({ repoPath, path, status }) => ({ repoPath: repoPath, path: path, status: status })),
stats: commit.stats,
};
}

+ 347
- 0
src/webviews/commitDetails/commitDetailsWebviewView.ts View File

@ -0,0 +1,347 @@
import { Uri, window } from 'vscode';
import type {
DiffWithPreviousCommandArgs,
DiffWithWorkingCommandArgs,
OpenFileOnRemoteCommandArgs,
} from '../../commands';
import { Commands, CoreCommands } from '../../constants';
import type { Container } from '../../container';
import { GitUri } from '../../git/gitUri';
import { GitCommit, GitFile, IssueOrPullRequest } from '../../git/models';
import { executeCommand, executeCoreCommand } from '../../system/command';
import { debug } from '../../system/decorators/log';
import { IpcMessage, onIpc } from '../protocol';
import { WebviewViewBase } from '../webviewViewBase';
import {
CommitActionsCommandType,
CommitDetails,
CommitSummary,
FileComparePreviousCommandType,
FileCompareWorkingCommandType,
FileMoreActionsCommandType,
FileParams,
OpenFileCommandType,
OpenFileOnRemoteCommandType,
PickCommitCommandType,
RichCommitDetails,
RichContentNotificationType,
State,
} from './protocol';
export class CommitDetailsWebviewView extends WebviewViewBase<State> {
private originalTitle?: string;
private commits?: GitCommit[];
private selectedCommit?: GitCommit;
private loadedOnce = false;
constructor(container: Container) {
super(container, 'gitlens.views.commitDetails', 'commitDetails.html', 'Commit Details');
this.originalTitle = this.title;
}
override async show(options?: { commit?: GitCommit; preserveFocus?: boolean | undefined }): Promise<void> {
if (options?.commit != null) {
this.selectCommit(options.commit);
void this.refresh();
}
return super.show(options != null ? { preserveFocus: options.preserveFocus } : undefined);
}
protected override onMessageReceived(e: IpcMessage) {
switch (e.method) {
case OpenFileOnRemoteCommandType.method:
onIpc(OpenFileOnRemoteCommandType, e, params => {
this.openFileOnRemote(params);
});
break;
case OpenFileCommandType.method:
onIpc(OpenFileCommandType, e, params => {
this.openFile(params);
});
break;
case FileCompareWorkingCommandType.method:
onIpc(FileCompareWorkingCommandType, e, params => {
this.openFileComparisonWithWorking(params);
});
break;
case FileComparePreviousCommandType.method:
onIpc(FileComparePreviousCommandType, e, params => {
this.openFileComparisonWithPrevious(params);
});
break;
case FileMoreActionsCommandType.method:
onIpc(FileMoreActionsCommandType, e, params => {
this.showFileActions(params);
});
break;
case CommitActionsCommandType.method:
onIpc(CommitActionsCommandType, e, params => {
this.showCommitActions();
});
break;
case PickCommitCommandType.method:
onIpc(PickCommitCommandType, e, params => {
this.showCommitSearch();
});
break;
}
}
private getFileFromParams(params: FileParams): GitFile | undefined {
return this.selectedCommit?.files?.find(file => file.path === params.path && file.repoPath === params.repoPath);
}
private showCommitSearch() {
void executeCommand(Commands.SearchCommits, {
showResultsInDetails: true,
});
}
private showCommitActions() {
if (this.selectedCommit === undefined) {
return;
}
void executeCommand(Commands.ShowQuickCommit, {
commit: this.selectedCommit,
});
}
private showFileActions(params: FileParams) {
const file = this.getFileFromParams(params);
if (this.selectedCommit === undefined || file === undefined) {
return;
}
const uri = GitUri.fromFile(file, this.selectedCommit.repoPath, this.selectedCommit.sha);
void executeCommand(Commands.ShowQuickCommitFile, uri, {
sha: this.selectedCommit.sha,
});
}
private openFileComparisonWithWorking(params: FileParams) {
const file = this.getFileFromParams(params);
if (this.selectedCommit === undefined || file === undefined) {
return;
}
const uri = GitUri.fromFile(file, this.selectedCommit.repoPath, this.selectedCommit.sha);
void executeCommand<[Uri, DiffWithWorkingCommandArgs]>(Commands.DiffWithWorking, uri, {
showOptions: {
preserveFocus: true,
preview: true,
},
});
}
private openFileComparisonWithPrevious(params: FileParams) {
const file = this.getFileFromParams(params);
if (this.selectedCommit === undefined || file === undefined) {
return;
}
const uri = GitUri.fromFile(file, this.selectedCommit.repoPath, this.selectedCommit.sha);
const line = this.selectedCommit.lines.length ? this.selectedCommit.lines[0].line - 1 : 0;
void executeCommand<[Uri, DiffWithPreviousCommandArgs]>(Commands.DiffWithPrevious, uri, {
commit: this.selectedCommit,
line: line,
showOptions: {
preserveFocus: true,
preview: true,
},
});
}
private openFile(params: FileParams) {
const file = this.getFileFromParams(params);
if (this.selectedCommit === undefined || file === undefined) {
return;
}
const uri = GitUri.fromFile(file, this.selectedCommit.repoPath, this.selectedCommit.sha);
void executeCoreCommand(CoreCommands.Open, uri, { background: false, preview: false });
}
private openFileOnRemote(params: FileParams) {
const file = this.getFileFromParams(params);
if (this.selectedCommit === undefined || file === undefined) {
return;
}
const uri = GitUri.fromFile(file, this.selectedCommit.repoPath, this.selectedCommit.sha);
void executeCommand<[Uri, OpenFileOnRemoteCommandArgs]>(Commands.OpenFileOnRemote, uri, {
sha: this.selectedCommit?.sha,
});
}
private copyRemoteFileUrl() {}
private async getRichContent(selected: GitCommit): Promise<RichCommitDetails> {
const pullRequest = selected != null ? await selected.getAssociatedPullRequest() : undefined;
console.log('CommitDetailsWebview pullRequest', pullRequest);
const issues: Record<string, any>[] = [];
let formattedMessage;
if (selected?.message !== undefined && typeof selected.message === 'string') {
const remote = await this.container.git.getBestRemoteWithRichProvider(selected.repoPath);
console.log('CommitDetailsWebview remote', remote);
if (remote != null) {
const issueSearch = await this.container.autolinks.getLinkedIssuesAndPullRequests(
selected.message,
remote,
);
// TODO: add HTML formatting option to linkify
// formattedMessage = this.container.autolinks.linkify(
// escapeMarkdown(selected.message, { quoted: true }),
// true,
// [remote],
// // issueSearch,
// );
formattedMessage = this.container.autolinks.linkify(
encodeMarkup(selected.message),
true,
[remote],
// issueSearch,
);
let filteredIssues;
if (issueSearch != null) {
if (pullRequest !== undefined) {
issueSearch.delete(pullRequest.id);
}
filteredIssues = Array.from(issueSearch.values()).filter(
value => value != null,
) as IssueOrPullRequest[];
}
console.log('CommitDetailsWebview filteredIssues', filteredIssues);
if (filteredIssues !== undefined) {
issues.push(...filteredIssues);
}
}
}
return {
formattedMessage: formattedMessage,
pullRequest: pullRequest,
issues: issues?.length ? issues : undefined,
};
}
private selectCommit(commit: GitCommit) {
this.commits = [commit];
this.selectedCommit = commit;
this.title = `${this.originalTitle}${
this.selectedCommit !== undefined ? `: ${this.selectedCommit.shortSha}` : ''
}`;
}
@debug({ args: false })
protected async getState(includeRichContent = true): Promise<State | undefined> {
if (this.commits === undefined) {
return;
}
console.log('CommitDetailsWebview selected', this.selectedCommit);
let richContent;
let formattedCommit;
if (this.selectedCommit !== undefined) {
if (includeRichContent) {
richContent = await this.getRichContent(this.selectedCommit);
}
formattedCommit = await this.getDetailsModel(this.selectedCommit, richContent?.formattedMessage);
}
const commitChoices = await Promise.all(this.commits.map(async commit => summaryModel(commit)));
return {
includeRichContent: includeRichContent,
commits: commitChoices,
selected: formattedCommit,
pullRequest: richContent?.pullRequest,
issues: richContent?.issues,
};
}
protected override async includeBootstrap() {
return window.withProgress({ location: { viewId: this.id } }, async () => {
const state = await this.getState(this.loadedOnce);
if (state === undefined) {
return {};
}
if (this.loadedOnce === false) {
void this.updateRichContent();
this.loadedOnce = true;
}
return state;
});
}
private async updateRichContent() {
if (this.selectedCommit === undefined) {
return;
}
const richContent = await this.getRichContent(this.selectedCommit);
if (richContent != null) {
void this.notify(RichContentNotificationType, richContent);
}
}
private async getDetailsModel(commit: GitCommit, formattedMessage?: string): Promise<CommitDetails | undefined> {
if (commit === undefined) {
return;
}
const authorAvatar = await commit.author?.getAvatarUri(commit);
// const committerAvatar = await commit.committer?.getAvatarUri(commit);
return {
sha: commit.sha,
shortSha: commit.shortSha,
summary: commit.summary,
message: formattedMessage ?? encodeMarkup(commit.message ?? ''),
author: { ...commit.author, avatar: authorAvatar?.toString(true) },
// committer: { ...commit.committer, avatar: committerAvatar?.toString(true) },
files: commit.files?.map(({ repoPath, path, status }) => {
const icon = GitFile.getStatusIcon(status);
return {
repoPath: repoPath,
path: path,
status: status,
icon: {
dark: this._view!.webview.asWebviewUri(
Uri.joinPath(this.container.context.extensionUri, 'images', 'dark', icon),
).toString(),
light: this._view!.webview.asWebviewUri(
Uri.joinPath(this.container.context.extensionUri, 'images', 'light', icon),
).toString(),
},
};
}),
stats: commit.stats,
};
}
}
async function summaryModel(commit: GitCommit): Promise<CommitSummary> {
return {
sha: commit.sha,
shortSha: commit.shortSha,
summary: commit.summary,
message: commit.message,
author: commit.author,
avatar: (await commit.getAvatarUri())?.toString(true),
};
}
function encodeMarkup(text: string): string {
return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}

+ 49
- 0
src/webviews/commitDetails/protocol.ts View File

@ -0,0 +1,49 @@
import { IpcCommandType, IpcNotificationType } from '../protocol';
export type CommitSummary = {
sha: string;
summary: string;
message?: string;
author: Record<string, any>;
shortSha: string;
avatar?: string;
};
export type CommitDetails = {
committer?: Record<string, any>;
files?: Record<string, any>[];
stats?: Record<string, any>;
} & CommitSummary;
export type RichCommitDetails = {
formattedMessage?: string;
pullRequest?: Record<string, any>;
issues?: Record<string, any>[];
};
export type State = {
commits?: CommitSummary[];
} & Record<string, any>;
export type ShowCommitDetailsPageCommandArgs = string[];
// COMMANDS
export interface FileParams {
path: string;
repoPath: string;
}
export const OpenFileOnRemoteCommandType = new IpcCommandType<FileParams>('commit/file/openOnRemote');
export const OpenFileCommandType = new IpcCommandType<FileParams>('commit/file/open');
export const FileCompareWorkingCommandType = new IpcCommandType<FileParams>('commit/file/compareWorking');
export const FileComparePreviousCommandType = new IpcCommandType<FileParams>('commit/file/comparePrevious');
export const FileMoreActionsCommandType = new IpcCommandType<FileParams>('commit/file/moreActions');
export const CommitActionsCommandType = new IpcCommandType<undefined>('commit/moreActions');
export const PickCommitCommandType = new IpcCommandType<undefined>('commit/pickCommit');
// NOTIFICATIONS
export interface DidChangeParams {
state: State;
}
export const DidChangeNotificationType = new IpcNotificationType<DidChangeParams>('commit/didChange');
export const RichContentNotificationType = new IpcNotificationType<RichCommitDetails>('commit/richContent');

+ 1
- 1
src/webviews/webviewViewBase.ts View File

@ -42,7 +42,7 @@ export abstract class WebviewViewBase implements WebviewViewProvider, Dis
protected readonly disposables: Disposable[] = [];
protected isReady: boolean = false;
private _disposableView: Disposable | undefined;
private _view: WebviewView | undefined;
protected _view: WebviewView | undefined;
constructor(
protected readonly container: Container,

+ 1
- 1
tsconfig.base.json View File

@ -20,7 +20,7 @@
"sourceMap": true,
"strict": true,
"target": "es2020",
"useDefineForClassFields": true,
"useDefineForClassFields": false,
"useUnknownInCatchVariables": false
},
"include": ["src/**/*"]

+ 5
- 3
webpack.config.js View File

@ -266,6 +266,7 @@ function getWebviewsConfig(mode, env) {
getHtmlPlugin('settings', false, mode, env),
getHtmlPlugin('timeline', true, mode, env),
getHtmlPlugin('welcome', false, mode, env),
getHtmlPlugin('commitDetails', false, mode, env),
getCspHtmlPlugin(mode, env),
new InlineChunkHtmlPlugin(HtmlPlugin, mode === 'production' ? ['\\.css$'] : []),
new CopyPlugin({
@ -309,6 +310,7 @@ function getWebviewsConfig(mode, env) {
settings: './settings/settings.ts',
timeline: './plus/timeline/timeline.ts',
welcome: './welcome/welcome.ts',
commitDetails: './commitDetails/commitDetails.ts',
},
mode: mode,
target: 'web',
@ -441,7 +443,7 @@ function getCspHtmlPlugin(mode, env) {
mode !== 'production'
? ['#{cspSource}', "'nonce-#{cspNonce}'", "'unsafe-eval'"]
: ['#{cspSource}', "'nonce-#{cspNonce}'"],
'style-src': ['#{cspSource}', "'nonce-#{cspNonce}'", "'unsafe-hashes'"],
'style-src': ['#{cspSource}', "'unsafe-hashes'", "'unsafe-inline'"],
'font-src': ['#{cspSource}'],
},
{
@ -449,11 +451,11 @@ function getCspHtmlPlugin(mode, env) {
hashingMethod: 'sha256',
hashEnabled: {
'script-src': true,
'style-src': true,
'style-src': false,
},
nonceEnabled: {
'script-src': true,
'style-src': true,
'style-src': false,
},
},
);

+ 66
- 1
yarn.lock View File

@ -125,11 +125,21 @@
methods "^1.1.2"
path-to-regexp "^6.1.0"
"@microsoft/fast-element@^1.10.4", "@microsoft/fast-element@^1.6.2", "@microsoft/fast-element@^1.9.0":
"@lit/reactive-element@^1.3.0":
version "1.3.3"
resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.3.3.tgz#851de2bd28d6c3378a816a28d2505075931559c2"
integrity sha512-ukelZ49tzUqgOAEbVujl/U62JNK3wdn5kKtXVqrjKND4QvHACZOMOYaZI6/5Jd8vsg+Fq9HDwiib70FBLydOiQ==
"@microsoft/fast-element@^1.10.4":
version "1.10.4"
resolved "https://registry.yarnpkg.com/@microsoft/fast-element/-/fast-element-1.10.4.tgz#c1929cdcf73d665f2278a7fd3a8857bcddd733d9"
integrity sha512-SD0L3Kt++VSTqdkmGupB5tNaSLboEB7H/rh70a4eECpzCQewEzjd85jVNpgab1A8n5d3N9sPwZGIyfiUN6x4hg==
"@microsoft/fast-element@^1.6.2", "@microsoft/fast-element@^1.9.0":
version "1.10.3"
resolved "https://registry.yarnpkg.com/@microsoft/fast-element/-/fast-element-1.10.3.tgz#0d513583207c2a33e6ec020c366d836c21423fca"
integrity sha512-ns/EEo5WSXNwRBe29O7sSA4SSqlapyHESXBT+JAcrR/3i0fLYQFMO/PdzfEMhsXmoUkZny6ewVbM4CttZa94Kg==
"@microsoft/fast-foundation@^2.38.0", "@microsoft/fast-foundation@^2.41.1":
version "2.46.11"
resolved "https://registry.yarnpkg.com/@microsoft/fast-foundation/-/fast-foundation-2.46.11.tgz#4761bb3b923875a50fd0ea01c45d8ce56f273477"
@ -398,6 +408,11 @@
resolved "https://registry.yarnpkg.com/@types/sortablejs/-/sortablejs-1.13.0.tgz#870223438f8f2cd81157b128a4c0261adbcaa946"
integrity sha512-C3064MH72iEfeGCYEGCt7FCxXoAXaMPG0QPnstcxvPmbl54erpISu06d++FY37Smja64iWy5L8wOyHHBghWbJQ==
"@types/trusted-types@^2.0.2":
version "2.0.2"
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.2.tgz#fc25ad9943bcac11cceb8168db4f275e0e72e756"
integrity sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==
"@types/uuid@8.3.4":
version "8.3.4"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc"
@ -2378,6 +2393,15 @@ eslint-plugin-import@2.26.0:
resolve "^1.22.0"
tsconfig-paths "^3.14.1"
eslint-plugin-lit@^1.6.1:
version "1.6.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-lit/-/eslint-plugin-lit-1.6.1.tgz#e1f51fe9e580d4095b58cc4bc4dc6b44409af6b0"
integrity sha512-BpPoWVhf8dQ/Sz5Pi9NlqbGoH5BcMcVyXhi2XTx2XGMAO9U2lS+GTSsqJjI5hL3OuxCicNiUEWXazAwi9cAGxQ==
dependencies:
parse5 "^6.0.1"
parse5-htmlparser2-tree-adapter "^6.0.1"
requireindex "^1.2.0"
eslint-scope@5.1.1, eslint-scope@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
@ -3856,6 +3880,30 @@ listenercount@~1.0.1:
resolved "https://registry.yarnpkg.com/listenercount/-/listenercount-1.0.1.tgz#84c8a72ab59c4725321480c975e6508342e70937"
integrity sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==
lit-element@^3.2.0:
version "3.2.1"
resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-3.2.1.tgz#f917d22451b848768f84164d41eb5e18903986e3"
integrity sha512-2PxyE9Yq9Jyo/YBK2anycaHcqo93YvB5D+24JxloPVqryW/BOXekne+jGsm0Ke3E5E2v7CDgkmpEmCAzYfrHCQ==
dependencies:
"@lit/reactive-element" "^1.3.0"
lit-html "^2.2.0"
lit-html@^2.2.0:
version "2.2.6"
resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-2.2.6.tgz#e70679605420a34c4f3cbd0c483b2fb1fff781df"
integrity sha512-xOKsPmq/RAKJ6dUeOxhmOYFjcjf0Q7aSdfBJgdJkOfCUnkmmJPxNrlZpRBeVe1Gg50oYWMlgm6ccAE/SpJgSdw==
dependencies:
"@types/trusted-types" "^2.0.2"
lit@^2.2.7:
version "2.2.7"
resolved "https://registry.yarnpkg.com/lit/-/lit-2.2.7.tgz#a563e8851db1f131912f510129dcc9a42324e838"
integrity sha512-WXYujlKFwme5ZqXOZoWuRVZQAwy7scbcVT3wCbAOHefOxyscqjywWGlF2e6nnC9E64yP9l2ZQlN8wZcRlrjUMQ==
dependencies:
"@lit/reactive-element" "^1.3.0"
lit-element "^3.2.0"
lit-html "^2.2.0"
loader-runner@^4.2.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1"
@ -4584,6 +4632,13 @@ parse-semver@^1.1.1:
dependencies:
semver "^5.1.0"
parse5-htmlparser2-tree-adapter@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6"
integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==
dependencies:
parse5 "^6.0.1"
parse5-htmlparser2-tree-adapter@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz#23c2cc233bcf09bb7beba8b8a69d46b08c62c2f1"
@ -4592,6 +4647,11 @@ parse5-htmlparser2-tree-adapter@^7.0.0:
domhandler "^5.0.2"
parse5 "^7.0.0"
parse5@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
parse5@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.0.0.tgz#51f74a5257f5fcc536389e8c2d0b3802e1bfa91a"
@ -5062,6 +5122,11 @@ require-from-string@^2.0.2:
resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
requireindex@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/requireindex/-/requireindex-1.2.0.tgz#3463cdb22ee151902635aa6c9535d4de9c2ef1ef"
integrity sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==
resolve-cwd@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d"

Loading…
Cancel
Save