Browse Source

Matches vscode alt modifier & dblclick behavior

main
Eric Amodio 1 year ago
parent
commit
b9dc1d3daa
4 changed files with 79 additions and 318 deletions
  1. +0
    -273
      src/webviews/apps/shared/components/commit/file-change-item.ts
  2. +27
    -21
      src/webviews/apps/shared/components/list/file-change-list-item.ts
  3. +26
    -18
      src/webviews/apps/shared/components/list/list-container.ts
  4. +26
    -6
      src/webviews/apps/shared/components/list/list-item.ts

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

@ -1,273 +0,0 @@
import { css, html, LitElement, nothing } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import type { FileShowOptions } from '../../../../commitDetails/protocol';
import '../code-icon';
export interface FileChangeItemEventDetail {
path: string;
repoPath: string;
showOptions?: FileShowOptions;
}
// 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-sideBar-foreground);
}
:host(:hover) {
color: var(--vscode-list-hoverForeground);
background-color: var(--vscode-list-hoverBackground);
}
:host(:focus-within) {
outline: 1px solid var(--vscode-list-focusOutline);
outline-offset: -1px;
color: var(--vscode-list-activeSelectionForeground);
background-color: var(--vscode-list-activeSelectionBackground);
}
* {
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.6rem;
}
.change-list__status-icon {
width: 16px;
aspect-ratio: 1;
vertical-align: text-bottom;
}
.change-list__path {
opacity: 0.7;
margin-left: 0.3rem;
}
.change-list__actions {
flex: none;
user-select: none;
display: flex;
align-items: center;
color: var(--vscode-icon-foreground);
}
:host(:focus-within) .change-list__actions {
color: var(--vscode-list-activeSelectionIconForeground);
}
: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:focus {
outline: 1px solid var(--vscode-focusBorder);
outline-offset: -1px;
}
.change-list__action:hover {
background-color: var(--vscode-toolbar-hoverBackground);
}
.change-list__action:active {
background-color: var(--vscode-toolbar-activeBackground);
}
`;
@property()
status = '';
@property()
path = '';
@property({ attribute: 'repo-path' })
repoPath = '';
@property()
icon = '';
@property({ type: Boolean, reflect: true })
stash = false;
@property({ type: Boolean, reflect: true })
uncommitted = false;
@property({ type: Boolean, reflect: true })
tree = false;
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 focus(options?: FocusOptions | undefined): void {
this.shadowRoot?.getElementById('item')?.focus(options);
}
open(showOptions?: FileShowOptions): void {
this.onComparePrevious(undefined, showOptions);
}
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 = !this.tree && pathIndex > -1 ? this.path.substring(0, pathIndex) : '';
return html`
<a id="item" class="change-list__link" @click=${this.onComparePrevious} href="#">
<span class="change-list__status" title="${statusName}" aria-label="${statusName}"
>${this.renderIcon()}</span
><span class="change-list__filename">${fileName}</span>
${!this.tree ? html`<small class="change-list__path">${filePath}</small>` : nothing}
</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
>${!this.uncommitted
? html`<a
class="change-list__action"
@click=${this.onCompareWorking}
href="#"
title="Open Changes with Working File"
aria-label="Open Changes with Working File"
><code-icon icon="git-compare"></code-icon
></a>`
: nothing}${!this.stash && !this.uncommitted
? html`<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>`
: nothing}
</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, showOptions?: FileShowOptions) {
e?.preventDefault();
this.fireEvent('file-compare-previous', showOptions);
}
private onMoreActions(e: Event) {
e.preventDefault();
this.fireEvent('file-more-actions');
}
private fireEvent(eventName: string, showOptions?: FileShowOptions) {
const event = new CustomEvent<FileChangeItemEventDetail>(eventName, {
detail: {
path: this.path,
repoPath: this.repoPath,
showOptions: showOptions,
},
bubbles: true,
composed: true,
});
this.dispatchEvent(event);
}
}

+ 27
- 21
src/webviews/apps/shared/components/list/file-change-list-item.ts View File

@ -1,10 +1,12 @@
import { attr, css, customElement, FASTElement, html, ref, volatile, when } from '@microsoft/fast-element'; import { attr, css, customElement, FASTElement, html, ref, volatile, when } from '@microsoft/fast-element';
import type { TextDocumentShowOptions } from 'vscode'; import type { TextDocumentShowOptions } from 'vscode';
import { numberConverter } from '../converters/number-converter'; import { numberConverter } from '../converters/number-converter';
import type { ListItem } from './list-item';
import './list-item';
import type { ListItem, ListItemSelectedEvent } from './list-item';
import '../code-icon'; import '../code-icon';
// Can only import types from 'vscode'
const BesideViewColumn = -2; /*ViewColumn.Beside*/
export interface FileChangeListItemDetail { export interface FileChangeListItemDetail {
path: string; path: string;
repoPath: string; repoPath: string;
@ -20,7 +22,7 @@ const template = html`
active="${x => x.active}" active="${x => x.active}"
expanded="${x => x.expanded}" expanded="${x => x.expanded}"
parentexpanded="${x => x.parentexpanded}" parentexpanded="${x => x.parentexpanded}"
@selected="${(x, c) => x.onComparePrevious(c.event)}"
@selected="${(x, c) => x.onComparePrevious(c.event as ListItemSelectedEvent)}"
> >
<img slot="icon" src="${x => x.icon}" title="${x => x.statusName}" alt="${x => x.statusName}" /> <img slot="icon" src="${x => x.icon}" title="${x => x.statusName}" alt="${x => x.statusName}" />
${x => x.fileName} ${x => x.fileName}
@ -28,7 +30,7 @@ const template = html`
<span slot="actions"> <span slot="actions">
<a <a
class="change-list__action" class="change-list__action"
@click="${(x, c) => x.onOpenFile(c.event)}"
@click="${(x, c) => x.onOpenFile(c.event as MouseEvent)}"
href="#" href="#"
title="Open file" title="Open file"
aria-label="Open file" aria-label="Open file"
@ -39,7 +41,7 @@ const template = html`
html<FileChangeListItem>` html<FileChangeListItem>`
<a <a
class="change-list__action" class="change-list__action"
@click="${(x, c) => x.onCompareWorking(c.event)}"
@click="${(x, c) => x.onCompareWorking(c.event as MouseEvent)}"
href="#" href="#"
title="Open Changes with Working File" title="Open Changes with Working File"
aria-label="Open Changes with Working File" aria-label="Open Changes with Working File"
@ -49,14 +51,14 @@ const template = html`
x => !x.stash, x => !x.stash,
html<FileChangeListItem>`<a html<FileChangeListItem>`<a
class="change-list__action" class="change-list__action"
@click="${(x, c) => x.onOpenFileOnRemote(c.event)}"
@click="${(x, c) => x.onOpenFileOnRemote(c.event as MouseEvent)}"
href="#" href="#"
title="Open on remote" title="Open on remote"
aria-label="Open on remote" aria-label="Open on remote"
><code-icon icon="globe"></code-icon></a ><code-icon icon="globe"></code-icon></a
><a ><a
class="change-list__action" class="change-list__action"
@click="${(x, c) => x.onMoreActions(c.event)}"
@click="${(x, c) => x.onMoreActions(c.event as MouseEvent)}"
href="#" href="#"
title="Show more actions" title="Show more actions"
aria-label="Show more actions" aria-label="Show more actions"
@ -195,28 +197,32 @@ export class FileChangeListItem extends FASTElement {
}; };
} }
onOpenFile(e: Event) {
e.preventDefault();
this.$emit('file-open', this.getEventDetail());
onOpenFile(e: MouseEvent) {
this.$emit(
'file-open',
this.getEventDetail({ preview: false, viewColumn: e.altKey ? BesideViewColumn : undefined }),
);
} }
onOpenFileOnRemote(e: Event) {
e.preventDefault();
this.$emit('file-open-on-remote', this.getEventDetail());
onOpenFileOnRemote(e: MouseEvent) {
this.$emit(
'file-open-on-remote',
this.getEventDetail({ preview: false, viewColumn: e.altKey ? BesideViewColumn : undefined }),
);
} }
onCompareWorking(e: Event) {
e.preventDefault();
this.$emit('file-compare-working', this.getEventDetail());
onCompareWorking(e: MouseEvent) {
this.$emit(
'file-compare-working',
this.getEventDetail({ preview: false, viewColumn: e.altKey ? BesideViewColumn : undefined }),
);
} }
onComparePrevious(e?: Event, showOptions?: TextDocumentShowOptions) {
e?.preventDefault();
this.$emit('file-compare-previous', this.getEventDetail(showOptions));
onComparePrevious(e: ListItemSelectedEvent) {
this.$emit('file-compare-previous', this.getEventDetail(e.detail.showOptions));
} }
onMoreActions(e: Event) {
e.preventDefault();
onMoreActions(_e: MouseEvent) {
this.$emit('file-more-actions', this.getEventDetail()); this.$emit('file-more-actions', this.getEventDetail());
} }
} }

+ 26
- 18
src/webviews/apps/shared/components/list/list-container.ts View File

@ -1,6 +1,9 @@
import { css, customElement, FASTElement, html, observable, slotted } from '@microsoft/fast-element'; import { css, customElement, FASTElement, html, observable, slotted } from '@microsoft/fast-element';
import type { FileChangeListItem } from './file-change-list-item'; import type { FileChangeListItem } from './file-change-list-item';
import type { ListItem, ListItemSelectedDetail } from './list-item';
import type { ListItem, ListItemSelectedEvent } from './list-item';
// Can only import types from 'vscode'
const BesideViewColumn = -2; /*ViewColumn.Beside*/
const template = html<ListContainer>` const template = html<ListContainer>`
<template role="tree"> <template role="tree">
@ -69,31 +72,33 @@ export class ListContainer extends FASTElement {
this._lastSelected = target; this._lastSelected = target;
} }
handleSelected(e: CustomEvent<ListItemSelectedDetail>) {
handleSelected(e: ListItemSelectedEvent) {
if (!e.target || !e.detail.branch) return; if (!e.target || !e.detail.branch) return;
const target = e.target as ListItem;
const level = target.getAttribute('level');
function getLevel(el: ListItem) {
return parseInt(el.getAttribute('level') ?? '0', 10);
}
const getLevel = (el: ListItem) => parseInt(el.getAttribute('level') ?? '0', 10);
const getParent = (el: ListItem) => {
function getParent(el: ListItem) {
const level = getLevel(el); const level = getLevel(el);
let prev = el.previousElementSibling;
let prev = el.previousElementSibling as ListItem | null;
while (prev) { while (prev) {
const prevLevel = getLevel(prev as ListItem);
if (prevLevel < level) {
return prev as ListItem;
}
prev = prev.previousElementSibling;
const prevLevel = getLevel(prev);
if (prevLevel < level) return prev;
prev = prev.previousElementSibling as ListItem | null;
} }
return undefined; return undefined;
};
let nextElement = target.nextElementSibling as ListItem;
}
const target = e.target as ListItem;
const level = getLevel(target);
let nextElement = target.nextElementSibling as ListItem | null;
while (nextElement) { while (nextElement) {
if (nextElement.getAttribute('level') === level) {
break;
}
if (level == getLevel(nextElement)) break;
const parentElement = getParent(nextElement); const parentElement = getParent(nextElement);
nextElement.setAttribute('parentexpanded', parentElement?.expanded === false ? 'false' : 'true'); nextElement.setAttribute('parentexpanded', parentElement?.expanded === false ? 'false' : 'true');
nextElement.setAttribute('expanded', e.detail.expanded ? 'true' : 'false'); nextElement.setAttribute('expanded', e.detail.expanded ? 'true' : 'false');
@ -106,7 +111,10 @@ export class ListContainer extends FASTElement {
const target = e.target as ListItem; const target = e.target as ListItem;
if (e.key === 'Enter' || e.key === ' ') { if (e.key === 'Enter' || e.key === ' ') {
target.select(e.key === 'Enter' ? { preserveFocus: false } : undefined);
target.select({
preserveFocus: e.key !== 'Enter',
viewColumn: e.altKey ? BesideViewColumn : undefined,
});
} else if (e.key === 'ArrowUp') { } else if (e.key === 'ArrowUp') {
const $previous: HTMLElement | null = target.previousElementSibling as HTMLElement; const $previous: HTMLElement | null = target.previousElementSibling as HTMLElement;
$previous?.focus(); $previous?.focus();

+ 26
- 6
src/webviews/apps/shared/components/list/list-item.ts View File

@ -3,17 +3,23 @@ import type { TextDocumentShowOptions } from 'vscode';
import { numberConverter } from '../converters/number-converter'; import { numberConverter } from '../converters/number-converter';
import '../code-icon'; import '../code-icon';
// Can only import types from 'vscode'
const BesideViewColumn = -2; /*ViewColumn.Beside*/
declare global { declare global {
interface HTMLElementEventMap { interface HTMLElementEventMap {
selected: CustomEvent; selected: CustomEvent;
} }
} }
export interface ListItemSelectedDetail {
export type ListItemSelectedEvent = CustomEvent<ListItemSelectedEventDetail>;
export interface ListItemSelectedEventDetail {
tree: boolean; tree: boolean;
branch: boolean; branch: boolean;
expanded: boolean; expanded: boolean;
level: number; level: number;
showOptions?: TextDocumentShowOptions;
} }
const template = html<ListItem>` const template = html<ListItem>`
@ -22,7 +28,13 @@ const template = html`
aria-expanded="${x => (x.expanded === true ? 'true' : 'false')}" aria-expanded="${x => (x.expanded === true ? 'true' : 'false')}"
aria-hidden="${x => x.isHidden}" aria-hidden="${x => x.isHidden}"
> >
<button id="item" class="item" type="button" @click="${(x, c) => x.onItemClick(c.event)}">
<button
id="item"
class="item"
type="button"
@click="${(x, c) => x.onItemClick(c.event as MouseEvent)}"
@dblclick="${(x, c) => x.onDblItemClick(c.event as MouseEvent)}"
>
${repeat( ${repeat(
x => x.treeLeaves, x => x.treeLeaves,
html<ListItem>`<span class="node node--connector"><code-icon name="blank"></code-icon></span>`, html<ListItem>`<span class="node node--connector"><code-icon name="blank"></code-icon></span>`,
@ -216,11 +228,18 @@ export class ListItem extends FASTElement {
return 'false'; return 'false';
} }
onItemClick(_e: Event) {
this.select();
onItemClick(e: MouseEvent) {
this.select(e.altKey ? { viewColumn: BesideViewColumn } : undefined);
}
onDblItemClick(e: MouseEvent) {
this.select({
preview: false,
viewColumn: e.altKey || e.ctrlKey || e.metaKey ? BesideViewColumn : undefined,
});
} }
select(_showOptions?: TextDocumentShowOptions, quiet = false) {
select(showOptions?: TextDocumentShowOptions, quiet = false) {
this.$emit('select'); this.$emit('select');
// TODO: this needs to be implemented // TODO: this needs to be implemented
@ -236,7 +255,8 @@ export class ListItem extends FASTElement {
branch: this.branch, branch: this.branch,
expanded: this.expanded, expanded: this.expanded,
level: this.level, level: this.level,
});
showOptions: showOptions,
} satisfies ListItemSelectedEventDetail);
}); });
} }
} }

Loading…
Cancel
Save