mirror of
https://github.com/Microsoft/vscode
synced 2024-09-13 21:55:38 +00:00
Merge branch 'main' into tyriar/xterm_231109
This commit is contained in:
commit
169836d114
|
@ -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()
|
||||
};
|
||||
}));
|
||||
|
|
4
scripts/debugger-scripts-api.d.ts
vendored
4
scripts/debugger-scripts-api.d.ts
vendored
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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>>());
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}));
|
||||
|
||||
|
|
Loading…
Reference in a new issue