Merge branch 'main' into tyriar/xterm_231109

This commit is contained in:
Daniel Imms 2023-11-09 11:55:20 -08:00 committed by GitHub
commit 169836d114
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 812 additions and 154 deletions

View file

@ -10,6 +10,7 @@ import { IDisposable } from './util';
import { toGitUri } from './uri';
import { SyncActionButton } from './actionButton';
import { RefType, Status } from './api/git';
import { emojify, ensureEmojis } from './emoji';
export class GitHistoryProvider implements SourceControlHistoryProvider, FileDecorationProvider, IDisposable {
@ -83,6 +84,8 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec
this.getSummaryHistoryItem(optionsRef, historyItemGroupIdRef)
]);
await ensureEmojis();
const historyItems = commits.length === 0 ? [] : [summary];
historyItems.push(...commits.map(commit => {
const newLineIndex = commit.message.indexOf('\n');
@ -91,9 +94,9 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec
return {
id: commit.hash,
parentIds: commit.parents,
label: subject,
label: emojify(subject),
description: commit.authorName,
icon: new ThemeIcon('account'),
icon: new ThemeIcon('git-commit'),
timestamp: commit.authorDate?.getTime()
};
}));

View file

@ -15,8 +15,8 @@ interface IDisposable {
dispose(): void;
}
interface GlobalThisAddition extends globalThis {
$hotReload_applyNewExports?(oldExports: Record<string, unknown>): AcceptNewExportsFn | undefined;
interface GlobalThisAddition {
$hotReload_applyNewExports?(args: { oldExports: Record<string, unknown>; newSrc: string }): AcceptNewExportsFn | undefined;
}
type AcceptNewExportsFn = (newExports: Record<string, unknown>) => boolean;

View file

@ -85,7 +85,7 @@ module.exports.run = async function (debugSession) {
// A frozen copy of the previous exports
const oldExports = Object.freeze({ ...oldModule.exports });
const reloadFn = g.$hotReload_applyNewExports?.(oldExports);
const reloadFn = g.$hotReload_applyNewExports?.({ oldExports, newSrc });
if (!reloadFn) {
console.log(debugSessionName, 'ignoring js change, as module does not support hot-reload', relativePath);
@ -94,7 +94,11 @@ module.exports.run = async function (debugSession) {
return;
}
const newScript = new Function('define', newSrc); // CodeQL [SM01632] This code is only executed during development. It is required for the hot-reload functionality.
// Eval maintains source maps
function newScript(/* this parameter is used by newSrc */ define) {
// eslint-disable-next-line no-eval
eval(newSrc); // CodeQL [SM01632] This code is only executed during development. It is required for the hot-reload functionality.
}
newScript(/* define */ function (deps, callback) {
// Evaluating the new code was successful.

View file

@ -14,7 +14,6 @@ export function registerHotReloadHandler(handler: HotReloadHandler): IDisposable
return { dispose() { } };
} else {
const handlers = registerGlobalHotReloadHandler();
handlers.add(handler);
return {
dispose() { handlers.delete(handler); }
@ -28,7 +27,7 @@ export function registerHotReloadHandler(handler: HotReloadHandler): IDisposable
*
* If no handler can apply the new exports, the module will not be reloaded.
*/
export type HotReloadHandler = (oldExports: Record<string, unknown>) => AcceptNewExportsHandler | undefined;
export type HotReloadHandler = (args: { oldExports: Record<string, unknown>; newSrc: string }) => AcceptNewExportsHandler | undefined;
export type AcceptNewExportsHandler = (newExports: Record<string, unknown>) => boolean;
function registerGlobalHotReloadHandler() {
@ -50,10 +49,44 @@ function registerGlobalHotReloadHandler() {
return hotReloadHandlers;
}
let hotReloadHandlers: Set<(oldExports: Record<string, unknown>) => AcceptNewExportsFn | undefined> | undefined = undefined;
let hotReloadHandlers: Set<(args: { oldExports: Record<string, unknown>; newSrc: string }) => AcceptNewExportsFn | undefined> | undefined = undefined;
interface GlobalThisAddition {
$hotReload_applyNewExports?(oldExports: Record<string, unknown>): AcceptNewExportsFn | undefined;
$hotReload_applyNewExports?(args: { oldExports: Record<string, unknown>; newSrc: string }): AcceptNewExportsFn | undefined;
}
type AcceptNewExportsFn = (newExports: Record<string, unknown>) => boolean;
if (isHotReloadEnabled()) {
// This code does not run in production.
registerHotReloadHandler(({ oldExports, newSrc }) => {
// Don't match its own source code
if (newSrc.indexOf('/* ' + 'hot-reload:patch-prototype-methods */') === -1) {
return undefined;
}
return newExports => {
for (const key in newExports) {
const exportedItem = newExports[key];
console.log(`[hot-reload] Patching prototype methods of '${key}'`, { exportedItem });
if (typeof exportedItem === 'function' && exportedItem.prototype) {
const oldExportedItem = oldExports[key];
if (oldExportedItem) {
for (const prop of Object.getOwnPropertyNames(exportedItem.prototype)) {
const descriptor = Object.getOwnPropertyDescriptor(exportedItem.prototype, prop)!;
const oldDescriptor = Object.getOwnPropertyDescriptor((oldExportedItem as any).prototype, prop);
if (descriptor?.value?.toString() !== oldDescriptor?.value?.toString()) {
console.log(`[hot-reload] Patching prototype method '${key}.${prop}'`);
}
Object.defineProperty((oldExportedItem as any).prototype, prop, descriptor);
}
newExports[key] = oldExportedItem;
}
}
}
return true;
};
});
}

View file

@ -18,6 +18,8 @@ import { BracketInfo, BracketPairInfo, BracketPairWithMinIndentationInfo, IBrack
import { IModelContentChangedEvent, IModelLanguageChangedEvent, IModelOptionsChangedEvent, IModelTokensChangedEvent } from 'vs/editor/common/textModelEvents';
import { LineTokens } from 'vs/editor/common/tokens/lineTokens';
/* hot-reload:patch-prototype-methods */
export class BracketPairsTextModelPart extends Disposable implements IBracketPairsTextModelPart {
private readonly bracketPairsTree = this._register(new MutableDisposable<IReference<BracketPairsTree>>());

View file

@ -32,6 +32,8 @@ import { LineTokens } from 'vs/editor/common/tokens/lineTokens';
import { SparseMultilineTokens } from 'vs/editor/common/tokens/sparseMultilineTokens';
import { SparseTokensStore } from 'vs/editor/common/tokens/sparseTokensStore';
/* hot-reload:patch-prototype-methods */
export class TokenizationTextModelPart extends TextModelPart implements ITokenizationTextModelPart {
private readonly _semanticTokens: SparseTokensStore = new SparseTokensStore(this._languageService.languageIdCodec);

View file

@ -886,6 +886,22 @@ export function getLinesForCommand(buffer: IBuffer, command: ITerminalCommand, c
return lines;
}
export function getPromptRowCount(command: ITerminalCommand, buffer: IBuffer): number {
if (!command.marker) {
return 1;
}
let promptRowCount = 1;
let promptStartLine = command.marker.line;
if (command.promptStartMarker) {
promptStartLine = Math.min(command.promptStartMarker?.line ?? command.marker.line, command.marker.line);
// Trim any leading whitespace-only lines to retain vertical space
while (promptStartLine < command.marker.line && (buffer.getLine(promptStartLine)?.translateToString(true) ?? '').length === 0) {
promptStartLine++;
}
promptRowCount = command.marker.line - promptStartLine + 1;
}
return promptRowCount;
}
function getXtermLineContent(buffer: IBuffer, lineStart: number, lineEnd: number, cols: number): string {
// Cap the maximum number of lines generated to prevent potential performance problems. This is

View file

@ -144,18 +144,36 @@
padding-right: 4px;
}
.scm-sync-view .monaco-list-row .history-item .monaco-icon-label .icon-container {
.scm-view .monaco-list-row .history-item .monaco-icon-label .icon-container {
display: flex;
font-size: 14px;
padding-right: 4px;
}
.scm-sync-view .monaco-list-row .history-item .monaco-icon-label .avatar {
.scm-view .monaco-list-row .history-item .monaco-icon-label .avatar {
width: 14px;
height: 14px;
border-radius: 14px;
}
.scm-view .monaco-list-row .separator-container {
display: flex;
align-items: center;
padding-left: 11px;
}
.scm-view .monaco-list-row .separator-container .label-name {
font-size: 10px;
}
.scm-view .monaco-list-row .separator-container .separator {
display: flex;
flex-grow: 1;
height: 0;
margin-left: 6px;
border-top: 1px solid var(--vscode-sideBar-border);
}
.scm-view .monaco-list-row .history > .name,
.scm-view .monaco-list-row .history-item-group > .name,
.scm-view .monaco-list-row .resource-group > .name {

View file

@ -32,8 +32,8 @@ import { Context as SuggestContext } from 'vs/editor/contrib/suggest/browser/sug
import { MANAGE_TRUST_COMMAND_ID, WorkspaceTrustContext } from 'vs/workbench/contrib/workspace/common/workspace';
import { IQuickDiffService } from 'vs/workbench/contrib/scm/common/quickDiff';
import { QuickDiffService } from 'vs/workbench/contrib/scm/common/quickDiffService';
import { SCMSyncViewPane } from 'vs/workbench/contrib/scm/browser/scmSyncViewPane';
import { getActiveElement } from 'vs/base/browser/dom';
import { SCMSyncViewPane } from 'vs/workbench/contrib/scm/browser/scmSyncViewPane';
ModesRegistry.registerLanguage({
id: 'scminput',
@ -81,7 +81,7 @@ viewsRegistry.registerViews([{
ctorDescriptor: new SyncDescriptor(SCMViewPane),
canToggleVisibility: true,
canMoveView: true,
weight: 60,
weight: 80,
order: -999,
containerIcon: sourceControlViewIcon,
openCommandActionDescriptor: {
@ -302,6 +302,25 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).regis
type: 'boolean',
description: localize('showSyncView', "Controls whether the Source Control Sync view is shown."),
default: false
},
'scm.experimental.showSyncInformation': {
type: 'object',
description: localize('showSyncInformation', "Controls whether incoming/outgoing changes are shown in the Source Control view."),
additionalProperties: false,
properties: {
'incoming': {
type: 'boolean',
description: localize('showSyncInformationIncoming', "Show incoming changes in the Source Control view."),
},
'outgoing': {
type: 'boolean',
description: localize('showSyncInformationOutgoing', "Show outgoing changes in the Source Control view."),
},
},
default: {
'incoming': false,
'outgoing': false
}
}
}
});

View file

@ -32,7 +32,6 @@ import { ActionButtonRenderer } from 'vs/workbench/contrib/scm/browser/scmViewPa
import { getActionViewItemProvider, isSCMActionButton, isSCMRepository, isSCMRepositoryArray } from 'vs/workbench/contrib/scm/browser/util';
import { ISCMActionButton, ISCMRepository, ISCMViewService, ISCMViewVisibleRepositoryChangeEvent, SYNC_VIEW_PANE_ID } from 'vs/workbench/contrib/scm/common/scm';
import { comparePaths } from 'vs/base/common/comparers';
import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryItemGroup } from 'vs/workbench/contrib/scm/common/history';
import { localize } from 'vs/nls';
import { Iterable } from 'vs/base/common/iterator';
import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
@ -50,6 +49,7 @@ import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree';
import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel';
import { IResourceNode, ResourceTree } from 'vs/base/common/resourceTree';
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
import { SCMHistoryItemChangeTreeElement, SCMHistoryItemGroupTreeElement, SCMHistoryItemTreeElement } from 'vs/workbench/contrib/scm/common/history';
type SCMHistoryItemChangeResourceTreeNode = IResourceNode<SCMHistoryItemChangeTreeElement, SCMHistoryItemTreeElement>;
type TreeElement = ISCMRepository[] | ISCMRepository | ISCMActionButton | SCMHistoryItemGroupTreeElement | SCMHistoryItemTreeElement | SCMHistoryItemChangeTreeElement | SCMHistoryItemChangeResourceTreeNode;
@ -117,24 +117,6 @@ const ContextKeys = {
ViewMode: new RawContextKey<ViewMode>('scmSyncViewMode', ViewMode.List),
};
interface SCMHistoryItemGroupTreeElement extends ISCMHistoryItemGroup {
readonly description?: string;
readonly ancestor?: string;
readonly count?: number;
readonly repository: ISCMRepository;
readonly type: 'historyItemGroup';
}
interface SCMHistoryItemTreeElement extends ISCMHistoryItem {
readonly historyItemGroup: SCMHistoryItemGroupTreeElement;
readonly type: 'historyItem';
}
interface SCMHistoryItemChangeTreeElement extends ISCMHistoryItemChange {
readonly historyItem: SCMHistoryItemTreeElement;
readonly type: 'historyItemChange';
}
class ListDelegate implements IListVirtualDelegate<any> {
getHeight(element: any): number {

View file

@ -8,8 +8,9 @@ import { Event, Emitter } from 'vs/base/common/event';
import { basename, dirname } from 'vs/base/common/resources';
import { IDisposable, Disposable, DisposableStore, combinedDisposable, dispose, toDisposable, MutableDisposable, IReference, DisposableMap } from 'vs/base/common/lifecycle';
import { ViewPane, IViewPaneOptions, ViewAction } from 'vs/workbench/browser/parts/views/viewPane';
import { append, $, Dimension, asCSSUrl, trackFocus, clearNode } from 'vs/base/browser/dom';
import { append, $, Dimension, asCSSUrl, trackFocus, clearNode, prepend } from 'vs/base/browser/dom';
import { IListVirtualDelegate, IIdentityProvider } from 'vs/base/browser/ui/list/list';
import { SCMHistoryItemChangeTreeElement, SCMHistoryItemGroupTreeElement, SCMHistoryItemTreeElement, SCMViewSeparatorElement } from 'vs/workbench/contrib/scm/common/history';
import { ISCMResourceGroup, ISCMResource, InputValidationType, ISCMRepository, ISCMInput, IInputValidation, ISCMViewService, ISCMViewVisibleRepositoryChangeEvent, ISCMService, SCMInputChangeReason, VIEW_PANE_ID, ISCMActionButton, ISCMActionButtonDescriptor, ISCMRepositorySortKey, REPOSITORIES_VIEW_PANE_ID } from 'vs/workbench/contrib/scm/common/scm';
import { ResourceLabels, IResourceLabel, IFileLabelOptions } from 'vs/workbench/browser/labels';
import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge';
@ -23,7 +24,7 @@ import { MenuItemAction, IMenuService, registerAction2, MenuId, IAction2Options,
import { IAction, ActionRunner, Action, Separator } from 'vs/base/common/actions';
import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar';
import { IThemeService, IFileIconTheme } from 'vs/platform/theme/common/themeService';
import { isSCMResource, isSCMResourceGroup, connectPrimaryMenuToInlineActionBar, isSCMRepository, isSCMInput, collectContextMenuActions, getActionViewItemProvider, isSCMActionButton, isSCMViewService } from './util';
import { isSCMResource, isSCMResourceGroup, connectPrimaryMenuToInlineActionBar, isSCMRepository, isSCMInput, collectContextMenuActions, getActionViewItemProvider, isSCMActionButton, isSCMViewService, isSCMHistoryItemGroupTreeElement, isSCMHistoryItemTreeElement, isSCMHistoryItemChangeTreeElement, toDiffEditorArguments, isSCMResourceNode, isSCMHistoryItemChangeNode, isSCMViewSeparator } from './util';
import { WorkbenchCompressibleAsyncDataTree, IOpenEvent } from 'vs/platform/list/browser/listService';
import { IConfigurationService, ConfigurationTarget, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration';
import { disposableTimeout, ThrottledDelayer, Sequencer } from 'vs/base/common/async';
@ -97,8 +98,23 @@ import { EditorOption } from 'vs/editor/common/config/editorOptions';
import { IAsyncDataTreeViewState, ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree';
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
import { EditOperation } from 'vs/editor/common/core/editOperation';
import { stripIcons } from 'vs/base/common/iconLabels';
import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel';
type TreeElement = ISCMRepository | ISCMInput | ISCMActionButton | ISCMResourceGroup | IResourceNode<ISCMResource, ISCMResourceGroup> | ISCMResource;
// type SCMResourceTreeNode = IResourceNode<ISCMResource, ISCMResourceGroup>;
// type SCMHistoryItemChangeResourceTreeNode = IResourceNode<SCMHistoryItemChangeTreeElement, SCMHistoryItemTreeElement>;
type TreeElement =
ISCMRepository |
ISCMInput |
ISCMActionButton |
ISCMResourceGroup |
ISCMResource |
IResourceNode<ISCMResource, ISCMResourceGroup> |
SCMHistoryItemGroupTreeElement |
SCMHistoryItemTreeElement |
SCMHistoryItemChangeTreeElement |
IResourceNode<SCMHistoryItemChangeTreeElement, SCMHistoryItemTreeElement> |
SCMViewSeparatorElement;
interface ISCMLayout {
height: number | undefined;
@ -678,6 +694,195 @@ class ResourceRenderer implements ICompressibleTreeRenderer<ISCMResource | IReso
}
}
interface HistoryItemGroupTemplate {
readonly label: IconLabel;
readonly count: CountBadge;
readonly disposables: IDisposable;
}
class HistoryItemGroupRenderer implements ICompressibleTreeRenderer<SCMHistoryItemGroupTreeElement, void, HistoryItemGroupTemplate> {
static readonly TEMPLATE_ID = 'history-item-group';
get templateId(): string { return HistoryItemGroupRenderer.TEMPLATE_ID; }
renderTemplate(container: HTMLElement) {
// hack
(container.parentElement!.parentElement!.querySelector('.monaco-tl-twistie')! as HTMLElement).classList.add('force-twistie');
const element = append(container, $('.history-item-group'));
const label = new IconLabel(element, { supportIcons: true });
const countContainer = append(element, $('.count'));
const count = new CountBadge(countContainer, {}, defaultCountBadgeStyles);
return { label, count, disposables: new DisposableStore() };
}
renderElement(node: ITreeNode<SCMHistoryItemGroupTreeElement>, index: number, templateData: HistoryItemGroupTemplate, height: number | undefined): void {
const historyItemGroup = node.element;
templateData.label.setLabel(historyItemGroup.label, historyItemGroup.description);
templateData.count.setCount(historyItemGroup.count ?? 0);
}
renderCompressedElements(node: ITreeNode<ICompressedTreeNode<SCMHistoryItemGroupTreeElement>, void>, index: number, templateData: HistoryItemGroupTemplate, height: number | undefined): void {
throw new Error('Should never happen since node is incompressible');
}
disposeTemplate(templateData: HistoryItemGroupTemplate): void {
templateData.disposables.dispose();
}
}
interface HistoryItemTemplate {
readonly iconContainer: HTMLElement;
// readonly avatarImg: HTMLImageElement;
readonly iconLabel: IconLabel;
// readonly timestampContainer: HTMLElement;
// readonly timestamp: HTMLSpanElement;
readonly disposables: IDisposable;
}
class HistoryItemRenderer implements ICompressibleTreeRenderer<SCMHistoryItemTreeElement, void, HistoryItemTemplate> {
static readonly TEMPLATE_ID = 'history-item';
get templateId(): string { return HistoryItemRenderer.TEMPLATE_ID; }
renderTemplate(container: HTMLElement): HistoryItemTemplate {
// hack
(container.parentElement!.parentElement!.querySelector('.monaco-tl-twistie')! as HTMLElement).classList.add('force-twistie');
const element = append(container, $('.history-item'));
const iconLabel = new IconLabel(element, { supportIcons: true });
const iconContainer = prepend(iconLabel.element, $('.icon-container'));
// const avatarImg = append(iconContainer, $('img.avatar')) as HTMLImageElement;
// const timestampContainer = append(iconLabel.element, $('.timestamp-container'));
// const timestamp = append(timestampContainer, $('span.timestamp'));
return { iconContainer, iconLabel, disposables: new DisposableStore() };
}
renderElement(node: ITreeNode<SCMHistoryItemTreeElement, void>, index: number, templateData: HistoryItemTemplate, height: number | undefined): void {
const historyItem = node.element;
templateData.iconContainer.className = 'icon-container';
if (historyItem.icon && ThemeIcon.isThemeIcon(historyItem.icon)) {
templateData.iconContainer.classList.add(...ThemeIcon.asClassNameArray(historyItem.icon));
}
// if (commit.authorAvatar) {
// templateData.avatarImg.src = commit.authorAvatar;
// templateData.avatarImg.style.display = 'block';
// templateData.iconContainer.classList.remove(...ThemeIcon.asClassNameArray(Codicon.account));
// } else {
// templateData.avatarImg.style.display = 'none';
// templateData.iconContainer.classList.add(...ThemeIcon.asClassNameArray(Codicon.account));
// }
templateData.iconLabel.setLabel(historyItem.label, historyItem.description);
// templateData.timestampContainer.classList.toggle('timestamp-duplicate', commit.hideTimestamp === true);
// templateData.timestamp.textContent = fromNow(commit.timestamp);
}
renderCompressedElements(node: ITreeNode<ICompressedTreeNode<SCMHistoryItemTreeElement>, void>, index: number, templateData: HistoryItemTemplate, height: number | undefined): void {
throw new Error('Should never happen since node is incompressible');
}
disposeTemplate(templateData: HistoryItemTemplate): void {
templateData.disposables.dispose();
}
}
interface HistoryItemChangeTemplate {
readonly element: HTMLElement;
readonly name: HTMLElement;
readonly fileLabel: IResourceLabel;
readonly decorationIcon: HTMLElement;
readonly disposables: IDisposable;
}
class HistoryItemChangeRenderer implements ICompressibleTreeRenderer<SCMHistoryItemChangeTreeElement | IResourceNode<SCMHistoryItemChangeTreeElement, SCMHistoryItemTreeElement>, void, HistoryItemChangeTemplate> {
static readonly TEMPLATE_ID = 'historyItemChange';
get templateId(): string { return HistoryItemChangeRenderer.TEMPLATE_ID; }
constructor(
private readonly viewMode: () => ViewMode,
private readonly labels: ResourceLabels,
@ILabelService private labelService: ILabelService) { }
renderTemplate(container: HTMLElement): HistoryItemChangeTemplate {
const element = append(container, $('.change'));
const name = append(element, $('.name'));
const fileLabel = this.labels.create(name, { supportDescriptionHighlights: true, supportHighlights: true });
const decorationIcon = append(element, $('.decoration-icon'));
return { element, name, fileLabel, decorationIcon, disposables: new DisposableStore() };
}
renderElement(node: ITreeNode<SCMHistoryItemChangeTreeElement | IResourceNode<SCMHistoryItemChangeTreeElement, SCMHistoryItemTreeElement>, void>, index: number, templateData: HistoryItemChangeTemplate, height: number | undefined): void {
const historyItemChangeOrFolder = node.element;
const uri = ResourceTree.isResourceNode(historyItemChangeOrFolder) ? historyItemChangeOrFolder.element?.uri ?? historyItemChangeOrFolder.uri : historyItemChangeOrFolder.uri;
const fileKind = ResourceTree.isResourceNode(historyItemChangeOrFolder) ? FileKind.FOLDER : FileKind.FILE;
const hidePath = this.viewMode() === ViewMode.Tree;
templateData.fileLabel.setFile(uri, { fileDecorations: { colors: false, badges: true }, fileKind, hidePath, });
}
renderCompressedElements(node: ITreeNode<ICompressedTreeNode<SCMHistoryItemChangeTreeElement | IResourceNode<SCMHistoryItemChangeTreeElement, SCMHistoryItemTreeElement>>, void>, index: number, templateData: HistoryItemChangeTemplate, height: number | undefined): void {
const compressed = node.element as ICompressedTreeNode<IResourceNode<SCMHistoryItemChangeTreeElement, SCMHistoryItemTreeElement>>;
const folder = compressed.elements[compressed.elements.length - 1];
const label = compressed.elements.map(e => e.name);
templateData.fileLabel.setResource({ resource: folder.uri, name: label }, {
fileDecorations: { colors: false, badges: true },
fileKind: FileKind.FOLDER,
separator: this.labelService.getSeparator(folder.uri.scheme)
});
}
disposeTemplate(templateData: HistoryItemChangeTemplate): void {
templateData.disposables.dispose();
}
}
interface SeparatorTemplate {
readonly label: IconLabel;
readonly disposables: IDisposable;
}
class SeparatorRenderer implements ICompressibleTreeRenderer<SCMViewSeparatorElement, void, SeparatorTemplate> {
static readonly TEMPLATE_ID = 'separator';
get templateId(): string { return SeparatorRenderer.TEMPLATE_ID; }
renderTemplate(container: HTMLElement): SeparatorTemplate {
// hack
(container.parentElement!.parentElement!.querySelector('.monaco-tl-twistie')! as HTMLElement).classList.add('force-no-twistie');
// Use default cursor & disable hover for list item
container.parentElement!.parentElement!.classList.add('cursor-default', 'force-no-hover');
const element = append(container, $('.separator-container'));
const label = new IconLabel(element, { supportIcons: true, });
append(element, $('.separator'));
return { label, disposables: new DisposableStore() };
}
renderElement(element: ITreeNode<SCMViewSeparatorElement, void>, index: number, templateData: SeparatorTemplate, height: number | undefined): void {
templateData.label.setLabel(element.element.label);
}
renderCompressedElements(node: ITreeNode<ICompressedTreeNode<SCMViewSeparatorElement>, void>, index: number, templateData: SeparatorTemplate, height: number | undefined): void {
throw new Error('Should never happen since node is incompressible');
}
disposeTemplate(templateData: SeparatorTemplate): void {
throw new Error('Method not implemented.');
}
}
class ListDelegate implements IListVirtualDelegate<TreeElement> {
constructor(private readonly inputRenderer: InputRenderer) { }
@ -699,10 +904,20 @@ class ListDelegate implements IListVirtualDelegate<TreeElement> {
return InputRenderer.TEMPLATE_ID;
} else if (isSCMActionButton(element)) {
return ActionButtonRenderer.TEMPLATE_ID;
} else if (ResourceTree.isResourceNode(element) || isSCMResource(element)) {
return ResourceRenderer.TEMPLATE_ID;
} else {
} else if (isSCMResourceGroup(element)) {
return ResourceGroupRenderer.TEMPLATE_ID;
} else if (isSCMResource(element) || isSCMResourceNode(element)) {
return ResourceRenderer.TEMPLATE_ID;
} else if (isSCMHistoryItemGroupTreeElement(element)) {
return HistoryItemGroupRenderer.TEMPLATE_ID;
} else if (isSCMHistoryItemTreeElement(element)) {
return HistoryItemRenderer.TEMPLATE_ID;
} else if (isSCMHistoryItemChangeTreeElement(element) || isSCMHistoryItemChangeNode(element)) {
return HistoryItemChangeRenderer.TEMPLATE_ID;
} else if (isSCMViewSeparator(element)) {
return SeparatorRenderer.TEMPLATE_ID;
} else {
throw new Error('Unknown element');
}
}
}
@ -726,6 +941,10 @@ class SCMTreeFilter implements ITreeFilter<TreeElement> {
return true;
} else if (isSCMResourceGroup(element)) {
return element.resources.length > 0 || !element.hideWhenEmpty;
} else if (isSCMHistoryItemGroupTreeElement(element)) {
return (element.count ?? 0) > 0;
} else if (isSCMViewSeparator(element)) {
return element.repository.provider.groups.some(g => g.resources.length > 0);
} else {
return true;
}
@ -759,6 +978,14 @@ export class SCMTreeSorter implements ITreeSorter<TreeElement> {
return 1;
}
if (isSCMViewSeparator(one)) {
if (!isSCMHistoryItemGroupTreeElement(other) && !isSCMResourceGroup(other)) {
throw new Error('Invalid comparison');
}
return 0;
}
if (isSCMResourceGroup(one)) {
if (!isSCMResourceGroup(other)) {
throw new Error('Invalid comparison');
@ -767,7 +994,44 @@ export class SCMTreeSorter implements ITreeSorter<TreeElement> {
return 0;
}
// List
if (isSCMHistoryItemGroupTreeElement(one)) {
if (!isSCMHistoryItemGroupTreeElement(other) && !isSCMResourceGroup(other) && !isSCMViewSeparator(other)) {
throw new Error('Invalid comparison');
}
return 0;
}
if (isSCMHistoryItemTreeElement(one)) {
if (!isSCMHistoryItemTreeElement(other)) {
throw new Error('Invalid comparison');
}
return 0;
}
if (isSCMHistoryItemChangeTreeElement(one) || isSCMHistoryItemChangeNode(one)) {
// List
if (this.viewMode() === ViewMode.List) {
if (!isSCMHistoryItemChangeTreeElement(other)) {
throw new Error('Invalid comparison');
}
return comparePaths(one.uri.fsPath, other.uri.fsPath);
}
// Tree
if (!isSCMHistoryItemChangeTreeElement(other) && !isSCMHistoryItemChangeNode(other)) {
throw new Error('Invalid comparison');
}
const oneName = isSCMHistoryItemChangeNode(one) ? one.name : basename(one.uri);
const otherName = isSCMHistoryItemChangeNode(other) ? other.name : basename(other.uri);
return compareFileNames(oneName, otherName);
}
// Resource (List)
if (this.viewMode() === ViewMode.List) {
// FileName
if (this.viewSortKey() === ViewSortKey.Name) {
@ -794,7 +1058,7 @@ export class SCMTreeSorter implements ITreeSorter<TreeElement> {
return comparePaths(onePath, otherPath);
}
// Tree
// Resource (Tree)
const oneIsDirectory = ResourceTree.isResourceNode(one);
const otherIsDirectory = ResourceTree.isResourceNode(other);
@ -823,19 +1087,23 @@ export class SCMTreeKeyboardNavigationLabelProvider implements ICompressibleKeyb
return undefined;
} else if (isSCMResourceGroup(element)) {
return element.label;
} else if (isSCMHistoryItemGroupTreeElement(element)) {
return element.label;
} else if (isSCMHistoryItemTreeElement(element)) {
return element.label;
} else if (isSCMViewSeparator(element)) {
return element.label;
} else {
if (this.viewMode() === ViewMode.List) {
// In List mode match using the file name and the path.
// Since we want to match both on the file name and the
// full path we return an array of labels. A match in the
// file name takes precedence over a match in the path.
const fileName = basename(element.sourceUri);
const filePath = this.labelService.getUriLabel(element.sourceUri, { relative: true });
return [fileName, filePath];
const uri = isSCMResource(element) ? element.sourceUri : element.uri;
return [basename(uri), this.labelService.getUriLabel(uri, { relative: true })];
} else {
// In Tree mode only match using the file name
return basename(element.sourceUri);
return basename(isSCMResource(element) ? element.sourceUri : element.uri);
}
}
}
@ -847,10 +1115,7 @@ export class SCMTreeKeyboardNavigationLabelProvider implements ICompressibleKeyb
}
function getSCMResourceId(element: TreeElement): string {
if (ResourceTree.isResourceNode(element)) {
const group = element.context;
return `folder:${group.provider.id}/${group.id}/$FOLDER/${element.uri.toString()}`;
} else if (isSCMRepository(element)) {
if (isSCMRepository(element)) {
const provider = element.provider;
return `repo:${provider.id}`;
} else if (isSCMInput(element)) {
@ -859,13 +1124,38 @@ function getSCMResourceId(element: TreeElement): string {
} else if (isSCMActionButton(element)) {
const provider = element.repository.provider;
return `actionButton:${provider.id}`;
} else if (isSCMResourceGroup(element)) {
const provider = element.provider;
return `resourceGroup:${provider.id}/${element.id}`;
} else if (isSCMResource(element)) {
const group = element.resourceGroup;
const provider = group.provider;
return `resource:${provider.id}/${group.id}/${element.sourceUri.toString()}`;
} else if (isSCMResourceNode(element)) {
const group = element.context;
return `folder:${group.provider.id}/${group.id}/$FOLDER/${element.uri.toString()}`;
} else if (isSCMHistoryItemGroupTreeElement(element)) {
const provider = element.repository.provider;
return `historyItemGroup:${provider.id}/${element.id}`;
} else if (isSCMHistoryItemTreeElement(element)) {
const historyItemGroup = element.historyItemGroup;
const provider = historyItemGroup.repository.provider;
return `historyItem:${provider.id}/${historyItemGroup.id}/${element.id}`;
} else if (isSCMHistoryItemChangeTreeElement(element)) {
const historyItem = element.historyItem;
const historyItemGroup = historyItem.historyItemGroup;
const provider = historyItemGroup.repository.provider;
return `historyItemChange:${provider.id}/${historyItemGroup.id}/${historyItem.id}/${element.uri.toString()}`;
} else if (isSCMHistoryItemChangeNode(element)) {
const historyItem = element.context;
const historyItemGroup = historyItem.historyItemGroup;
const provider = historyItemGroup.repository.provider;
return `folder:${provider.id}/${historyItemGroup.id}/${historyItem.id}/$FOLDER/${element.uri.toString()}`;
} else if (isSCMViewSeparator(element)) {
const provider = element.repository.provider;
return `separator:${provider.id}`;
} else {
const provider = element.provider;
return `group:${provider.id}/${element.id}`;
throw new Error('Invalid tree element');
}
}
@ -908,6 +1198,21 @@ export class SCMAccessibilityProvider implements IListAccessibilityProvider<Tree
return element.button?.command.title ?? '';
} else if (isSCMResourceGroup(element)) {
return element.label;
} else if (isSCMHistoryItemGroupTreeElement(element)) {
return `${stripIcons(element.label).trim()}${element.description ? `, ${element.description}` : ''}`;
} else if (isSCMHistoryItemTreeElement(element)) {
return `${stripIcons(element.label).trim()}${element.description ? `, ${element.description}` : ''}`;
} else if (isSCMHistoryItemChangeTreeElement(element)) {
const result = [basename(element.uri)];
const path = this.labelService.getUriLabel(dirname(element.uri), { relative: true, noPrefix: true });
if (path) {
result.push(path);
}
return result.join(', ');
} else if (isSCMViewSeparator(element)) {
return element.label;
} else {
const result: string[] = [];
@ -1917,6 +2222,9 @@ export class SCMViewPane extends ViewPane {
private _alwaysShowRepositories = false;
get alwaysShowRepositories(): boolean { return this._alwaysShowRepositories; }
private _showSyncInformation: { incoming: boolean; outgoing: boolean } = { incoming: false, outgoing: false };
get showSyncInformation(): { incoming: boolean; outgoing: boolean } { return this._showSyncInformation; }
private readonly items = new DisposableMap<ISCMRepository, IDisposable>();
private readonly visibilityDisposables = new DisposableStore();
private readonly asyncOperationSequencer = new Sequencer();
@ -2044,6 +2352,14 @@ export class SCMViewPane extends ViewPane {
Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.alwaysShowRepositories'), this.visibilityDisposables)(updateRepositoryVisibility, this, this.visibilityDisposables);
updateRepositoryVisibility();
const updateSyncInformationVisibility = () => {
const setting = this.configurationService.getValue<{ incoming: boolean; outgoing: boolean }>('scm.experimental.showSyncInformation');
this._showSyncInformation = { incoming: setting.incoming === true, outgoing: setting.outgoing === true };
this.updateChildren();
};
Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.experimental.showSyncInformation'), this.visibilityDisposables)(updateSyncInformationVisibility, this, this.visibilityDisposables);
updateSyncInformationVisibility();
// Add visible repositories
this.scmViewService.onDidChangeVisibleRepositories(this.onDidChangeVisibleRepositories, this, this.visibilityDisposables);
this.onDidChangeVisibleRepositories({ added: this.scmViewService.visibleRepositories, removed: Iterable.empty() });
@ -2096,9 +2412,13 @@ export class SCMViewPane extends ViewPane {
this.actionButtonRenderer,
this.instantiationService.createInstance(RepositoryRenderer, getActionViewItemProvider(this.instantiationService)),
this.instantiationService.createInstance(ResourceGroupRenderer, getActionViewItemProvider(this.instantiationService)),
this.instantiationService.createInstance(ResourceRenderer, () => this.viewMode, this.listLabels, getActionViewItemProvider(this.instantiationService), actionRunner)
this.instantiationService.createInstance(ResourceRenderer, () => this.viewMode, this.listLabels, getActionViewItemProvider(this.instantiationService), actionRunner),
this.instantiationService.createInstance(HistoryItemGroupRenderer),
this.instantiationService.createInstance(HistoryItemRenderer),
this.instantiationService.createInstance(HistoryItemChangeRenderer, () => this.viewMode, this.listLabels),
this.instantiationService.createInstance(SeparatorRenderer)
],
this.instantiationService.createInstance(SCMTreeDataSource, () => this.viewMode, () => this.alwaysShowRepositories, () => this.showActionButton),
this.instantiationService.createInstance(SCMTreeDataSource, () => this.viewMode, () => this.alwaysShowRepositories, () => this.showActionButton, () => this.showSyncInformation),
{
horizontalScrolling: false,
setRowLineHeight: false,
@ -2111,7 +2431,7 @@ export class SCMViewPane extends ViewPane {
overrideStyles: {
listBackground: this.viewDescriptorService.getViewLocationById(this.id) === ViewContainerLocation.Panel ? PANEL_BACKGROUND : SIDE_BAR_BACKGROUND
},
collapseByDefault: (e) => false,
collapseByDefault: (e: unknown) => isSCMHistoryItemGroupTreeElement(e) || isSCMHistoryItemTreeElement(e) || isSCMHistoryItemChangeTreeElement(e),
accessibilityProvider: this.instantiationService.createInstance(SCMAccessibilityProvider)
}) as WorkbenchCompressibleAsyncDataTree<ISCMViewService, TreeElement, FuzzyScore>;
@ -2131,20 +2451,6 @@ export class SCMViewPane extends ViewPane {
} else if (isSCMRepository(e.element)) {
this.scmViewService.focus(e.element);
return;
} else if (isSCMResourceGroup(e.element)) {
const provider = e.element.provider;
const repository = Iterable.find(this.scmService.repositories, r => r.provider === provider);
if (repository) {
this.scmViewService.focus(repository);
}
return;
} else if (ResourceTree.isResourceNode(e.element)) {
const provider = e.element.context.provider;
const repository = Iterable.find(this.scmService.repositories, r => r.provider === provider);
if (repository) {
this.scmViewService.focus(repository);
}
return;
} else if (isSCMInput(e.element)) {
this.scmViewService.focus(e.element.repository);
@ -2170,26 +2476,55 @@ export class SCMViewPane extends ViewPane {
this.tree.setFocus([], e.browserEvent);
return;
}
// ISCMResource
if (e.element.command?.id === API_OPEN_EDITOR_COMMAND_ID || e.element.command?.id === API_OPEN_DIFF_EDITOR_COMMAND_ID) {
await this.commandService.executeCommand(e.element.command.id, ...(e.element.command.arguments || []), e);
} else {
await e.element.open(!!e.editorOptions.preserveFocus);
if (e.editorOptions.pinned) {
const activeEditorPane = this.editorService.activeEditorPane;
activeEditorPane?.group.pinEditor(activeEditorPane.input);
} else if (isSCMResourceGroup(e.element)) {
const provider = e.element.provider;
const repository = Iterable.find(this.scmService.repositories, r => r.provider === provider);
if (repository) {
this.scmViewService.focus(repository);
}
}
return;
} else if (isSCMResource(e.element)) {
if (e.element.command?.id === API_OPEN_EDITOR_COMMAND_ID || e.element.command?.id === API_OPEN_DIFF_EDITOR_COMMAND_ID) {
await this.commandService.executeCommand(e.element.command.id, ...(e.element.command.arguments || []), e);
} else {
await e.element.open(!!e.editorOptions.preserveFocus);
const provider = e.element.resourceGroup.provider;
const repository = Iterable.find(this.scmService.repositories, r => r.provider === provider);
if (e.editorOptions.pinned) {
const activeEditorPane = this.editorService.activeEditorPane;
if (repository) {
this.scmViewService.focus(repository);
activeEditorPane?.group.pinEditor(activeEditorPane.input);
}
}
const provider = e.element.resourceGroup.provider;
const repository = Iterable.find(this.scmService.repositories, r => r.provider === provider);
if (repository) {
this.scmViewService.focus(repository);
}
} else if (isSCMResourceNode(e.element)) {
const provider = e.element.context.provider;
const repository = Iterable.find(this.scmService.repositories, r => r.provider === provider);
if (repository) {
this.scmViewService.focus(repository);
}
return;
} else if (isSCMHistoryItemGroupTreeElement(e.element)) {
this.scmViewService.focus(e.element.repository);
return;
} else if (isSCMHistoryItemTreeElement(e.element)) {
this.scmViewService.focus(e.element.historyItemGroup.repository);
return;
} else if (isSCMHistoryItemChangeTreeElement(e.element)) {
if (e.element.originalUri && e.element.modifiedUri) {
await this.commandService.executeCommand(API_OPEN_DIFF_EDITOR_COMMAND_ID, ...toDiffEditorArguments(e.element.uri, e.element.originalUri, e.element.modifiedUri), e);
}
this.scmViewService.focus(e.element.historyItem.historyItemGroup.repository);
return;
} else if (isSCMHistoryItemChangeNode(e.element)) {
this.scmViewService.focus(e.element.context.historyItemGroup.repository);
return;
}
}
@ -2311,7 +2646,11 @@ export class SCMViewPane extends ViewPane {
const menus = this.scmViewService.menus.getRepositoryMenus(element.provider);
const menu = menus.getResourceGroupMenu(element);
actions = collectContextMenuActions(menu);
} else if (ResourceTree.isResourceNode(element)) {
} else if (isSCMResource(element)) {
const menus = this.scmViewService.menus.getRepositoryMenus(element.resourceGroup.provider);
const menu = menus.getResourceMenu(element);
actions = collectContextMenuActions(menu);
} else if (isSCMResourceNode(element)) {
if (element.element) {
const menus = this.scmViewService.menus.getRepositoryMenus(element.element.resourceGroup.provider);
const menu = menus.getResourceMenu(element.element);
@ -2321,10 +2660,6 @@ export class SCMViewPane extends ViewPane {
const menu = menus.getResourceFolderMenu(element.context);
actions = collectContextMenuActions(menu);
}
} else {
const menus = this.scmViewService.menus.getRepositoryMenus(element.resourceGroup.provider);
const menu = menus.getResourceMenu(element);
actions = collectContextMenuActions(menu);
}
const actionRunner = new RepositoryPaneActionRunner(() => this.getSelectedResources());
@ -2489,7 +2824,10 @@ class SCMTreeDataSource implements IAsyncDataSource<ISCMViewService, TreeElement
private readonly viewMode: () => ViewMode,
private readonly alwaysShowRepositories: () => boolean,
private readonly showActionButton: () => boolean,
@ISCMViewService private readonly scmViewService: ISCMViewService) { }
private readonly showSyncInformation: () => { incoming: boolean; outgoing: boolean },
@ISCMViewService private readonly scmViewService: ISCMViewService,
@IUriIdentityService private uriIdentityService: IUriIdentityService,
) { }
hasChildren(inputOrElement: ISCMViewService | TreeElement): boolean {
if (isSCMViewService(inputOrElement)) {
@ -2506,12 +2844,20 @@ class SCMTreeDataSource implements IAsyncDataSource<ISCMViewService, TreeElement
return false;
} else if (ResourceTree.isResourceNode(inputOrElement)) {
return inputOrElement.childrenCount > 0;
} else if (isSCMHistoryItemGroupTreeElement(inputOrElement)) {
return (inputOrElement.count ?? 0) > 0;
} else if (isSCMHistoryItemTreeElement(inputOrElement)) {
return true;
} else if (isSCMHistoryItemChangeTreeElement(inputOrElement)) {
return false;
} else if (isSCMViewSeparator(inputOrElement)) {
return false;
} else {
throw new Error('hasChildren not implemented.');
}
}
getChildren(inputOrElement: ISCMViewService | TreeElement): Iterable<TreeElement> | Promise<Iterable<TreeElement>> {
async getChildren(inputOrElement: ISCMViewService | TreeElement): Promise<Iterable<TreeElement>> {
const repositoryCount = this.scmViewService.visibleRepositories.length;
const alwaysShowRepositories = this.alwaysShowRepositories();
@ -2545,6 +2891,26 @@ class SCMTreeDataSource implements IAsyncDataSource<ISCMViewService, TreeElement
children.push(...resourceGroups);
}
// History item groups
if (this.showSyncInformation().incoming || this.showSyncInformation().outgoing) {
const historyProvider = inputOrElement.provider.historyProvider;
const historyItemGroup = historyProvider?.currentHistoryItemGroup;
if (historyProvider && historyItemGroup) {
const historyItemGroups = await this.getHistoryItemGroups(inputOrElement);
if (historyItemGroups.some(h => h.count ?? 0 > 0)) {
// Separator
children.push({
label: localize('syncSeparatorHeader', "Incoming/Outgoing"),
repository: inputOrElement,
type: 'separator'
} as SCMViewSeparatorElement);
}
children.push(...historyItemGroups);
}
}
return children;
} else if (isSCMResourceGroup(inputOrElement)) {
if (this.viewMode() === ViewMode.List) {
@ -2554,26 +2920,129 @@ class SCMTreeDataSource implements IAsyncDataSource<ISCMViewService, TreeElement
// Resources (Tree)
const children: TreeElement[] = [];
for (const node of inputOrElement.resourceTree.root.children) {
children.push(node.childrenCount === 0 ? node.element ?? node : node);
children.push(node.element ?? node);
}
return children;
}
} else if (ResourceTree.isResourceNode(inputOrElement)) {
// Resources (Tree)
} else if (isSCMResourceNode(inputOrElement) || isSCMHistoryItemChangeNode(inputOrElement)) {
// Resources (Tree), History item changes (Tree)
const children: TreeElement[] = [];
for (const node of inputOrElement.children) {
children.push(node.childrenCount === 0 ? node.element ?? node : node);
children.push(node.element ?? node);
}
return children;
} else if (isSCMHistoryItemGroupTreeElement(inputOrElement)) {
// History item group
return this.getHistoryItems(inputOrElement);
} else if (isSCMHistoryItemTreeElement(inputOrElement)) {
// History item changes (List/Tree)
return this.getHistoryItemChanges(inputOrElement);
}
return [];
}
private async getHistoryItemGroups(element: ISCMRepository): Promise<SCMHistoryItemGroupTreeElement[]> {
const scmProvider = element.provider;
const historyProvider = scmProvider.historyProvider;
const currentHistoryItemGroup = historyProvider?.currentHistoryItemGroup;
if (!historyProvider || !currentHistoryItemGroup || (this.showSyncInformation().incoming === false && this.showSyncInformation().outgoing === false)) {
return [];
}
// History item group base
const historyItemGroupBase = await historyProvider.resolveHistoryItemGroupBase(currentHistoryItemGroup.id);
if (!historyItemGroupBase) {
return [];
}
// Common ancestor, ahead, behind
const ancestor = await historyProvider.resolveHistoryItemGroupCommonAncestor(currentHistoryItemGroup.id, historyItemGroupBase.id);
const children: SCMHistoryItemGroupTreeElement[] = [];
// Incoming
if (historyItemGroupBase && this.showSyncInformation().incoming) {
children.push({
id: historyItemGroupBase.id,
label: `$(cloud-download) ${historyItemGroupBase.label}`,
// description: localize('incoming', "Incoming Changes"),
ancestor: ancestor?.id,
count: ancestor?.behind ?? 0,
repository: element,
type: 'historyItemGroup'
});
}
// Outgoing
if (this.showSyncInformation().outgoing) {
children.push({
id: currentHistoryItemGroup.id,
label: `$(cloud-upload) ${currentHistoryItemGroup.label}`,
// description: localize('outgoing', "Outgoing Changes"),
ancestor: ancestor?.id,
count: ancestor?.ahead ?? 0,
repository: element,
type: 'historyItemGroup'
});
}
return children;
}
private async getHistoryItems(element: SCMHistoryItemGroupTreeElement): Promise<SCMHistoryItemTreeElement[]> {
const scmProvider = element.repository.provider;
const historyProvider = scmProvider.historyProvider;
if (!historyProvider) {
return [];
}
const historyItems = await historyProvider.provideHistoryItems(element.id, { limit: { id: element.ancestor } }) ?? [];
return historyItems.map(historyItem => ({
...historyItem,
historyItemGroup: element,
type: 'historyItem'
}));
}
private async getHistoryItemChanges(element: SCMHistoryItemTreeElement): Promise<(SCMHistoryItemChangeTreeElement | IResourceNode<SCMHistoryItemChangeTreeElement, SCMHistoryItemTreeElement>)[]> {
const repository = element.historyItemGroup.repository;
const historyProvider = repository.provider.historyProvider;
if (!historyProvider) {
return [];
}
// History Item Changes
const changes = await historyProvider.provideHistoryItemChanges(element.id) ?? [];
if (this.viewMode() === ViewMode.List) {
// List
return changes.map(change => ({
...change,
historyItem: element,
type: 'historyItemChange'
}));
}
// Tree
const tree = new ResourceTree<SCMHistoryItemChangeTreeElement, SCMHistoryItemTreeElement>(element, repository.provider.rootUri ?? URI.file('/'), this.uriIdentityService.extUri);
for (const change of changes) {
tree.add(change.uri, {
...change,
historyItem: element,
type: 'historyItemChange'
});
}
return [...tree.root.children];
}
getParent(element: TreeElement): ISCMViewService | TreeElement {
if (ResourceTree.isResourceNode(element)) {
if (isSCMResourceNode(element)) {
if (element.parent === element.context.resourceTree.root) {
return element.context;
} else if (!element.parent) {

View file

@ -3,6 +3,8 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'vs/base/common/path';
import { SCMHistoryItemChangeTreeElement, SCMHistoryItemGroupTreeElement, SCMHistoryItemTreeElement, SCMViewSeparatorElement } from 'vs/workbench/contrib/scm/common/history';
import { ISCMResource, ISCMRepository, ISCMResourceGroup, ISCMInput, ISCMActionButton, ISCMViewService } from 'vs/workbench/contrib/scm/common/scm';
import { IMenu } from 'vs/platform/actions/common/actions';
import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar';
@ -16,6 +18,8 @@ import { ICommandService } from 'vs/platform/commands/common/commands';
import { Command } from 'vs/editor/common/languages';
import { reset } from 'vs/base/browser/dom';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { URI } from 'vs/base/common/uri';
import { IResourceNode, ResourceTree } from 'vs/base/common/resourceTree';
export function isSCMRepositoryArray(element: any): element is ISCMRepository[] {
return Array.isArray(element) && element.every(r => isSCMRepository(r));
@ -45,6 +49,41 @@ export function isSCMResource(element: any): element is ISCMResource {
return !!(element as ISCMResource).sourceUri && isSCMResourceGroup((element as ISCMResource).resourceGroup);
}
export function isSCMResourceNode(element: any): element is IResourceNode<ISCMResource, ISCMResourceGroup> {
return ResourceTree.isResourceNode(element) && isSCMResourceGroup(element.context);
}
export function isSCMHistoryItemGroupTreeElement(element: any): element is SCMHistoryItemGroupTreeElement {
return (element as SCMHistoryItemGroupTreeElement).type === 'historyItemGroup';
}
export function isSCMHistoryItemTreeElement(element: any): element is SCMHistoryItemTreeElement {
return (element as SCMHistoryItemTreeElement).type === 'historyItem';
}
export function isSCMHistoryItemChangeTreeElement(element: any): element is SCMHistoryItemChangeTreeElement {
return (element as SCMHistoryItemChangeTreeElement).type === 'historyItemChange';
}
export function isSCMHistoryItemChangeNode(element: any): element is IResourceNode<SCMHistoryItemChangeTreeElement, SCMHistoryItemTreeElement> {
return ResourceTree.isResourceNode(element) && isSCMHistoryItemTreeElement(element.context);
}
export function isSCMViewSeparator(element: any): element is SCMViewSeparatorElement {
return (element as SCMViewSeparatorElement).type === 'separator';
}
export function toDiffEditorArguments(uri: URI, originalUri: URI, modifiedUri: URI): unknown[] {
const basename = path.basename(uri.fsPath);
const originalQuery = JSON.parse(originalUri.query) as { path: string; ref: string };
const modifiedQuery = JSON.parse(modifiedUri.query) as { path: string; ref: string };
const originalShortRef = originalQuery.ref.substring(0, 8).concat(originalQuery.ref.endsWith('^') ? '^' : '');
const modifiedShortRef = modifiedQuery.ref.substring(0, 8).concat(modifiedQuery.ref.endsWith('^') ? '^' : '');
return [originalUri, modifiedUri, `${basename} (${originalShortRef}) ↔ ${basename} (${modifiedShortRef})`, null];
}
const compareActions = (a: IAction, b: IAction) => a.id === b.id && a.enabled === b.enabled;
export function connectPrimaryMenu(menu: IMenu, callback: (primary: IAction[], secondary: IAction[]) => void, primaryGroup?: string): IDisposable {

View file

@ -6,7 +6,7 @@
import { Event } from 'vs/base/common/event';
import { ThemeIcon } from 'vs/base/common/themables';
import { URI } from 'vs/base/common/uri';
import { ISCMActionButtonDescriptor } from 'vs/workbench/contrib/scm/common/scm';
import { ISCMActionButtonDescriptor, ISCMRepository } from 'vs/workbench/contrib/scm/common/scm';
export interface ISCMHistoryProvider {
@ -30,15 +30,23 @@ export interface ISCMHistoryOptions {
readonly limit?: number | { id?: string };
}
export interface ISCMRemoteHistoryItemGroup {
readonly id: string;
readonly label: string;
}
export interface ISCMHistoryItemGroup {
readonly id: string;
readonly label: string;
readonly upstream?: ISCMRemoteHistoryItemGroup;
}
export interface ISCMRemoteHistoryItemGroup {
readonly id: string;
readonly label: string;
export interface SCMHistoryItemGroupTreeElement extends ISCMHistoryItemGroup {
readonly description?: string;
readonly ancestor?: string;
readonly count?: number;
readonly repository: ISCMRepository;
readonly type: 'historyItemGroup';
}
export interface ISCMHistoryItem {
@ -50,9 +58,25 @@ export interface ISCMHistoryItem {
readonly timestamp?: number;
}
export interface SCMHistoryItemTreeElement extends ISCMHistoryItem {
readonly historyItemGroup: SCMHistoryItemGroupTreeElement;
readonly type: 'historyItem';
}
export interface ISCMHistoryItemChange {
readonly uri: URI;
readonly originalUri?: URI;
readonly modifiedUri?: URI;
readonly renameUri?: URI;
}
export interface SCMHistoryItemChangeTreeElement extends ISCMHistoryItemChange {
readonly historyItem: SCMHistoryItemTreeElement;
readonly type: 'historyItemChange';
}
export interface SCMViewSeparatorElement {
readonly label: string;
readonly repository: ISCMRepository;
readonly type: 'separator';
}

View file

@ -494,7 +494,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer
}
});
CommandsRegistry.registerCommand('workbench.action.tasks.showLog', () => {
this._showOutput();
this._showOutput(undefined, true);
});
CommandsRegistry.registerCommand('workbench.action.tasks.build', async () => {
@ -636,15 +636,19 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer
this._workspace = setup[4];
}
protected _showOutput(runSource: TaskRunSource = TaskRunSource.User): void {
protected _showOutput(runSource: TaskRunSource = TaskRunSource.User, userRequested?: boolean): void {
if (!VirtualWorkspaceContext.getValue(this._contextKeyService) && ((runSource === TaskRunSource.User) || (runSource === TaskRunSource.ConfigurationChange))) {
this._notificationService.prompt(Severity.Warning, nls.localize('taskServiceOutputPrompt', 'There are task errors. See the output for details.'),
[{
label: nls.localize('showOutput', "Show output"),
run: () => {
this._outputService.showChannel(this._outputChannel.id, true);
}
}]);
if (userRequested) {
this._outputService.showChannel(this._outputChannel.id, true);
} else {
this._notificationService.prompt(Severity.Warning, nls.localize('taskServiceOutputPrompt', 'There are task errors. See the output for details.'),
[{
label: nls.localize('showOutput', "Show output"),
run: () => {
this._outputService.showChannel(this._outputChannel.id, true);
}
}]);
}
}
}

View file

@ -502,11 +502,19 @@
}
.terminal-scroll-highlight-outline {
border: 1px solid #ffffff;
border-left: 1px solid #ffffff;
border-right: 1px solid #ffffff;
pointer-events: none;
}
.terminal-scroll-highlight-outline.top {
border-top: 1px solid #ffffff;
}
.terminal-scroll-highlight-outline.bottom {
border-bottom: 1px solid #ffffff;
}
.terminal-scroll-highlight {
.terminal-scroll-highlight,
.terminal-scroll-highlight.terminal-scroll-highlight-outline {
border-color: var(--vscode-focusBorder);
}
.hc-black .xterm-find-result-decoration,

View file

@ -110,6 +110,7 @@ export interface IMarkTracker {
scrollToClosestMarker(startMarkerId: string, endMarkerId?: string, highlight?: boolean | undefined): void;
scrollToLine(line: number, position: ScrollPosition): void;
revealCommand(command: ITerminalCommand, position?: ScrollPosition): void;
registerTemporaryDecoration(marker: IMarker, endMarker?: IMarker): void;
}

View file

@ -6,12 +6,13 @@
import { coalesce } from 'vs/base/common/arrays';
import { Disposable, dispose } from 'vs/base/common/lifecycle';
import { IMarkTracker } from 'vs/workbench/contrib/terminal/browser/terminal';
import { ITerminalCapabilityStore, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities';
import { ITerminalCapabilityStore, ITerminalCommand, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities';
import type { Terminal, IMarker, ITerminalAddon, IDecoration } from '@xterm/xterm';
import { timeout } from 'vs/base/common/async';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { TERMINAL_OVERVIEW_RULER_CURSOR_FOREGROUND_COLOR } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry';
import { getWindow } from 'vs/base/browser/dom';
import { getPromptRowCount } from 'vs/platform/terminal/common/capabilities/commandDetectionCapability';
enum Boundary {
Top,
@ -67,6 +68,14 @@ export class MarkNavigationAddon extends Disposable implements IMarkTracker, ITe
return markers;
}
private _findCommand(marker: IMarker): ITerminalCommand | undefined {
const commandCapability = this._capabilities.get(TerminalCapability.CommandDetection);
if (commandCapability) {
return commandCapability.commands.find(e => e.marker?.line === marker.line);
}
return undefined;
}
clearMarker(): void {
// Clear the current marker so successive focus/selection actions are performed from the
// bottom of the buffer
@ -137,7 +146,7 @@ export class MarkNavigationAddon extends Disposable implements IMarkTracker, ITe
}
this._currentMarker = this._getMarkers(skipEmptyCommands)[markerIndex];
this._scrollToMarker(this._currentMarker, scrollPosition);
this._scrollToCommand(this._currentMarker, scrollPosition);
}
scrollToNextMark(scrollPosition: ScrollPosition = ScrollPosition.Middle, retainSelection: boolean = false, skipEmptyCommands: boolean = true): void {
@ -183,43 +192,65 @@ export class MarkNavigationAddon extends Disposable implements IMarkTracker, ITe
}
this._currentMarker = this._getMarkers(skipEmptyCommands)[markerIndex];
this._scrollToMarker(this._currentMarker, scrollPosition);
this._scrollToCommand(this._currentMarker, scrollPosition);
}
private _scrollToMarker(marker: IMarker, position: ScrollPosition, endMarker?: IMarker, hideDecoration?: boolean): void {
private _scrollToCommand(marker: IMarker, position: ScrollPosition): void {
const command = this._findCommand(marker);
if (command) {
this.revealCommand(command, position);
} else {
this._scrollToMarker(marker, position);
}
}
private _scrollToMarker(start: IMarker | number, position: ScrollPosition, end?: IMarker | number, hideDecoration?: boolean): void {
if (!this._terminal) {
return;
}
if (!this._isMarkerInViewport(this._terminal, marker)) {
const line = this.getTargetScrollLine(marker.line, position);
if (!this._isMarkerInViewport(this._terminal, start)) {
const line = this.getTargetScrollLine(toLineIndex(start), position);
this._terminal.scrollToLine(line);
}
if (!hideDecoration) {
this.registerTemporaryDecoration(marker, endMarker);
this.registerTemporaryDecoration(start, end);
}
}
private _createMarkerForOffset(marker: IMarker, offset: number): IMarker {
if (offset === 0) {
private _createMarkerForOffset(marker: IMarker | number, offset: number): IMarker {
if (offset === 0 && isMarker(marker)) {
return marker;
} else {
const offsetMarker = this._terminal?.registerMarker(-this._terminal.buffer.active.cursorY + marker.line - this._terminal.buffer.active.baseY + offset);
const offsetMarker = this._terminal?.registerMarker(-this._terminal.buffer.active.cursorY + toLineIndex(marker) - this._terminal.buffer.active.baseY + offset);
if (offsetMarker) {
return offsetMarker;
} else {
throw new Error(`Could not register marker with offset ${marker.line}, ${offset}`);
throw new Error(`Could not register marker with offset ${toLineIndex(marker)}, ${offset}`);
}
}
}
registerTemporaryDecoration(marker: IMarker, endMarker?: IMarker): void {
revealCommand(command: ITerminalCommand, position: ScrollPosition = ScrollPosition.Middle): void {
if (!this._terminal || !command.marker) {
return;
}
const promptRowCount = getPromptRowCount(command, this._terminal.buffer.active);
this._scrollToMarker(
command.marker.line - (promptRowCount - 1),
position,
command.marker
);
}
registerTemporaryDecoration(marker: IMarker | number, endMarker?: IMarker | number): void {
if (!this._terminal) {
return;
}
this._resetNavigationDecorations();
const color = this._themeService.getColorTheme().getColor(TERMINAL_OVERVIEW_RULER_CURSOR_FOREGROUND_COLOR);
const startLine = marker.line;
const decorationCount = endMarker ? endMarker.line - startLine + 1 : 1;
const startLine = toLineIndex(marker);
const decorationCount = endMarker ? toLineIndex(endMarker) - startLine + 1 : 1;
for (let i = 0; i < decorationCount; i++) {
const decoration = this._terminal.registerDecoration({
@ -236,14 +267,18 @@ export class MarkNavigationAddon extends Disposable implements IMarkTracker, ITe
decoration.onRender(element => {
if (!renderedElement) {
renderedElement = element;
if (decorationCount > 1) {
element.classList.add('terminal-scroll-highlight');
} else {
element.classList.add('terminal-scroll-highlight', 'terminal-scroll-highlight-outline');
element.classList.add('terminal-scroll-highlight', 'terminal-scroll-highlight-outline');
if (i === 0) {
element.classList.add('top');
}
if (this._terminal?.element) {
element.style.marginLeft = `-${getWindow(this._terminal.element).getComputedStyle(this._terminal.element).paddingLeft}`;
if (i === decorationCount - 1) {
element.classList.add('bottom');
}
} else {
element.classList.add('terminal-scroll-highlight', 'terminal-scroll-highlight-outline');
}
if (this._terminal?.element) {
element.style.marginLeft = `-${getWindow(this._terminal.element).getComputedStyle(this._terminal.element).paddingLeft}`;
}
});
decoration.onDispose(() => { this._navigationDecorations = this._navigationDecorations?.filter(d => d !== decoration); });
@ -270,9 +305,10 @@ export class MarkNavigationAddon extends Disposable implements IMarkTracker, ITe
return line;
}
private _isMarkerInViewport(terminal: Terminal, marker: IMarker) {
private _isMarkerInViewport(terminal: Terminal, marker: IMarker | number) {
const viewportY = terminal.buffer.active.viewportY;
return marker.line >= viewportY && marker.line < viewportY + terminal.rows;
const line = toLineIndex(marker);
return line >= viewportY && line < viewportY + terminal.rows;
}
scrollToClosestMarker(startMarkerId: string, endMarkerId?: string, highlight?: boolean | undefined): void {
@ -474,3 +510,11 @@ export function selectLines(xterm: Terminal, start: IMarker | Boundary, end: IMa
xterm.selectLines(startLine, endLine);
}
function isMarker(value: IMarker | number): value is IMarker {
return typeof value !== 'number';
}
function toLineIndex(line: IMarker | number): number {
return isMarker(line) ? line.line : line;
}

View file

@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import type { CanvasAddon as CanvasAddonType } from '@xterm/addon-canvas';
import type { SerializeAddon as SerializeAddonType } from '@xterm/addon-serialize';
import type { IMarker, ITerminalOptions, Terminal as RawXtermTerminal, Terminal as XTermTerminal } from '@xterm/xterm';
import type { ITerminalOptions, Terminal as RawXtermTerminal, Terminal as XTermTerminal } from '@xterm/xterm';
import { importAMDNodeModule } from 'vs/amdX';
import { $, addStandardDisposableListener } from 'vs/base/browser/dom';
import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
@ -13,10 +13,10 @@ import { Event } from 'vs/base/common/event';
import { Disposable, MutableDisposable, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle';
import 'vs/css!./media/stickyScroll';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ICommandDetectionCapability } from 'vs/platform/terminal/common/capabilities/capabilities';
import { ICommandDetectionCapability, ITerminalCommand } from 'vs/platform/terminal/common/capabilities/capabilities';
import { getPromptRowCount } from 'vs/platform/terminal/common/capabilities/commandDetectionCapability';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IXtermColorProvider, IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal';
import { ScrollPosition } from 'vs/workbench/contrib/terminal/browser/xterm/markNavigationAddon';
import { TERMINAL_CONFIG_SECTION } from 'vs/workbench/contrib/terminal/common/terminal';
import { terminalStickyScrollHoverBackground } from 'vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollColorRegistry';
@ -38,7 +38,7 @@ export class TerminalStickyScrollOverlay extends Disposable {
private _pendingCanvasAddon?: CancelablePromise<void>;
private _element?: HTMLElement;
private _currentStickyMarker?: IMarker;
private _currentStickyCommand?: ITerminalCommand;
private _currentContent?: string;
private _refreshListeners = this._register(new MutableDisposable());
@ -141,7 +141,7 @@ export class TerminalStickyScrollOverlay extends Disposable {
// The command from viewportY + 1 is used because this one will not be obscured by sticky
// scroll.
const command = this._commandDetection.getCommandForLine(this._xterm.raw.buffer.active.viewportY + 1);
this._currentStickyMarker = undefined;
this._currentStickyCommand = undefined;
// Sticky scroll only works with non-partial commands
if (!command || !('marker' in command)) {
@ -166,16 +166,7 @@ export class TerminalStickyScrollOverlay extends Disposable {
// TODO: Support multi-line commands
// Determine prompt length
let promptRowCount = 1;
let promptStartLine = marker.line;
if (command.promptStartMarker) {
promptStartLine = Math.min(command.promptStartMarker?.line ?? marker.line, marker.line);
// Trim any leading whitespace-only lines to retain vertical space
while (promptStartLine < marker.line && (buffer.getLine(promptStartLine)?.translateToString(true) ?? '').length === 0) {
promptStartLine++;
}
promptRowCount = marker.line - promptStartLine + 1;
}
const promptRowCount = getPromptRowCount(command, this._xterm.raw.buffer.active);
// Clear attrs, reset cursor position, clear right
const content = this._serializeAddon.serialize({
@ -198,7 +189,7 @@ export class TerminalStickyScrollOverlay extends Disposable {
}
if (content && command.exitCode !== undefined) {
this._currentStickyMarker = marker;
this._currentStickyCommand = command;
this._setVisible(true);
} else {
this._setVisible(false);
@ -229,9 +220,8 @@ export class TerminalStickyScrollOverlay extends Disposable {
// Scroll to the command on click
this._register(addStandardDisposableListener(hoverOverlay, 'click', () => {
if (this._xterm && this._currentStickyMarker) {
this._xterm.scrollToLine(this._currentStickyMarker.line, ScrollPosition.Middle);
this._xterm.markTracker.registerTemporaryDecoration(this._currentStickyMarker);
if (this._xterm && this._currentStickyCommand) {
this._xterm.markTracker.revealCommand(this._currentStickyCommand);
}
}));