Merge branch 'main' into fix-201081

This commit is contained in:
Megan Rogge 2023-12-18 11:05:49 -06:00 committed by GitHub
commit 2535c6ca13
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 579 additions and 293 deletions

View file

@ -2961,8 +2961,10 @@
"configurationDefaults": {
"[git-commit]": {
"editor.rulers": [
50,
72
],
"editor.wordWrap": "off",
"workbench.editor.restoreViewState": false
},
"[git-rebase]": {

View file

@ -2543,7 +2543,7 @@ export class Repository {
return branch;
}
return Promise.reject<Branch>(new Error('No such branch'));
return Promise.reject<Branch>(new Error(`No such branch: ${name}`));
}
async getDefaultBranch(): Promise<Branch> {

View file

@ -14,8 +14,7 @@
{ "open": "(", "close": ")" },
{ "open": "\"", "close": "\"", "notIn": ["string"] },
{ "open": "'", "close": "'", "notIn": ["string"] },
{ "open": "<!--", "close": "-->", "notIn": [ "comment", "string" ]},
{ "open": "<![CDATA[", "close": "]]>", "notIn": [ "comment", "string" ]}
{ "open": "<!--", "close": "-->", "notIn": [ "comment", "string" ]}
],
"surroundingPairs": [
{ "open": "'", "close": "'" },

View file

@ -436,15 +436,17 @@ export function unmnemonicLabel(label: string): string {
}
/**
* Splits a recent label in name and parent path, supporting both '/' and '\' and workspace suffixes
* Splits a recent label in name and parent path, supporting both '/' and '\' and workspace suffixes.
* If the location is remote, the remote name is included in the name part.
*/
export function splitRecentLabel(recentLabel: string) {
export function splitRecentLabel(recentLabel: string): { name: string; parentPath: string } {
if (recentLabel.endsWith(']')) {
// label with workspace suffix
const lastIndexOfSquareBracket = recentLabel.lastIndexOf(' [', recentLabel.length - 2);
if (lastIndexOfSquareBracket !== -1) {
const split = splitName(recentLabel.substring(0, lastIndexOfSquareBracket));
return { name: split.name, parentPath: split.parentPath + recentLabel.substring(lastIndexOfSquareBracket) };
const remoteNameWithSpace = recentLabel.substring(lastIndexOfSquareBracket);
return { name: split.name + remoteNameWithSpace, parentPath: split.parentPath };
}
}
return splitName(recentLabel);

View file

@ -48,6 +48,25 @@ export function format2(template: string, values: Record<string, unknown>): stri
return template.replace(_format2Regexp, (match, group) => (values[group] ?? match) as string);
}
/**
* Encodes the given value so that it can be used as literal value in html attributes.
*
* In other words, computes `$val`, such that `attr` in `<div attr="$val" />` has the runtime value `value`.
* This prevents XSS injection.
*/
export function htmlAttributeEncodeValue(value: string): string {
return value.replace(/[<>"'&]/g, ch => {
switch (ch) {
case '<': return '&lt;';
case '>': return '&gt;';
case '"': return '&quot;';
case '\'': return '&apos;';
case '&': return '&amp;';
}
return ch;
});
}
/**
* Converts HTML characters inside the string to use entities instead. Makes the string safe from
* being used e.g. in HTMLElement.innerHTML.

View file

@ -532,3 +532,13 @@ suite('Strings', () => {
ensureNoDisposablesAreLeakedInTestSuite();
});
test('htmlAttributeEncodeValue', () => {
assert.strictEqual(strings.htmlAttributeEncodeValue(''), '');
assert.strictEqual(strings.htmlAttributeEncodeValue('abc'), 'abc');
assert.strictEqual(strings.htmlAttributeEncodeValue('<script>alert("Hello")</script>'), '&lt;script&gt;alert(&quot;Hello&quot;)&lt;/script&gt;');
assert.strictEqual(strings.htmlAttributeEncodeValue('Hello & World'), 'Hello &amp; World');
assert.strictEqual(strings.htmlAttributeEncodeValue('"Hello"'), '&quot;Hello&quot;');
assert.strictEqual(strings.htmlAttributeEncodeValue('\'Hello\''), '&apos;Hello&apos;');
assert.strictEqual(strings.htmlAttributeEncodeValue('<>&\'"'), '&lt;&gt;&amp;&apos;&quot;');
});

View file

@ -20,17 +20,17 @@ import { ViewContext } from 'vs/editor/common/viewModel/viewContext';
* This can end up producing multiple `LineDecorationToRender`.
*/
export class DecorationToRender {
_decorationToRenderBrand: void = undefined;
public readonly _decorationToRenderBrand: void = undefined;
public startLineNumber: number;
public endLineNumber: number;
public className: string;
public readonly zIndex: number;
constructor(startLineNumber: number, endLineNumber: number, className: string, zIndex: number | undefined) {
this.startLineNumber = +startLineNumber;
this.endLineNumber = +endLineNumber;
this.className = String(className);
constructor(
public readonly startLineNumber: number,
public readonly endLineNumber: number,
public readonly className: string,
public readonly tooltip: string | null,
zIndex: number | undefined,
) {
this.zIndex = zIndex ?? 0;
}
}
@ -42,6 +42,7 @@ export class LineDecorationToRender {
constructor(
public readonly className: string,
public readonly zIndex: number,
public readonly tooltip: string | null,
) { }
}
@ -108,7 +109,7 @@ export abstract class DedupOverlay extends DynamicViewOverlay {
}
for (let i = startLineIndex; i <= prevEndLineIndex; i++) {
output[i].add(new LineDecorationToRender(className, zIndex));
output[i].add(new LineDecorationToRender(className, zIndex, d.tooltip));
}
}

View file

@ -78,11 +78,11 @@ export class LinesDecorationsOverlay extends DedupOverlay {
const linesDecorationsClassName = d.options.linesDecorationsClassName;
const zIndex = d.options.zIndex;
if (linesDecorationsClassName) {
r[rLen++] = new DecorationToRender(d.range.startLineNumber, d.range.endLineNumber, linesDecorationsClassName, zIndex);
r[rLen++] = new DecorationToRender(d.range.startLineNumber, d.range.endLineNumber, linesDecorationsClassName, d.options.linesDecorationsTooltip ?? null, zIndex);
}
const firstLineDecorationClassName = d.options.firstLineDecorationClassName;
if (firstLineDecorationClassName) {
r[rLen++] = new DecorationToRender(d.range.startLineNumber, d.range.startLineNumber, firstLineDecorationClassName, zIndex);
r[rLen++] = new DecorationToRender(d.range.startLineNumber, d.range.startLineNumber, firstLineDecorationClassName, d.options.linesDecorationsTooltip ?? null, zIndex);
}
}
return r;
@ -103,7 +103,12 @@ export class LinesDecorationsOverlay extends DedupOverlay {
const decorations = toRender[lineIndex].getDecorations();
let lineOutput = '';
for (const decoration of decorations) {
lineOutput += '<div class="cldr ' + decoration.className + common;
let addition = '<div class="cldr ' + decoration.className;
if (decoration.tooltip !== null) {
addition += '" title="' + decoration.tooltip; // The tooltip is already escaped.
}
addition += common;
lineOutput += addition;
}
output[lineIndex] = lineOutput;
}

View file

@ -64,7 +64,7 @@ export class MarginViewLineDecorationsOverlay extends DedupOverlay {
const marginClassName = d.options.marginClassName;
const zIndex = d.options.zIndex;
if (marginClassName) {
r[rLen++] = new DecorationToRender(d.range.startLineNumber, d.range.endLineNumber, marginClassName, zIndex);
r[rLen++] = new DecorationToRender(d.range.startLineNumber, d.range.endLineNumber, marginClassName, null, zIndex);
}
}
return r;

View file

@ -1778,6 +1778,7 @@ export interface CommentReaction {
readonly count?: number;
readonly hasReacted?: boolean;
readonly canEdit?: boolean;
readonly reactors?: readonly string[];
}
/**

View file

@ -169,6 +169,10 @@ export interface IModelDecorationOptions {
* If set, the decoration will be rendered in the lines decorations with this CSS class name.
*/
linesDecorationsClassName?: string | null;
/**
* Controls the tooltip text of the line decoration.
*/
linesDecorationsTooltip?: string | null;
/**
* If set, the decoration will be rendered in the lines decorations with this CSS class name, but only for the first line in case of line wrapping.
*/

View file

@ -2297,6 +2297,7 @@ export class ModelDecorationOptions implements model.IModelDecorationOptions {
readonly glyphMargin?: model.IModelDecorationGlyphMarginOptions | null | undefined;
readonly glyphMarginClassName: string | null;
readonly linesDecorationsClassName: string | null;
readonly linesDecorationsTooltip: string | null;
readonly firstLineDecorationClassName: string | null;
readonly marginClassName: string | null;
readonly inlineClassName: string | null;
@ -2328,6 +2329,7 @@ export class ModelDecorationOptions implements model.IModelDecorationOptions {
this.glyphMargin = options.glyphMarginClassName ? new ModelDecorationGlyphMarginOptions(options.glyphMargin) : null;
this.glyphMarginClassName = options.glyphMarginClassName ? cleanClassName(options.glyphMarginClassName) : null;
this.linesDecorationsClassName = options.linesDecorationsClassName ? cleanClassName(options.linesDecorationsClassName) : null;
this.linesDecorationsTooltip = options.linesDecorationsTooltip ? strings.htmlAttributeEncodeValue(options.linesDecorationsTooltip) : null;
this.firstLineDecorationClassName = options.firstLineDecorationClassName ? cleanClassName(options.firstLineDecorationClassName) : null;
this.marginClassName = options.marginClassName ? cleanClassName(options.marginClassName) : null;
this.inlineClassName = options.inlineClassName ? cleanClassName(options.inlineClassName) : null;

View file

@ -24,6 +24,9 @@ export const foldingManualExpandedIcon = registerIcon('folding-manual-expanded',
const foldedBackgroundMinimap = { color: themeColorFromId(foldBackground), position: MinimapPosition.Inline };
const collapsed = localize('linesCollapsed', "Click to expand the range.");
const expanded = localize('linesExpanded', "Click to collapse the range.");
export class FoldingDecorationProvider implements IDecorationProvider {
private static readonly COLLAPSED_VISUAL_DECORATION = ModelDecorationOptions.register({
@ -31,6 +34,7 @@ export class FoldingDecorationProvider implements IDecorationProvider {
stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges,
afterContentClassName: 'inline-folded',
isWholeLine: true,
linesDecorationsTooltip: collapsed,
firstLineDecorationClassName: ThemeIcon.asClassName(foldingCollapsedIcon),
});
@ -41,6 +45,7 @@ export class FoldingDecorationProvider implements IDecorationProvider {
className: 'folded-background',
minimap: foldedBackgroundMinimap,
isWholeLine: true,
linesDecorationsTooltip: collapsed,
firstLineDecorationClassName: ThemeIcon.asClassName(foldingCollapsedIcon)
});
@ -49,6 +54,7 @@ export class FoldingDecorationProvider implements IDecorationProvider {
stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges,
afterContentClassName: 'inline-folded',
isWholeLine: true,
linesDecorationsTooltip: collapsed,
firstLineDecorationClassName: ThemeIcon.asClassName(foldingManualCollapsedIcon)
});
@ -59,6 +65,7 @@ export class FoldingDecorationProvider implements IDecorationProvider {
className: 'folded-background',
minimap: foldedBackgroundMinimap,
isWholeLine: true,
linesDecorationsTooltip: collapsed,
firstLineDecorationClassName: ThemeIcon.asClassName(foldingManualCollapsedIcon)
});
@ -66,7 +73,8 @@ export class FoldingDecorationProvider implements IDecorationProvider {
description: 'folding-no-controls-range-decoration',
stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges,
afterContentClassName: 'inline-folded',
isWholeLine: true
isWholeLine: true,
linesDecorationsTooltip: collapsed,
});
private static readonly NO_CONTROLS_COLLAPSED_HIGHLIGHTED_RANGE_DECORATION = ModelDecorationOptions.register({
@ -75,35 +83,40 @@ export class FoldingDecorationProvider implements IDecorationProvider {
afterContentClassName: 'inline-folded',
className: 'folded-background',
minimap: foldedBackgroundMinimap,
isWholeLine: true
isWholeLine: true,
linesDecorationsTooltip: collapsed,
});
private static readonly EXPANDED_VISUAL_DECORATION = ModelDecorationOptions.register({
description: 'folding-expanded-visual-decoration',
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
isWholeLine: true,
firstLineDecorationClassName: 'alwaysShowFoldIcons ' + ThemeIcon.asClassName(foldingExpandedIcon)
firstLineDecorationClassName: 'alwaysShowFoldIcons ' + ThemeIcon.asClassName(foldingExpandedIcon),
linesDecorationsTooltip: expanded,
});
private static readonly EXPANDED_AUTO_HIDE_VISUAL_DECORATION = ModelDecorationOptions.register({
description: 'folding-expanded-auto-hide-visual-decoration',
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
isWholeLine: true,
firstLineDecorationClassName: ThemeIcon.asClassName(foldingExpandedIcon)
firstLineDecorationClassName: ThemeIcon.asClassName(foldingExpandedIcon),
linesDecorationsTooltip: expanded,
});
private static readonly MANUALLY_EXPANDED_VISUAL_DECORATION = ModelDecorationOptions.register({
description: 'folding-manually-expanded-visual-decoration',
stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges,
isWholeLine: true,
firstLineDecorationClassName: 'alwaysShowFoldIcons ' + ThemeIcon.asClassName(foldingManualExpandedIcon)
firstLineDecorationClassName: 'alwaysShowFoldIcons ' + ThemeIcon.asClassName(foldingManualExpandedIcon),
linesDecorationsTooltip: expanded,
});
private static readonly MANUALLY_EXPANDED_AUTO_HIDE_VISUAL_DECORATION = ModelDecorationOptions.register({
description: 'folding-manually-expanded-auto-hide-visual-decoration',
stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges,
isWholeLine: true,
firstLineDecorationClassName: ThemeIcon.asClassName(foldingManualExpandedIcon)
firstLineDecorationClassName: ThemeIcon.asClassName(foldingManualExpandedIcon),
linesDecorationsTooltip: expanded,
});
private static readonly NO_CONTROLS_EXPANDED_RANGE_DECORATION = ModelDecorationOptions.register({

4
src/vs/monaco.d.ts vendored
View file

@ -1699,6 +1699,10 @@ declare namespace monaco.editor {
* If set, the decoration will be rendered in the lines decorations with this CSS class name.
*/
linesDecorationsClassName?: string | null;
/**
* Controls the tooltip text of the line decoration.
*/
linesDecorationsTooltip?: string | null;
/**
* If set, the decoration will be rendered in the lines decorations with this CSS class name, but only for the first line in case of line wrapping.
*/

View file

@ -137,6 +137,9 @@ async function doResolveUnixShellEnv(logService: ILogService, token: Cancellatio
} else if (name === 'nu') { // nushell requires ^ before quoted path to treat it as a command
command = `^'${process.execPath}' ${extraArgs} -p '"${mark}" + JSON.stringify(process.env) + "${mark}"'`;
shellArgs = ['-i', '-l', '-c'];
} else if (name === 'xonsh') { // #200374: native implementation is shorter
command = `import os, json; print("${mark}", json.dumps(dict(os.environ)), "${mark}")`;
shellArgs = ['-i', '-l', '-c'];
} else {
command = `'${process.execPath}' ${extraArgs} -p '"${mark}" + JSON.stringify(process.env) + "${mark}"'`;

View file

@ -48,10 +48,37 @@ export class ThemeMainService extends Disposable implements IThemeMainService {
constructor(@IStateService private stateService: IStateService, @IConfigurationService private configurationService: IConfigurationService) {
super();
// System Theme
this._register(this.configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('window.systemColorTheme')) {
this.updateSystemColorTheme();
}
}));
this.updateSystemColorTheme();
// Color Scheme changes
nativeTheme.on('updated', () => {
this._onDidChangeColorScheme.fire(this.getColorScheme());
});
this._register(Event.fromNodeEventEmitter(nativeTheme, 'updated')(() => this._onDidChangeColorScheme.fire(this.getColorScheme())));
}
private updateSystemColorTheme(): void {
switch (this.configurationService.getValue<'default' | 'auto' | 'light' | 'dark'>('window.systemColorTheme')) {
case 'dark':
nativeTheme.themeSource = 'dark';
break;
case 'light':
nativeTheme.themeSource = 'light';
break;
case 'auto':
switch (this.getBaseTheme()) {
case 'vs': nativeTheme.themeSource = 'light'; break;
case 'vs-dark': nativeTheme.themeSource = 'dark'; break;
default: nativeTheme.themeSource = 'system';
}
break;
default:
nativeTheme.themeSource = 'system';
break;
}
}
getColorScheme(): IColorScheme {
@ -86,8 +113,7 @@ export class ThemeMainService extends Disposable implements IThemeMainService {
let background = this.stateService.getItem<string | null>(THEME_BG_STORAGE_KEY, null);
if (!background) {
const baseTheme = this.stateService.getItem<string>(THEME_STORAGE_KEY, 'vs-dark').split(' ')[0];
switch (baseTheme) {
switch (this.getBaseTheme()) {
case 'vs': background = DEFAULT_BG_LIGHT; break;
case 'hc-black': background = DEFAULT_BG_HC_BLACK; break;
case 'hc-light': background = DEFAULT_BG_HC_LIGHT; break;
@ -102,6 +128,16 @@ export class ThemeMainService extends Disposable implements IThemeMainService {
return background;
}
private getBaseTheme(): 'vs' | 'vs-dark' | 'hc-black' | 'hc-light' {
const baseTheme = this.stateService.getItem<string>(THEME_STORAGE_KEY, 'vs-dark').split(' ')[0];
switch (baseTheme) {
case 'vs': return 'vs';
case 'hc-black': return 'hc-black';
case 'hc-light': return 'hc-light';
default: return 'vs-dark';
}
}
saveWindowSplash(windowId: number | undefined, splash: IPartsSplash): void {
// Update in storage
@ -115,6 +151,9 @@ export class ThemeMainService extends Disposable implements IThemeMainService {
if (typeof windowId === 'number') {
this.updateBackgroundColor(windowId, splash);
}
// Update system theme
this.updateSystemColorTheme();
}
private updateBackgroundColor(windowId: number, splash: IPartsSplash): void {

View file

@ -673,6 +673,10 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo
checkProposedApiEnabled(extension, 'commentsDraftState');
}
if (vscodeComment.reactions?.some(reaction => reaction.reactors !== undefined)) {
checkProposedApiEnabled(extension, 'commentReactor');
}
return {
mode: vscodeComment.mode,
contextValue: vscodeComment.contextValue,
@ -693,6 +697,7 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo
iconPath: reaction.iconPath ? extHostTypeConverter.pathOrURIToURI(reaction.iconPath) : undefined,
count: reaction.count,
hasReacted: reaction.authorHasReacted,
reactors: reaction.reactors
};
}
@ -701,7 +706,8 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo
label: reaction.label || '',
count: reaction.count || 0,
iconPath: reaction.iconPath ? URI.revive(reaction.iconPath) : '',
authorHasReacted: reaction.hasReacted || false
authorHasReacted: reaction.hasReacted || false,
reactors: reaction.reactors
};
}

View file

@ -459,7 +459,7 @@ export class CommentNode<T extends IRange | ICellRange> extends Disposable {
}
this.notificationService.error(error);
}
}, reaction.iconPath, reaction.count);
}, reaction.reactors, reaction.iconPath, reaction.count);
this._reactionsActionBar?.push(action, { label: true, icon: true });
});

View file

@ -20,6 +20,7 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/commentContextKeys';
import { ILogService } from 'vs/platform/log/common/log';
import { CommentsModel, ICommentsModel } from 'vs/workbench/contrib/comments/browser/commentsModel';
export const ICommentService = createDecorator<ICommentService>('commentService');
@ -86,6 +87,7 @@ export interface ICommentService {
readonly onDidDeleteDataProvider: Event<string | undefined>;
readonly onDidChangeCommentingEnabled: Event<boolean>;
readonly isCommentingEnabled: boolean;
readonly commentsModel: ICommentsModel;
setDocumentComments(resource: URI, commentInfos: ICommentInfo[]): void;
setWorkspaceComments(owner: string, commentsByResource: CommentThread<IRange | ICellRange>[]): void;
removeWorkspaceComments(owner: string): void;
@ -162,6 +164,9 @@ export class CommentService extends Disposable implements ICommentService {
private _continueOnComments = new Map<string, PendingCommentThread[]>(); // owner -> PendingCommentThread[]
private _continueOnCommentProviders = new Set<IContinueOnCommentProvider>();
private readonly _commentsModel: CommentsModel = this._register(new CommentsModel());
public readonly commentsModel: ICommentsModel = this._commentsModel;
constructor(
@IInstantiationService protected readonly instantiationService: IInstantiationService,
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
@ -200,7 +205,7 @@ export class CommentService extends Disposable implements ICommentService {
removed: [],
changed: []
};
this._onDidUpdateCommentThreads.fire(evt);
this.updateModelThreads(evt);
}
}));
this._register(storageService.onWillSaveState(() => {
@ -272,20 +277,31 @@ export class CommentService extends Disposable implements ICommentService {
this._onDidSetResourceCommentInfos.fire({ resource, commentInfos });
}
private setModelThreads(ownerId: string, ownerLabel: string, commentThreads: CommentThread<IRange>[]) {
this._commentsModel.setCommentThreads(ownerId, ownerLabel, commentThreads);
this._onDidSetAllCommentThreads.fire({ ownerId, ownerLabel, commentThreads });
}
private updateModelThreads(event: ICommentThreadChangedEvent) {
this._commentsModel.updateCommentThreads(event);
this._onDidUpdateCommentThreads.fire(event);
}
setWorkspaceComments(owner: string, commentsByResource: CommentThread[]): void {
if (commentsByResource.length) {
this._workspaceHasCommenting.set(true);
}
const control = this._commentControls.get(owner);
if (control) {
this._onDidSetAllCommentThreads.fire({ ownerId: owner, ownerLabel: control.label, commentThreads: commentsByResource });
this.setModelThreads(owner, control.label, commentsByResource);
}
}
removeWorkspaceComments(owner: string): void {
const control = this._commentControls.get(owner);
if (control) {
this._onDidSetAllCommentThreads.fire({ ownerId: owner, ownerLabel: control.label, commentThreads: [] });
this.setModelThreads(owner, control.label, []);
}
}
@ -300,6 +316,7 @@ export class CommentService extends Disposable implements ICommentService {
} else {
this._commentControls.clear();
}
this._commentsModel.deleteCommentsByOwner(owner);
this._onDidDeleteDataProvider.fire(owner);
}
@ -346,7 +363,7 @@ export class CommentService extends Disposable implements ICommentService {
const control = this._commentControls.get(ownerId);
if (control) {
const evt: ICommentThreadChangedEvent = Object.assign({}, event, { owner: ownerId, ownerLabel: control.label });
this._onDidUpdateCommentThreads.fire(evt);
this.updateModelThreads(evt);
}
}

View file

@ -7,7 +7,7 @@ import * as nls from 'vs/nls';
import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { Registry } from 'vs/platform/registry/common/platform';
import 'vs/workbench/contrib/comments/browser/commentsEditorContribution';
import { ICommentService, CommentService } from 'vs/workbench/contrib/comments/browser/commentService';
import { ICommentService, CommentService, IWorkspaceCommentThreadsEvent } from 'vs/workbench/contrib/comments/browser/commentService';
import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry';
import { ctxCommentEditorFocused } from 'vs/workbench/contrib/comments/browser/simpleCommentEditor';
import * as strings from 'vs/base/common/strings';
@ -16,12 +16,17 @@ import { AccessibleViewType, IAccessibleContentProvider, IAccessibleViewOptions,
import { AccessibilityHelpAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { Disposable } from 'vs/base/common/lifecycle';
import { Disposable, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/commentContextKeys';
import { CommentCommandId } from 'vs/workbench/contrib/comments/common/commentCommandIds';
import { ToggleTabFocusModeAction } from 'vs/editor/contrib/toggleTabFocusMode/browser/toggleTabFocusMode';
import { getActiveElement } from 'vs/base/browser/dom';
import { Extensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions';
import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity';
import { COMMENTS_VIEW_ID } from 'vs/workbench/contrib/comments/browser/commentsTreeViewer';
import { CommentThreadState } from 'vs/editor/common/languages';
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).registerConfiguration({
id: 'comments',
@ -68,7 +73,6 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).regis
registerSingleton(ICommentService, CommentService, InstantiationType.Delayed);
export namespace CommentAccessibilityHelpNLS {
export const intro = nls.localize('intro', "The editor contains commentable range(s). Some useful commands include:");
export const introWidget = nls.localize('introWidget', "This widget contains a text area, for composition of new comments, and actions, that can be tabbed to once tab moves focus mode has been enabled ({0}).");
@ -134,3 +138,51 @@ export class CommentsAccessibilityHelpProvider implements IAccessibleContentProv
this._element?.focus();
}
}
export class UnresolvedCommentsBadge extends Disposable implements IWorkbenchContribution {
private readonly activity = this._register(new MutableDisposable<IDisposable>());
private totalUnresolved = 0;
constructor(
@ICommentService private readonly _commentService: ICommentService,
@IActivityService private readonly activityService: IActivityService) {
super();
this._register(this._commentService.onDidSetAllCommentThreads(this.onAllCommentsChanged, this));
this._register(this._commentService.onDidUpdateCommentThreads(this.onCommentsUpdated, this));
}
private onAllCommentsChanged(e: IWorkspaceCommentThreadsEvent): void {
let unresolved = 0;
for (const thread of e.commentThreads) {
if (thread.state === CommentThreadState.Unresolved) {
unresolved++;
}
}
this.updateBadge(unresolved);
}
private onCommentsUpdated(): void {
let unresolved = 0;
for (const resource of this._commentService.commentsModel.resourceCommentThreads) {
for (const thread of resource.commentThreads) {
if (thread.threadState === CommentThreadState.Unresolved) {
unresolved++;
}
}
}
this.updateBadge(unresolved);
}
private updateBadge(unresolved: number) {
if (unresolved === this.totalUnresolved) {
return;
}
this.totalUnresolved = unresolved;
const message = nls.localize('totalUnresolvedComments', '{0} Unresolved Comments', this.totalUnresolved);
this.activity.value = this.activityService.showViewActivity(COMMENTS_VIEW_ID, { badge: new NumberBadge(this.totalUnresolved, () => message) });
}
}
Registry.as<IWorkbenchContributionsRegistry>(Extensions.Workbench).registerWorkbenchContribution(UnresolvedCommentsBadge, LifecyclePhase.Eventually);

View file

@ -0,0 +1,155 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { groupBy } from 'vs/base/common/arrays';
import { URI } from 'vs/base/common/uri';
import { CommentThread } from 'vs/editor/common/languages';
import { localize } from 'vs/nls';
import { ResourceWithCommentThreads, ICommentThreadChangedEvent } from 'vs/workbench/contrib/comments/common/commentModel';
import { Disposable } from 'vs/base/common/lifecycle';
export interface ICommentsModel {
hasCommentThreads(): boolean;
getMessage(): string;
readonly resourceCommentThreads: ResourceWithCommentThreads[];
readonly commentThreadsMap: Map<string, { resourceWithCommentThreads: ResourceWithCommentThreads[]; ownerLabel?: string }>;
}
export class CommentsModel extends Disposable implements ICommentsModel {
readonly _serviceBrand: undefined;
private _resourceCommentThreads: ResourceWithCommentThreads[];
get resourceCommentThreads(): ResourceWithCommentThreads[] { return this._resourceCommentThreads; }
readonly commentThreadsMap: Map<string, { resourceWithCommentThreads: ResourceWithCommentThreads[]; ownerLabel?: string }>;
constructor(
) {
super();
this._resourceCommentThreads = [];
this.commentThreadsMap = new Map<string, { resourceWithCommentThreads: ResourceWithCommentThreads[]; ownerLabel: string }>();
}
private updateResourceCommentThreads() {
const includeLabel = this.commentThreadsMap.size > 1;
this._resourceCommentThreads = [...this.commentThreadsMap.values()].map(value => {
return value.resourceWithCommentThreads.map(resource => {
resource.ownerLabel = includeLabel ? value.ownerLabel : undefined;
return resource;
}).flat();
}).flat();
this._resourceCommentThreads.sort((a, b) => {
return a.resource.toString() > b.resource.toString() ? 1 : -1;
});
}
public setCommentThreads(owner: string, ownerLabel: string, commentThreads: CommentThread[]): void {
this.commentThreadsMap.set(owner, { ownerLabel, resourceWithCommentThreads: this.groupByResource(owner, commentThreads) });
this.updateResourceCommentThreads();
}
public deleteCommentsByOwner(owner?: string): void {
if (owner) {
const existingOwner = this.commentThreadsMap.get(owner);
this.commentThreadsMap.set(owner, { ownerLabel: existingOwner?.ownerLabel, resourceWithCommentThreads: [] });
} else {
this.commentThreadsMap.clear();
}
this.updateResourceCommentThreads();
}
public updateCommentThreads(event: ICommentThreadChangedEvent): boolean {
const { owner, ownerLabel, removed, changed, added } = event;
const threadsForOwner = this.commentThreadsMap.get(owner)?.resourceWithCommentThreads || [];
removed.forEach(thread => {
// Find resource that has the comment thread
const matchingResourceIndex = threadsForOwner.findIndex((resourceData) => resourceData.id === thread.resource);
const matchingResourceData = matchingResourceIndex >= 0 ? threadsForOwner[matchingResourceIndex] : undefined;
// Find comment node on resource that is that thread and remove it
const index = matchingResourceData?.commentThreads.findIndex((commentThread) => commentThread.threadId === thread.threadId) ?? 0;
if (index >= 0) {
matchingResourceData?.commentThreads.splice(index, 1);
}
// If the comment thread was the last thread for a resource, remove that resource from the list
if (matchingResourceData?.commentThreads.length === 0) {
threadsForOwner.splice(matchingResourceIndex, 1);
}
});
changed.forEach(thread => {
// Find resource that has the comment thread
const matchingResourceIndex = threadsForOwner.findIndex((resourceData) => resourceData.id === thread.resource);
const matchingResourceData = matchingResourceIndex >= 0 ? threadsForOwner[matchingResourceIndex] : undefined;
if (!matchingResourceData) {
return;
}
// Find comment node on resource that is that thread and replace it
const index = matchingResourceData.commentThreads.findIndex((commentThread) => commentThread.threadId === thread.threadId);
if (index >= 0) {
matchingResourceData.commentThreads[index] = ResourceWithCommentThreads.createCommentNode(owner, URI.parse(matchingResourceData.id), thread);
} else if (thread.comments && thread.comments.length) {
matchingResourceData.commentThreads.push(ResourceWithCommentThreads.createCommentNode(owner, URI.parse(matchingResourceData.id), thread));
}
});
added.forEach(thread => {
const existingResource = threadsForOwner.filter(resourceWithThreads => resourceWithThreads.resource.toString() === thread.resource);
if (existingResource.length) {
const resource = existingResource[0];
if (thread.comments && thread.comments.length) {
resource.commentThreads.push(ResourceWithCommentThreads.createCommentNode(owner, resource.resource, thread));
}
} else {
threadsForOwner.push(new ResourceWithCommentThreads(owner, URI.parse(thread.resource!), [thread]));
}
});
this.commentThreadsMap.set(owner, { ownerLabel, resourceWithCommentThreads: threadsForOwner });
this.updateResourceCommentThreads();
return removed.length > 0 || changed.length > 0 || added.length > 0;
}
public hasCommentThreads(): boolean {
return !!this._resourceCommentThreads.length;
}
public getMessage(): string {
if (!this._resourceCommentThreads.length) {
return localize('noComments', "There are no comments in this workspace yet.");
} else {
return '';
}
}
private groupByResource(owner: string, commentThreads: CommentThread[]): ResourceWithCommentThreads[] {
const resourceCommentThreads: ResourceWithCommentThreads[] = [];
const commentThreadsByResource = new Map<string, ResourceWithCommentThreads>();
for (const group of groupBy(commentThreads, CommentsModel._compareURIs)) {
commentThreadsByResource.set(group[0].resource!, new ResourceWithCommentThreads(owner, URI.parse(group[0].resource!), group));
}
commentThreadsByResource.forEach((v, i, m) => {
resourceCommentThreads.push(v);
});
return resourceCommentThreads;
}
private static _compareURIs(a: CommentThread, b: CommentThread) {
const resourceA = a.resource!.toString();
const resourceB = b.resource!.toString();
if (resourceA < resourceB) {
return -1;
} else if (resourceA > resourceB) {
return 1;
} else {
return 0;
}
}
}

View file

@ -9,7 +9,7 @@ import { renderMarkdown } from 'vs/base/browser/markdownRenderer';
import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels';
import { CommentNode, CommentsModel, ResourceWithCommentThreads } from 'vs/workbench/contrib/comments/common/commentModel';
import { CommentNode, ResourceWithCommentThreads } from 'vs/workbench/contrib/comments/common/commentModel';
import { ITreeFilter, ITreeNode, TreeFilterResult, TreeVisibility } from 'vs/base/browser/ui/tree/tree';
import { IListVirtualDelegate, IListRenderer } from 'vs/base/browser/ui/list/list';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
@ -31,6 +31,7 @@ import { openLinkFromMarkdown } from 'vs/editor/contrib/markdownRenderer/browser
import { IStyleOverride } from 'vs/platform/theme/browser/defaultStyles';
import { IListStyles } from 'vs/base/browser/ui/list/listWidget';
import { ILocalizedString } from 'vs/platform/action/common/action';
import { CommentsModel } from 'vs/workbench/contrib/comments/browser/commentsModel';
export const COMMENTS_VIEW_ID = 'workbench.panel.comments';
export const COMMENTS_VIEW_STORAGE_ID = 'Comments';

View file

@ -10,7 +10,7 @@ import { basename } from 'vs/base/common/resources';
import { isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { CommentNode, CommentsModel, ResourceWithCommentThreads, ICommentThreadChangedEvent } from 'vs/workbench/contrib/comments/common/commentModel';
import { CommentNode, ResourceWithCommentThreads, ICommentThreadChangedEvent } from 'vs/workbench/contrib/comments/common/commentModel';
import { IWorkspaceCommentThreadsEvent, ICommentService } from 'vs/workbench/contrib/comments/browser/commentService';
import { IEditorService, ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
@ -35,20 +35,19 @@ import { CommentsFilters, CommentsFiltersChangeEvent } from 'vs/workbench/contri
import { Memento, MementoObject } from 'vs/workbench/common/memento';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { FilterOptions } from 'vs/workbench/contrib/comments/browser/commentsFilterOptions';
import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity';
import { CommentThreadState } from 'vs/editor/common/languages';
import { IDisposable, MutableDisposable } from 'vs/base/common/lifecycle';
import { ITreeElement } from 'vs/base/browser/ui/tree/tree';
import { Iterable } from 'vs/base/common/iterator';
import { CommentController } from 'vs/workbench/contrib/comments/browser/commentsController';
import { Range } from 'vs/editor/common/core/range';
import { registerNavigableContainer } from 'vs/workbench/browser/actions/widgetNavigationCommands';
import { CommentsModel, ICommentsModel } from 'vs/workbench/contrib/comments/browser/commentsModel';
const CONTEXT_KEY_HAS_COMMENTS = new RawContextKey<boolean>('commentsView.hasComments', false);
const CONTEXT_KEY_SOME_COMMENTS_EXPANDED = new RawContextKey<boolean>('commentsView.someCommentsExpanded', false);
const VIEW_STORAGE_ID = 'commentsViewState';
function createResourceCommentsIterator(model: CommentsModel): Iterable<ITreeElement<ResourceWithCommentThreads | CommentNode>> {
function createResourceCommentsIterator(model: ICommentsModel): Iterable<ITreeElement<ResourceWithCommentThreads | CommentNode>> {
return Iterable.map(model.resourceCommentThreads, m => {
const CommentNodeIt = Iterable.from(m.commentThreads);
const children = Iterable.map(CommentNodeIt, r => ({ element: r }));
@ -62,14 +61,11 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView {
private tree: CommentsList | undefined;
private treeContainer!: HTMLElement;
private messageBoxContainer!: HTMLElement;
private commentsModel!: CommentsModel;
private totalComments: number = 0;
private totalUnresolved = 0;
private readonly hasCommentsContextKey: IContextKey<boolean>;
private readonly someCommentsExpandedContextKey: IContextKey<boolean>;
private readonly filter: Filter;
readonly filters: CommentsFilters;
private readonly activity = this._register(new MutableDisposable<IDisposable>());
private currentHeight = 0;
private currentWidth = 0;
@ -93,7 +89,6 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView {
@ICommentService private readonly commentService: ICommentService,
@ITelemetryService telemetryService: ITelemetryService,
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
@IActivityService private readonly activityService: IActivityService,
@IStorageService storageService: IStorageService
) {
const stateMemento = new Memento(VIEW_STORAGE_ID, storageService);
@ -127,16 +122,6 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView {
this._register(this.filterWidget.onDidChangeFilterText(() => this.updateFilter()));
}
private updateBadge(unresolved: number) {
if (unresolved === this.totalUnresolved) {
return;
}
this.totalUnresolved = unresolved;
const message = nls.localize('totalUnresolvedComments', '{0} Unresolved Comments', this.totalUnresolved);
this.activity.value = this.activityService.showViewActivity(this.id, { badge: new NumberBadge(this.totalUnresolved, () => message) });
}
override saveState(): void {
this.viewState['filter'] = this.filterWidget.getFilterText();
this.viewState['filterHistory'] = this.filterWidget.getHistory();
@ -201,7 +186,6 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView {
this.treeContainer = dom.append(domContainer, dom.$('.tree-container'));
this.treeContainer.classList.add('file-icon-themable-tree', 'show-file-icons');
this.commentsModel = new CommentsModel();
this.cachedFilterStats = undefined;
this.createTree();
@ -232,7 +216,7 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView {
return;
}
if (!this.commentsModel.hasCommentThreads() && this.messageBoxContainer) {
if (!this.commentService.commentsModel.hasCommentThreads() && this.messageBoxContainer) {
this.messageBoxContainer.focus();
} else if (this.tree) {
this.tree.domFocus();
@ -267,9 +251,9 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView {
}
private async renderComments(): Promise<void> {
this.treeContainer.classList.toggle('hidden', !this.commentsModel.hasCommentThreads());
this.treeContainer.classList.toggle('hidden', !this.commentService.commentsModel.hasCommentThreads());
this.renderMessage();
await this.tree?.setChildren(null, createResourceCommentsIterator(this.commentsModel));
await this.tree?.setChildren(null, createResourceCommentsIterator(this.commentService.commentsModel));
}
public collapseAll() {
@ -311,8 +295,8 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView {
}
private renderMessage(): void {
this.messageBoxContainer.textContent = this.commentsModel.getMessage();
this.messageBoxContainer.classList.toggle('hidden', this.commentsModel.hasCommentThreads());
this.messageBoxContainer.textContent = this.commentService.commentsModel.getMessage();
this.messageBoxContainer.classList.toggle('hidden', this.commentService.commentsModel.hasCommentThreads());
}
private createTree(): void {
@ -437,15 +421,15 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView {
return;
}
if (this.isVisible()) {
this.hasCommentsContextKey.set(this.commentsModel.hasCommentThreads());
this.hasCommentsContextKey.set(this.commentService.commentsModel.hasCommentThreads());
this.treeContainer.classList.toggle('hidden', !this.commentsModel.hasCommentThreads());
this.treeContainer.classList.toggle('hidden', !this.commentService.commentsModel.hasCommentThreads());
this.cachedFilterStats = undefined;
this.renderMessage();
this.tree?.setChildren(null, createResourceCommentsIterator(this.commentsModel));
this.tree?.setChildren(null, createResourceCommentsIterator(this.commentService.commentsModel));
if (this.tree.getSelection().length === 0 && this.commentsModel.hasCommentThreads()) {
const firstComment = this.commentsModel.resourceCommentThreads[0].commentThreads[0];
if (this.tree.getSelection().length === 0 && this.commentService.commentsModel.hasCommentThreads()) {
const firstComment = this.commentService.commentsModel.resourceCommentThreads[0].commentThreads[0];
if (firstComment) {
this.tree.setFocus([firstComment]);
this.tree.setSelection([firstComment]);
@ -456,7 +440,6 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView {
private onAllCommentsChanged(e: IWorkspaceCommentThreadsEvent): void {
this.cachedFilterStats = undefined;
this.commentsModel.setCommentThreads(e.ownerId, e.ownerLabel, e.commentThreads);
this.totalComments += e.commentThreads.length;
let unresolved = 0;
@ -465,37 +448,28 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView {
unresolved++;
}
}
this.updateBadge(unresolved);
this.refresh();
}
private onCommentsUpdated(e: ICommentThreadChangedEvent): void {
this.cachedFilterStats = undefined;
const didUpdate = this.commentsModel.updateCommentThreads(e);
this.totalComments += e.added.length;
this.totalComments -= e.removed.length;
let unresolved = 0;
for (const resource of this.commentsModel.resourceCommentThreads) {
for (const resource of this.commentService.commentsModel.resourceCommentThreads) {
for (const thread of resource.commentThreads) {
if (thread.threadState === CommentThreadState.Unresolved) {
unresolved++;
}
}
}
this.updateBadge(unresolved);
if (didUpdate) {
this.refresh();
}
this.refresh();
}
private onDataProviderDeleted(owner: string | undefined): void {
this.cachedFilterStats = undefined;
this.commentsModel.deleteCommentsByOwner(owner);
this.totalComments = 0;
this.refresh();
}

View file

@ -66,27 +66,46 @@ export class ReactionActionViewItem extends ActionViewItem {
'This is a tooltip for an emoji button so that the current user can toggle their reaction to a comment.',
'The first arg is localized message "Toggle reaction" or empty if the user doesn\'t have permission to toggle the reaction, the second is the name of the reaction.']
}, "{0}{1} reaction", toggleMessage, action.label);
} else if (action.count === 1) {
return nls.localize({
key: 'comment.reactionLabelOne', comment: [
'This is a tooltip for an emoji that is a "reaction" to a comment where the count of the reactions is 1.',
'The emoji is also a button so that the current user can also toggle their own emoji reaction.',
'The first arg is localized message "Toggle reaction" or empty if the user doesn\'t have permission to toggle the reaction, the second is the name of the reaction.']
}, "{0}1 reaction with {1}", toggleMessage, action.label);
} else if (action.count > 1) {
return nls.localize({
key: 'comment.reactionLabelMany', comment: [
'This is a tooltip for an emoji that is a "reaction" to a comment where the count of the reactions is greater than 1.',
'The emoji is also a button so that the current user can also toggle their own emoji reaction.',
'The first arg is localized message "Toggle reaction" or empty if the user doesn\'t have permission to toggle the reaction, the second is number of users who have reacted with that reaction, and the third is the name of the reaction.']
}, "{0}{1} reactions with {2}", toggleMessage, action.count, action.label);
} else if (action.reactors === undefined || action.reactors.length === 0) {
if (action.count === 1) {
return nls.localize({
key: 'comment.reactionLabelOne', comment: [
'This is a tooltip for an emoji that is a "reaction" to a comment where the count of the reactions is 1.',
'The emoji is also a button so that the current user can also toggle their own emoji reaction.',
'The first arg is localized message "Toggle reaction" or empty if the user doesn\'t have permission to toggle the reaction, the second is the name of the reaction.']
}, "{0}1 reaction with {1}", toggleMessage, action.label);
} else if (action.count > 1) {
return nls.localize({
key: 'comment.reactionLabelMany', comment: [
'This is a tooltip for an emoji that is a "reaction" to a comment where the count of the reactions is greater than 1.',
'The emoji is also a button so that the current user can also toggle their own emoji reaction.',
'The first arg is localized message "Toggle reaction" or empty if the user doesn\'t have permission to toggle the reaction, the second is number of users who have reacted with that reaction, and the third is the name of the reaction.']
}, "{0}{1} reactions with {2}", toggleMessage, action.count, action.label);
}
} else {
if (action.reactors.length <= 10 && action.reactors.length === action.count) {
return nls.localize({
key: 'comment.reactionLessThanTen', comment: [
'This is a tooltip for an emoji that is a "reaction" to a comment where the count of the reactions is less than or equal to 10.',
'The emoji is also a button so that the current user can also toggle their own emoji reaction.',
'The first arg is localized message "Toggle reaction" or empty if the user doesn\'t have permission to toggle the reaction, the second iis a list of the reactors, and the third is the name of the reaction.']
}, "{0}{1} reacted with {2}", toggleMessage, action.reactors.join(', '), action.label);
} else if (action.count > 1) {
const displayedReactors = action.reactors.slice(0, 10);
return nls.localize({
key: 'comment.reactionMoreThanTen', comment: [
'This is a tooltip for an emoji that is a "reaction" to a comment where the count of the reactions is less than or equal to 10.',
'The emoji is also a button so that the current user can also toggle their own emoji reaction.',
'The first arg is localized message "Toggle reaction" or empty if the user doesn\'t have permission to toggle the reaction, the second iis a list of the reactors, and the third is the name of the reaction.']
}, "{0}{1} and {2} more reacted with {3}", toggleMessage, displayedReactors.join(', '), action.count - displayedReactors.length, action.label);
}
}
return undefined;
}
}
export class ReactionAction extends Action {
static readonly ID = 'toolbar.toggle.reaction';
constructor(id: string, label: string = '', cssClass: string = '', enabled: boolean = true, actionCallback?: (event?: any) => Promise<any>, public icon?: UriComponents, public count?: number) {
constructor(id: string, label: string = '', cssClass: string = '', enabled: boolean = true, actionCallback?: (event?: any) => Promise<any>, public readonly reactors?: readonly string[], public icon?: UriComponents, public count?: number) {
super(ReactionAction.ID, label, cssClass, enabled, actionCallback);
}
}

View file

@ -6,8 +6,6 @@
import { URI } from 'vs/base/common/uri';
import { IRange } from 'vs/editor/common/core/range';
import { Comment, CommentThread, CommentThreadChangedEvent, CommentThreadState } from 'vs/editor/common/languages';
import { groupBy } from 'vs/base/common/arrays';
import { localize } from 'vs/nls';
export interface ICommentThreadChangedEvent extends CommentThreadChangedEvent<IRange> {
owner: string;
@ -66,135 +64,3 @@ export class ResourceWithCommentThreads {
}
}
export class CommentsModel {
resourceCommentThreads: ResourceWithCommentThreads[];
commentThreadsMap: Map<string, { resourceWithCommentThreads: ResourceWithCommentThreads[]; ownerLabel?: string }>;
constructor() {
this.resourceCommentThreads = [];
this.commentThreadsMap = new Map<string, { resourceWithCommentThreads: ResourceWithCommentThreads[]; ownerLabel: string }>();
}
private updateResourceCommentThreads() {
const includeLabel = this.commentThreadsMap.size > 1;
this.resourceCommentThreads = [...this.commentThreadsMap.values()].map(value => {
return value.resourceWithCommentThreads.map(resource => {
resource.ownerLabel = includeLabel ? value.ownerLabel : undefined;
return resource;
}).flat();
}).flat();
this.resourceCommentThreads.sort((a, b) => {
return a.resource.toString() > b.resource.toString() ? 1 : -1;
});
}
public setCommentThreads(owner: string, ownerLabel: string, commentThreads: CommentThread[]): void {
this.commentThreadsMap.set(owner, { ownerLabel, resourceWithCommentThreads: this.groupByResource(owner, commentThreads) });
this.updateResourceCommentThreads();
}
public deleteCommentsByOwner(owner?: string): void {
if (owner) {
const existingOwner = this.commentThreadsMap.get(owner);
this.commentThreadsMap.set(owner, { ownerLabel: existingOwner?.ownerLabel, resourceWithCommentThreads: [] });
} else {
this.commentThreadsMap.clear();
}
this.updateResourceCommentThreads();
}
public updateCommentThreads(event: ICommentThreadChangedEvent): boolean {
const { owner, ownerLabel, removed, changed, added } = event;
const threadsForOwner = this.commentThreadsMap.get(owner)?.resourceWithCommentThreads || [];
removed.forEach(thread => {
// Find resource that has the comment thread
const matchingResourceIndex = threadsForOwner.findIndex((resourceData) => resourceData.id === thread.resource);
const matchingResourceData = matchingResourceIndex >= 0 ? threadsForOwner[matchingResourceIndex] : undefined;
// Find comment node on resource that is that thread and remove it
const index = matchingResourceData?.commentThreads.findIndex((commentThread) => commentThread.threadId === thread.threadId) ?? 0;
if (index >= 0) {
matchingResourceData?.commentThreads.splice(index, 1);
}
// If the comment thread was the last thread for a resource, remove that resource from the list
if (matchingResourceData?.commentThreads.length === 0) {
threadsForOwner.splice(matchingResourceIndex, 1);
}
});
changed.forEach(thread => {
// Find resource that has the comment thread
const matchingResourceIndex = threadsForOwner.findIndex((resourceData) => resourceData.id === thread.resource);
const matchingResourceData = matchingResourceIndex >= 0 ? threadsForOwner[matchingResourceIndex] : undefined;
if (!matchingResourceData) {
return;
}
// Find comment node on resource that is that thread and replace it
const index = matchingResourceData.commentThreads.findIndex((commentThread) => commentThread.threadId === thread.threadId);
if (index >= 0) {
matchingResourceData.commentThreads[index] = ResourceWithCommentThreads.createCommentNode(owner, URI.parse(matchingResourceData.id), thread);
} else if (thread.comments && thread.comments.length) {
matchingResourceData.commentThreads.push(ResourceWithCommentThreads.createCommentNode(owner, URI.parse(matchingResourceData.id), thread));
}
});
added.forEach(thread => {
const existingResource = threadsForOwner.filter(resourceWithThreads => resourceWithThreads.resource.toString() === thread.resource);
if (existingResource.length) {
const resource = existingResource[0];
if (thread.comments && thread.comments.length) {
resource.commentThreads.push(ResourceWithCommentThreads.createCommentNode(owner, resource.resource, thread));
}
} else {
threadsForOwner.push(new ResourceWithCommentThreads(owner, URI.parse(thread.resource!), [thread]));
}
});
this.commentThreadsMap.set(owner, { ownerLabel, resourceWithCommentThreads: threadsForOwner });
this.updateResourceCommentThreads();
return removed.length > 0 || changed.length > 0 || added.length > 0;
}
public hasCommentThreads(): boolean {
return !!this.resourceCommentThreads.length;
}
public getMessage(): string {
if (!this.resourceCommentThreads.length) {
return localize('noComments', "There are no comments in this workspace yet.");
} else {
return '';
}
}
private groupByResource(owner: string, commentThreads: CommentThread[]): ResourceWithCommentThreads[] {
const resourceCommentThreads: ResourceWithCommentThreads[] = [];
const commentThreadsByResource = new Map<string, ResourceWithCommentThreads>();
for (const group of groupBy(commentThreads, CommentsModel._compareURIs)) {
commentThreadsByResource.set(group[0].resource!, new ResourceWithCommentThreads(owner, URI.parse(group[0].resource!), group));
}
commentThreadsByResource.forEach((v, i, m) => {
resourceCommentThreads.push(v);
});
return resourceCommentThreads;
}
private static _compareURIs(a: CommentThread, b: CommentThread) {
const resourceA = a.resource!.toString();
const resourceB = b.resource!.toString();
if (resourceA < resourceB) {
return -1;
} else if (resourceA > resourceB) {
return 1;
} else {
return 0;
}
}
}

View file

@ -54,6 +54,7 @@ const breakpointHelperDecoration: IModelDecorationOptions = {
description: 'breakpoint-helper-decoration',
glyphMarginClassName: ThemeIcon.asClassName(icons.debugBreakpointHint),
glyphMargin: { position: GlyphMarginLane.Right },
glyphMarginHoverMessage: new MarkdownString().appendText(nls.localize('breakpointHelper', "Click to add a breakpoint.")),
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges
};

View file

@ -591,7 +591,7 @@ configurationRegistry.registerConfiguration({
'*.jsx': '${capture}.js',
'*.tsx': '${capture}.ts',
'tsconfig.json': 'tsconfig.*.json',
'package.json': 'package-lock.json, yarn.lock, pnpm-lock.yaml',
'package.json': 'package-lock.json, yarn.lock, pnpm-lock.yaml, bun.lockb',
}
}
}

View file

@ -316,7 +316,7 @@
/* decoration styles */
.monaco-editor .inline-chat-inserted-range {
background-color: var(--vscode-diffEditor-insertedTextBackground);
background-color: var(--vscode-inlineChatDiff-inserted);
}
.monaco-editor .inline-chat-inserted-range-linehighlight {

View file

@ -42,7 +42,7 @@ import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { chatAgentLeader, chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { IChatService } from 'vs/workbench/contrib/chat/common/chatService';
import { EmptyResponse, ErrorResponse, ExpansionState, IInlineChatSessionService, ReplyResponse, Session, SessionExchange, SessionPrompt } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession';
import { EditModeStrategy, LivePreviewStrategy, LiveStrategy3, PreviewStrategy, ProgressingEditsOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatStrategies';
import { EditModeStrategy, LivePreviewStrategy, LiveStrategy, PreviewStrategy, ProgressingEditsOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatStrategies';
import { IInlineChatMessageAppender, InlineChatZoneWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget';
import { CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST, CTX_INLINE_CHAT_HAS_STASHED_SESSION, CTX_INLINE_CHAT_LAST_FEEDBACK, CTX_INLINE_CHAT_RESPONSE_TYPES, CTX_INLINE_CHAT_SUPPORT_ISSUE_REPORTING, CTX_INLINE_CHAT_USER_DID_EDIT, EditMode, IInlineChatProgressItem, IInlineChatRequest, IInlineChatResponse, INLINE_CHAT_ID, InlineChatResponseFeedbackKind, InlineChatResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
@ -364,7 +364,7 @@ export class InlineChatController implements IEditorContribution {
switch (session.editMode) {
case EditMode.Live:
this._strategy = this._instaService.createInstance(LiveStrategy3, session, this._editor, this._zone.value);
this._strategy = this._instaService.createInstance(LiveStrategy, session, this._editor, this._zone.value);
break;
case EditMode.Preview:
this._strategy = this._instaService.createInstance(PreviewStrategy, session, this._zone.value);

View file

@ -156,6 +156,7 @@ export class Session {
extension: provider.debugName,
startTime: this._startTime.toISOString(),
edits: false,
finishedByEdit: false,
rounds: '',
undos: '',
editMode

View file

@ -13,7 +13,7 @@ import { Codicon } from 'vs/base/common/codicons';
import { Emitter, Event } from 'vs/base/common/event';
import { Lazy } from 'vs/base/common/lazy';
import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { ThemeIcon } from 'vs/base/common/themables';
import { ThemeIcon, themeColorFromId } from 'vs/base/common/themables';
import { ICodeEditor, IViewZone } from 'vs/editor/browser/editorBrowser';
import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService';
import { StableEditorScrollState } from 'vs/editor/browser/stableEditorScroll';
@ -26,7 +26,7 @@ import { IDocumentDiff } from 'vs/editor/common/diff/documentDiffProvider';
import { DetailedLineRangeMapping, LineRangeMapping } from 'vs/editor/common/diff/rangeMapping';
import { IEditorDecorationsCollection } from 'vs/editor/common/editorCommon';
import { TextEdit } from 'vs/editor/common/languages';
import { ICursorStateComputer, IIdentifiedSingleEditOperation, IModelDeltaDecoration, ITextModel, TrackedRangeStickiness } from 'vs/editor/common/model';
import { ICursorStateComputer, IIdentifiedSingleEditOperation, IModelDeltaDecoration, ITextModel, OverviewRulerLane, TrackedRangeStickiness } from 'vs/editor/common/model';
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker';
import { InlineDecoration, InlineDecorationType } from 'vs/editor/common/viewModel';
@ -39,7 +39,7 @@ import { countWords, getNWords } from 'vs/workbench/contrib/chat/common/chatWord
import { InlineChatFileCreatePreviewWidget, InlineChatLivePreviewWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatLivePreviewWidget';
import { ReplyResponse, Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession';
import { InlineChatZoneWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget';
import { CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, CTX_INLINE_CHAT_DOCUMENT_CHANGED } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
import { CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, CTX_INLINE_CHAT_DOCUMENT_CHANGED, overviewRulerInlineChatDiffInserted } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
export abstract class EditModeStrategy {
@ -469,7 +469,28 @@ export function asProgressiveEdit(edit: IIdentifiedSingleEditOperation, wordsPer
// ---
export class LiveStrategy3 extends EditModeStrategy {
export class LiveStrategy extends EditModeStrategy {
private readonly _decoModifiedInteractedWith = ModelDecorationOptions.register({
description: 'inline-chat-modified-interacted-with',
stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges
});
private readonly _decoInsertedText = ModelDecorationOptions.register({
description: 'inline-modified-line',
className: 'inline-chat-inserted-range-linehighlight',
isWholeLine: true,
overviewRuler: {
position: OverviewRulerLane.Full,
color: themeColorFromId(overviewRulerInlineChatDiffInserted),
}
});
private readonly _decoInsertedTextRange = ModelDecorationOptions.register({
description: 'inline-chat-inserted-range-linehighlight',
className: 'inline-chat-inserted-range',
});
private readonly _store: DisposableStore = new DisposableStore();
private readonly _sessionStore: DisposableStore = new DisposableStore();
@ -579,7 +600,7 @@ export class LiveStrategy3 extends EditModeStrategy {
}
const listener = this._session.textModelN.onDidChangeContent(async () => {
await this._showDiff(false, false);
await this._showDiff(false, false, false);
});
try {
@ -627,9 +648,8 @@ export class LiveStrategy3 extends EditModeStrategy {
}
private readonly _decoModifiedInteractedWith = ModelDecorationOptions.register({ description: 'inline-chat-modified-interacted-with', stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges });
private async _showDiff(isFinalChanges: boolean, isAfterManualInteraction: boolean): Promise<Position | undefined> {
private async _showDiff(isFinalChanges: boolean, isAfterManualInteraction: boolean, revealWidget: boolean): Promise<Position | undefined> {
const diff = await this._computeDiff();
@ -668,24 +688,17 @@ export class LiveStrategy3 extends EditModeStrategy {
continue;
}
if (innerChanges) {
for (const { modifiedRange } of innerChanges) {
newDecorations.push({
range: modifiedRange,
options: {
description: 'inline-modified',
className: 'inline-chat-inserted-range',
}
});
newDecorations.push({
range: modifiedRange,
options: {
description: 'inline-modified',
className: 'inline-chat-inserted-range-linehighlight',
isWholeLine: true
}
});
}
// highlight modified lines
newDecorations.push({
range: modifiedRange,
options: this._decoInsertedText
});
for (const innerChange of innerChanges ?? []) {
newDecorations.push({
range: innerChange.modifiedRange,
options: this._decoInsertedTextRange
});
}
// original view zone
@ -718,7 +731,7 @@ export class LiveStrategy3 extends EditModeStrategy {
class: ThemeIcon.asClassName(Codicon.check),
run: () => {
this._modifiedRangesThatHaveBeenInteractedWith.push(id);
return this._showDiff(true, true);
return this._showDiff(true, true, true);
}
}),
toAction({
@ -732,7 +745,7 @@ export class LiveStrategy3 extends EditModeStrategy {
edits.push(EditOperation.replace(innerChange.modifiedRange, originalValue));
}
this._session.textModelN.pushEditOperations(null, edits, () => null);
return this._showDiff(true, true);
return this._showDiff(true, true, true);
}
}),
];
@ -757,7 +770,7 @@ export class LiveStrategy3 extends EditModeStrategy {
: undefined;
this._sessionStore.add(this._session.textModelN.onDidChangeContent(e => {
this._showDiff(true, true);
this._showDiff(true, true, false);
}));
const zoneLineNumber = this._zone.position!.lineNumber;
@ -774,7 +787,9 @@ export class LiveStrategy3 extends EditModeStrategy {
if (widgetData) {
this._zone.widget.setExtraButtons(widgetData.actions);
this._zone.updatePositionAndHeight(widgetData.position);
this._editor.revealPositionInCenterIfOutsideViewport(widgetData.position);
if (revealWidget) {
this._editor.revealPositionInCenterIfOutsideViewport(widgetData.position);
}
this._updateSummaryMessage(diff.changes, widgetData.index);
@ -807,7 +822,7 @@ export class LiveStrategy3 extends EditModeStrategy {
this._previewZone.value.hide();
}
return await this._showDiff(true, false);
return await this._showDiff(true, false, true);
}
protected _updateSummaryMessage(mappings: readonly LineRangeMapping[], index: number) {

View file

@ -174,7 +174,11 @@ export const inlineChatInputPlaceholderForeground = registerColor('inlineChatInp
export const inlineChatInputBackground = registerColor('inlineChatInput.background', { dark: inputBackground, light: inputBackground, hcDark: inputBackground, hcLight: inputBackground }, localize('inlineChatInput.background', "Background color of the interactive editor input"));
export const inlineChatDiffInserted = registerColor('inlineChatDiff.inserted', { dark: transparent(diffInserted, .5), light: transparent(diffInserted, .5), hcDark: transparent(diffInserted, .5), hcLight: transparent(diffInserted, .5) }, localize('inlineChatDiff.inserted', "Background color of inserted text in the interactive editor input"));
export const overviewRulerInlineChatDiffInserted = registerColor('editorOverviewRuler.inlineChatInserted', { dark: transparent(diffInserted, 0.6), light: transparent(diffInserted, 0.8), hcDark: transparent(diffInserted, 0.6), hcLight: transparent(diffInserted, 0.8) }, localize('editorOverviewRuler.inlineChatInserted', 'Overview ruler marker color for inline chat inserted content.'));
export const inlineChatDiffRemoved = registerColor('inlineChatDiff.removed', { dark: transparent(diffRemoved, .5), light: transparent(diffRemoved, .5), hcDark: transparent(diffRemoved, .5), hcLight: transparent(diffRemoved, .5) }, localize('inlineChatDiff.removed', "Background color of removed text in the interactive editor input"));
export const overviewRulerInlineChatDiffRemoved = registerColor('editorOverviewRuler.inlineChatRemoved', { dark: transparent(diffRemoved, 0.6), light: transparent(diffRemoved, 0.8), hcDark: transparent(diffRemoved, 0.6), hcLight: transparent(diffRemoved, 0.8) }, localize('editorOverviewRuler.inlineChatRemoved', 'Overview ruler marker color for inline chat removed content.'));
// settings

View file

@ -1047,7 +1047,7 @@ const overviewRulerDeletedForeground = registerColor('editorOverviewRuler.delete
class DirtyDiffDecorator extends Disposable {
static createDecoration(className: string, options: { gutter: boolean; overview: { active: boolean; color: string }; minimap: { active: boolean; color: string }; isWholeLine: boolean }): ModelDecorationOptions {
static createDecoration(className: string, tooltip: string | null, options: { gutter: boolean; overview: { active: boolean; color: string }; minimap: { active: boolean; color: string }; isWholeLine: boolean }): ModelDecorationOptions {
const decorationOptions: IModelDecorationOptions = {
description: 'dirty-diff-decoration',
isWholeLine: options.isWholeLine,
@ -1055,6 +1055,7 @@ class DirtyDiffDecorator extends Disposable {
if (options.gutter) {
decorationOptions.linesDecorationsClassName = `dirty-diff-glyph ${className}`;
decorationOptions.linesDecorationsTooltip = tooltip;
}
if (options.overview.active) {
@ -1096,31 +1097,33 @@ class DirtyDiffDecorator extends Disposable {
const overview = decorations === 'all' || decorations === 'overview';
const minimap = decorations === 'all' || decorations === 'minimap';
this.addedOptions = DirtyDiffDecorator.createDecoration('dirty-diff-added', {
const diffAdded = nls.localize('diffAdded', 'Added lines');
this.addedOptions = DirtyDiffDecorator.createDecoration('dirty-diff-added', diffAdded, {
gutter,
overview: { active: overview, color: overviewRulerAddedForeground },
minimap: { active: minimap, color: minimapGutterAddedBackground },
isWholeLine: true
});
this.addedPatternOptions = DirtyDiffDecorator.createDecoration('dirty-diff-added-pattern', {
this.addedPatternOptions = DirtyDiffDecorator.createDecoration('dirty-diff-added-pattern', diffAdded, {
gutter,
overview: { active: overview, color: overviewRulerAddedForeground },
minimap: { active: minimap, color: minimapGutterAddedBackground },
isWholeLine: true
});
this.modifiedOptions = DirtyDiffDecorator.createDecoration('dirty-diff-modified', {
const diffModified = nls.localize('diffModified', 'Changed lines');
this.modifiedOptions = DirtyDiffDecorator.createDecoration('dirty-diff-modified', diffModified, {
gutter,
overview: { active: overview, color: overviewRulerModifiedForeground },
minimap: { active: minimap, color: minimapGutterModifiedBackground },
isWholeLine: true
});
this.modifiedPatternOptions = DirtyDiffDecorator.createDecoration('dirty-diff-modified-pattern', {
this.modifiedPatternOptions = DirtyDiffDecorator.createDecoration('dirty-diff-modified-pattern', diffModified, {
gutter,
overview: { active: overview, color: overviewRulerModifiedForeground },
minimap: { active: minimap, color: minimapGutterModifiedBackground },
isWholeLine: true
});
this.deletedOptions = DirtyDiffDecorator.createDecoration('dirty-diff-deleted', {
this.deletedOptions = DirtyDiffDecorator.createDecoration('dirty-diff-deleted', nls.localize('diffDeleted', 'Removed lines'), {
gutter,
overview: { active: overview, color: overviewRulerDeletedForeground },
minimap: { active: minimap, color: minimapGutterDeletedBackground },

View file

@ -255,14 +255,14 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).regis
markdownDescription: localize('inputFontSize', "Controls the font size for the input message in pixels."),
default: 13
},
'scm.inputMaxLines': {
'scm.inputMaxLineCount': {
type: 'number',
markdownDescription: localize('inputMaxLines', "Controls the maximum number of lines that the input will auto-grow to."),
minimum: 1,
maximum: 50,
default: 10
},
'scm.inputMinLines': {
'scm.inputMinLineCount': {
type: 'number',
markdownDescription: localize('inputMinLines', "Controls the minimum number of lines that the input will auto-grow from."),
minimum: 1,

View file

@ -93,7 +93,7 @@ import { fillEditorsDragData } from 'vs/workbench/browser/dnd';
import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView';
import { CodeDataTransfers } from 'vs/platform/dnd/browser/dnd';
import { FormatOnType } from 'vs/editor/contrib/format/browser/formatActions';
import { EditorOption } from 'vs/editor/common/config/editorOptions';
import { EditorOption, EditorOptions, IRulerOption } 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';
@ -2142,7 +2142,8 @@ class SCMInputWidget {
overflowWidgetsDomNode,
formatOnType: true,
renderWhitespace: 'none',
dropIntoEditor: { enabled: true }
dropIntoEditor: { enabled: true },
...this.getInputEditorLanguageConfiguration(),
};
const codeEditorWidgetOptions: ICodeEditorWidgetOptions = {
@ -2208,7 +2209,9 @@ class SCMInputWidget {
'editor.fontFamily', // When `scm.inputFontFamily` is 'editor', we use it as an effective value
'scm.inputFontSize',
'editor.accessibilitySupport',
'editor.cursorBlinking'
'editor.cursorBlinking',
'editor.rulers',
'editor.wordWrap'
];
const onRelevantSettingChanged = Event.filter(
@ -2235,7 +2238,8 @@ class SCMInputWidget {
fontSize: fontSize,
lineHeight: lineHeight,
accessibilitySupport,
cursorBlinking
cursorBlinking,
...this.getInputEditorLanguageConfiguration()
});
this.setPlaceholderFontStyles(fontFamily, fontSize, lineHeight);
@ -2267,11 +2271,11 @@ class SCMInputWidget {
const lineHeight = this.computeLineHeight(fontSize);
const { top, bottom } = this.inputEditor.getOption(EditorOption.padding);
const inputMinLinesConfig = this.configurationService.getValue('scm.inputMinLines');
const inputMinLinesConfig = this.configurationService.getValue('scm.inputMinLineCount');
const inputMinLines = typeof inputMinLinesConfig === 'number' ? clamp(inputMinLinesConfig, 1, 50) : 1;
const editorMinHeight = inputMinLines * lineHeight + top + bottom;
const inputMaxLinesConfig = this.configurationService.getValue('scm.inputMaxLines');
const inputMaxLinesConfig = this.configurationService.getValue('scm.inputMaxLineCount');
const inputMaxLines = typeof inputMaxLinesConfig === 'number' ? clamp(inputMaxLinesConfig, 1, 50) : 10;
const editorMaxHeight = inputMaxLines * lineHeight + top + bottom;
@ -2412,6 +2416,16 @@ class SCMInputWidget {
return this.configurationService.getValue<number>('scm.inputFontSize');
}
private getInputEditorLanguageConfiguration(): { rulers: (number | IRulerOption)[]; wordWrap: 'off' | 'on' | 'wordWrapColumn' | 'bounded' } {
const rulers = this.configurationService.inspect('editor.rulers', { overrideIdentifier: 'scminput' });
const wordWrap = this.configurationService.inspect('editor.wordWrap', { overrideIdentifier: 'scminput' });
return {
rulers: rulers.overrideIdentifiers?.includes('scminput') ? EditorOptions.rulers.validate(rulers.value) : [],
wordWrap: wordWrap.overrideIdentifiers?.includes('scminput') ? EditorOptions.wordWrap.validate(wordWrap) : 'on'
};
}
private getToolbarWidth(): number {
const showInputActionButton = this.configurationService.getValue<boolean>('scm.showInputActionButton');
if (!this.toolbar || !showInputActionButton || this.toolbar?.isEmpty() === true) {
@ -2648,8 +2662,8 @@ export class SCMViewPane extends ViewPane {
e.affectsConfiguration('scm.alwaysShowRepositories') ||
e.affectsConfiguration('scm.showIncomingChanges') ||
e.affectsConfiguration('scm.showOutgoingChanges') ||
e.affectsConfiguration('scm.inputMinLines') ||
e.affectsConfiguration('scm.inputMaxLines')) {
e.affectsConfiguration('scm.inputMinLineCount') ||
e.affectsConfiguration('scm.inputMaxLineCount')) {
this._showActionButton = this.configurationService.getValue<boolean>('scm.showActionButton');
this._alwaysShowRepositories = this.configurationService.getValue<boolean>('scm.alwaysShowRepositories');
this._showIncomingChanges = this.configurationService.getValue<ShowChangesSetting>('scm.showIncomingChanges');
@ -2854,6 +2868,12 @@ export class SCMViewPane extends ViewPane {
return;
}
// Do not set focus/selection when the resource is already focused and selected
if (this.tree.getFocus().some(e => isSCMResource(e) && this.uriIdentityService.extUri.isEqual(e.sourceUri, uri)) &&
this.tree.getSelection().some(e => isSCMResource(e) && this.uriIdentityService.extUri.isEqual(e.sourceUri, uri))) {
return;
}
this.revealResourceThrottler.queue(
() => this.treeOperationSequencer.queue(
async () => {

View file

@ -960,6 +960,15 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem {
return Promise.reject(new Error(`Failed to create terminal for task ${task._label}`));
}
this._fireTaskEvent(TaskEvent.start(task, terminal.instanceId, resolver.values));
const mapKey = task.getMapKey();
this._busyTasks[mapKey] = task;
this._fireTaskEvent(TaskEvent.general(TaskEventKind.Active, task, terminal.instanceId));
const problemMatchers = await this._resolveMatchers(resolver, task.configurationProperties.problemMatchers);
const startStopProblemMatcher = new StartStopProblemCollector(problemMatchers, this._markerService, this._modelService, ProblemHandlingStrategy.Clean, this._fileService);
this._terminalStatusManager.addTerminal(task, terminal, startStopProblemMatcher);
let processStartedSignaled = false;
terminal.processReady.then(() => {
if (!processStartedSignaled) {
@ -969,13 +978,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem {
}, (_error) => {
// The process never got ready. Need to think how to handle this.
});
this._fireTaskEvent(TaskEvent.start(task, terminal.instanceId, resolver.values));
const mapKey = task.getMapKey();
this._busyTasks[mapKey] = task;
this._fireTaskEvent(TaskEvent.general(TaskEventKind.Active, task, terminal.instanceId));
const problemMatchers = await this._resolveMatchers(resolver, task.configurationProperties.problemMatchers);
const startStopProblemMatcher = new StartStopProblemCollector(problemMatchers, this._markerService, this._modelService, ProblemHandlingStrategy.Clean, this._fileService);
this._terminalStatusManager.addTerminal(task, terminal, startStopProblemMatcher);
const onData = terminal.onLineData((line) => {
startStopProblemMatcher.processLine(line);
});

View file

@ -19,6 +19,7 @@ export const allApiProposals = Object.freeze({
chatVariables: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatVariables.d.ts',
codeActionAI: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.codeActionAI.d.ts',
codiconDecoration: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.codiconDecoration.d.ts',
commentReactor: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentReactor.d.ts',
commentsDraftState: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentsDraftState.d.ts',
contribCommentEditorActionsMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentEditorActionsMenu.d.ts',
contribCommentPeekContext: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentPeekContext.d.ts',

View file

@ -208,7 +208,8 @@ export class WorkbenchThemeService implements IWorkbenchThemeService {
const initializeColorTheme = async () => {
const devThemes = this.colorThemeRegistry.findThemeByExtensionLocation(extDevLoc);
if (devThemes.length) {
return this.setColorTheme(devThemes[0].id, ConfigurationTarget.MEMORY);
const matchedColorTheme = devThemes.find(theme => theme.type === this.currentColorTheme.type);
return this.setColorTheme(matchedColorTheme ? matchedColorTheme.id : devThemes[0].id, undefined);
}
const preferredColorScheme = this.getPreferredColorScheme();

View file

@ -0,0 +1,29 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
import { Registry } from 'vs/platform/registry/common/platform';
import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
import { isLinux } from 'vs/base/common/platform';
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
configurationRegistry.registerConfiguration({
'properties': {
'window.systemColorTheme': {
'type': 'string',
'enum': ['default', 'auto', 'light', 'dark'],
'enumDescriptions': [
localize('window.systemColorTheme.default', "System color theme matches the configured OS theme."),
localize('window.systemColorTheme.auto', "Enforce a light system color theme when a light workbench color theme is configured and the same for configured dark workbench color themes."),
localize('window.systemColorTheme.light', "Enforce a light system color theme."),
localize('window.systemColorTheme.dark', "Enforce a dark system color theme."),
],
'markdownDescription': localize('window.systemColorTheme', "The system color theme applies to native UI elements such as native dialogs, menus and title bar. Even if your OS is configured in light appearance mode, you can select a dark system color theme for the window. You can also configure to automatically adjust based on the `#workbench.colorTheme#` setting."),
'default': 'default',
'included': !isLinux, // not supported on Linux (https://github.com/electron/electron/issues/28887)
'scope': ConfigurationScope.APPLICATION
}
}
});

View file

@ -134,8 +134,9 @@ import 'vs/workbench/contrib/configExporter/electron-sandbox/configurationExport
// Terminal
import 'vs/workbench/contrib/terminal/electron-sandbox/terminal.contribution';
// Themes Support
// Themes
import 'vs/workbench/contrib/themes/browser/themes.test.contribution';
import 'vs/workbench/services/themes/electron-sandbox/themes.contribution';
// User Data Sync
import 'vs/workbench/contrib/userDataSync/electron-sandbox/userDataSync.contribution';

View file

@ -2012,10 +2012,10 @@ declare module 'vscode' {
/**
* A set of file filters that are used by the dialog. Each entry is a human-readable label,
* like "TypeScript", and an array of extensions, e.g.
* like "TypeScript", and an array of extensions, for example:
* ```ts
* {
* 'Images': ['png', 'jpg']
* 'Images': ['png', 'jpg'],
* 'TypeScript': ['ts', 'tsx']
* }
* ```
@ -2047,10 +2047,10 @@ declare module 'vscode' {
/**
* A set of file filters that are used by the dialog. Each entry is a human-readable label,
* like "TypeScript", and an array of extensions, e.g.
* like "TypeScript", and an array of extensions, for example:
* ```ts
* {
* 'Images': ['png', 'jpg']
* 'Images': ['png', 'jpg'],
* 'TypeScript': ['ts', 'tsx']
* }
* ```

View file

@ -0,0 +1,13 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
declare module 'vscode' {
// @alexr00 https://github.com/microsoft/vscode/issues/201131
export interface CommentReaction {
readonly reactors?: readonly string[];
}
}