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": { "configurationDefaults": {
"[git-commit]": { "[git-commit]": {
"editor.rulers": [ "editor.rulers": [
50,
72 72
], ],
"editor.wordWrap": "off",
"workbench.editor.restoreViewState": false "workbench.editor.restoreViewState": false
}, },
"[git-rebase]": { "[git-rebase]": {

View file

@ -2543,7 +2543,7 @@ export class Repository {
return branch; 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> { async getDefaultBranch(): Promise<Branch> {

View file

@ -14,8 +14,7 @@
{ "open": "(", "close": ")" }, { "open": "(", "close": ")" },
{ "open": "\"", "close": "\"", "notIn": ["string"] }, { "open": "\"", "close": "\"", "notIn": ["string"] },
{ "open": "'", "close": "'", "notIn": ["string"] }, { "open": "'", "close": "'", "notIn": ["string"] },
{ "open": "<!--", "close": "-->", "notIn": [ "comment", "string" ]}, { "open": "<!--", "close": "-->", "notIn": [ "comment", "string" ]}
{ "open": "<![CDATA[", "close": "]]>", "notIn": [ "comment", "string" ]}
], ],
"surroundingPairs": [ "surroundingPairs": [
{ "open": "'", "close": "'" }, { "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(']')) { if (recentLabel.endsWith(']')) {
// label with workspace suffix // label with workspace suffix
const lastIndexOfSquareBracket = recentLabel.lastIndexOf(' [', recentLabel.length - 2); const lastIndexOfSquareBracket = recentLabel.lastIndexOf(' [', recentLabel.length - 2);
if (lastIndexOfSquareBracket !== -1) { if (lastIndexOfSquareBracket !== -1) {
const split = splitName(recentLabel.substring(0, lastIndexOfSquareBracket)); 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); 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); 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 * Converts HTML characters inside the string to use entities instead. Makes the string safe from
* being used e.g. in HTMLElement.innerHTML. * being used e.g. in HTMLElement.innerHTML.

View file

@ -532,3 +532,13 @@ suite('Strings', () => {
ensureNoDisposablesAreLeakedInTestSuite(); 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`. * This can end up producing multiple `LineDecorationToRender`.
*/ */
export class DecorationToRender { export class DecorationToRender {
_decorationToRenderBrand: void = undefined; public readonly _decorationToRenderBrand: void = undefined;
public startLineNumber: number;
public endLineNumber: number;
public className: string;
public readonly zIndex: number; public readonly zIndex: number;
constructor(startLineNumber: number, endLineNumber: number, className: string, zIndex: number | undefined) { constructor(
this.startLineNumber = +startLineNumber; public readonly startLineNumber: number,
this.endLineNumber = +endLineNumber; public readonly endLineNumber: number,
this.className = String(className); public readonly className: string,
public readonly tooltip: string | null,
zIndex: number | undefined,
) {
this.zIndex = zIndex ?? 0; this.zIndex = zIndex ?? 0;
} }
} }
@ -42,6 +42,7 @@ export class LineDecorationToRender {
constructor( constructor(
public readonly className: string, public readonly className: string,
public readonly zIndex: number, 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++) { 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 linesDecorationsClassName = d.options.linesDecorationsClassName;
const zIndex = d.options.zIndex; const zIndex = d.options.zIndex;
if (linesDecorationsClassName) { 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; const firstLineDecorationClassName = d.options.firstLineDecorationClassName;
if (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; return r;
@ -103,7 +103,12 @@ export class LinesDecorationsOverlay extends DedupOverlay {
const decorations = toRender[lineIndex].getDecorations(); const decorations = toRender[lineIndex].getDecorations();
let lineOutput = ''; let lineOutput = '';
for (const decoration of decorations) { 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; output[lineIndex] = lineOutput;
} }

View file

@ -64,7 +64,7 @@ export class MarginViewLineDecorationsOverlay extends DedupOverlay {
const marginClassName = d.options.marginClassName; const marginClassName = d.options.marginClassName;
const zIndex = d.options.zIndex; const zIndex = d.options.zIndex;
if (marginClassName) { 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; return r;

View file

@ -1778,6 +1778,7 @@ export interface CommentReaction {
readonly count?: number; readonly count?: number;
readonly hasReacted?: boolean; readonly hasReacted?: boolean;
readonly canEdit?: 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. * If set, the decoration will be rendered in the lines decorations with this CSS class name.
*/ */
linesDecorationsClassName?: string | null; 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. * 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 glyphMargin?: model.IModelDecorationGlyphMarginOptions | null | undefined;
readonly glyphMarginClassName: string | null; readonly glyphMarginClassName: string | null;
readonly linesDecorationsClassName: string | null; readonly linesDecorationsClassName: string | null;
readonly linesDecorationsTooltip: string | null;
readonly firstLineDecorationClassName: string | null; readonly firstLineDecorationClassName: string | null;
readonly marginClassName: string | null; readonly marginClassName: string | null;
readonly inlineClassName: 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.glyphMargin = options.glyphMarginClassName ? new ModelDecorationGlyphMarginOptions(options.glyphMargin) : null;
this.glyphMarginClassName = options.glyphMarginClassName ? cleanClassName(options.glyphMarginClassName) : null; this.glyphMarginClassName = options.glyphMarginClassName ? cleanClassName(options.glyphMarginClassName) : null;
this.linesDecorationsClassName = options.linesDecorationsClassName ? cleanClassName(options.linesDecorationsClassName) : 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.firstLineDecorationClassName = options.firstLineDecorationClassName ? cleanClassName(options.firstLineDecorationClassName) : null;
this.marginClassName = options.marginClassName ? cleanClassName(options.marginClassName) : null; this.marginClassName = options.marginClassName ? cleanClassName(options.marginClassName) : null;
this.inlineClassName = options.inlineClassName ? cleanClassName(options.inlineClassName) : 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 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 { export class FoldingDecorationProvider implements IDecorationProvider {
private static readonly COLLAPSED_VISUAL_DECORATION = ModelDecorationOptions.register({ private static readonly COLLAPSED_VISUAL_DECORATION = ModelDecorationOptions.register({
@ -31,6 +34,7 @@ export class FoldingDecorationProvider implements IDecorationProvider {
stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges,
afterContentClassName: 'inline-folded', afterContentClassName: 'inline-folded',
isWholeLine: true, isWholeLine: true,
linesDecorationsTooltip: collapsed,
firstLineDecorationClassName: ThemeIcon.asClassName(foldingCollapsedIcon), firstLineDecorationClassName: ThemeIcon.asClassName(foldingCollapsedIcon),
}); });
@ -41,6 +45,7 @@ export class FoldingDecorationProvider implements IDecorationProvider {
className: 'folded-background', className: 'folded-background',
minimap: foldedBackgroundMinimap, minimap: foldedBackgroundMinimap,
isWholeLine: true, isWholeLine: true,
linesDecorationsTooltip: collapsed,
firstLineDecorationClassName: ThemeIcon.asClassName(foldingCollapsedIcon) firstLineDecorationClassName: ThemeIcon.asClassName(foldingCollapsedIcon)
}); });
@ -49,6 +54,7 @@ export class FoldingDecorationProvider implements IDecorationProvider {
stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges,
afterContentClassName: 'inline-folded', afterContentClassName: 'inline-folded',
isWholeLine: true, isWholeLine: true,
linesDecorationsTooltip: collapsed,
firstLineDecorationClassName: ThemeIcon.asClassName(foldingManualCollapsedIcon) firstLineDecorationClassName: ThemeIcon.asClassName(foldingManualCollapsedIcon)
}); });
@ -59,6 +65,7 @@ export class FoldingDecorationProvider implements IDecorationProvider {
className: 'folded-background', className: 'folded-background',
minimap: foldedBackgroundMinimap, minimap: foldedBackgroundMinimap,
isWholeLine: true, isWholeLine: true,
linesDecorationsTooltip: collapsed,
firstLineDecorationClassName: ThemeIcon.asClassName(foldingManualCollapsedIcon) firstLineDecorationClassName: ThemeIcon.asClassName(foldingManualCollapsedIcon)
}); });
@ -66,7 +73,8 @@ export class FoldingDecorationProvider implements IDecorationProvider {
description: 'folding-no-controls-range-decoration', description: 'folding-no-controls-range-decoration',
stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges,
afterContentClassName: 'inline-folded', afterContentClassName: 'inline-folded',
isWholeLine: true isWholeLine: true,
linesDecorationsTooltip: collapsed,
}); });
private static readonly NO_CONTROLS_COLLAPSED_HIGHLIGHTED_RANGE_DECORATION = ModelDecorationOptions.register({ private static readonly NO_CONTROLS_COLLAPSED_HIGHLIGHTED_RANGE_DECORATION = ModelDecorationOptions.register({
@ -75,35 +83,40 @@ export class FoldingDecorationProvider implements IDecorationProvider {
afterContentClassName: 'inline-folded', afterContentClassName: 'inline-folded',
className: 'folded-background', className: 'folded-background',
minimap: foldedBackgroundMinimap, minimap: foldedBackgroundMinimap,
isWholeLine: true isWholeLine: true,
linesDecorationsTooltip: collapsed,
}); });
private static readonly EXPANDED_VISUAL_DECORATION = ModelDecorationOptions.register({ private static readonly EXPANDED_VISUAL_DECORATION = ModelDecorationOptions.register({
description: 'folding-expanded-visual-decoration', description: 'folding-expanded-visual-decoration',
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
isWholeLine: true, isWholeLine: true,
firstLineDecorationClassName: 'alwaysShowFoldIcons ' + ThemeIcon.asClassName(foldingExpandedIcon) firstLineDecorationClassName: 'alwaysShowFoldIcons ' + ThemeIcon.asClassName(foldingExpandedIcon),
linesDecorationsTooltip: expanded,
}); });
private static readonly EXPANDED_AUTO_HIDE_VISUAL_DECORATION = ModelDecorationOptions.register({ private static readonly EXPANDED_AUTO_HIDE_VISUAL_DECORATION = ModelDecorationOptions.register({
description: 'folding-expanded-auto-hide-visual-decoration', description: 'folding-expanded-auto-hide-visual-decoration',
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
isWholeLine: true, isWholeLine: true,
firstLineDecorationClassName: ThemeIcon.asClassName(foldingExpandedIcon) firstLineDecorationClassName: ThemeIcon.asClassName(foldingExpandedIcon),
linesDecorationsTooltip: expanded,
}); });
private static readonly MANUALLY_EXPANDED_VISUAL_DECORATION = ModelDecorationOptions.register({ private static readonly MANUALLY_EXPANDED_VISUAL_DECORATION = ModelDecorationOptions.register({
description: 'folding-manually-expanded-visual-decoration', description: 'folding-manually-expanded-visual-decoration',
stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges,
isWholeLine: true, 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({ private static readonly MANUALLY_EXPANDED_AUTO_HIDE_VISUAL_DECORATION = ModelDecorationOptions.register({
description: 'folding-manually-expanded-auto-hide-visual-decoration', description: 'folding-manually-expanded-auto-hide-visual-decoration',
stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges,
isWholeLine: true, isWholeLine: true,
firstLineDecorationClassName: ThemeIcon.asClassName(foldingManualExpandedIcon) firstLineDecorationClassName: ThemeIcon.asClassName(foldingManualExpandedIcon),
linesDecorationsTooltip: expanded,
}); });
private static readonly NO_CONTROLS_EXPANDED_RANGE_DECORATION = ModelDecorationOptions.register({ 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. * If set, the decoration will be rendered in the lines decorations with this CSS class name.
*/ */
linesDecorationsClassName?: string | null; 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. * 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 } 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}"'`; command = `^'${process.execPath}' ${extraArgs} -p '"${mark}" + JSON.stringify(process.env) + "${mark}"'`;
shellArgs = ['-i', '-l', '-c']; 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 { } else {
command = `'${process.execPath}' ${extraArgs} -p '"${mark}" + JSON.stringify(process.env) + "${mark}"'`; 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) { constructor(@IStateService private stateService: IStateService, @IConfigurationService private configurationService: IConfigurationService) {
super(); super();
// System Theme
this._register(this.configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('window.systemColorTheme')) {
this.updateSystemColorTheme();
}
}));
this.updateSystemColorTheme();
// Color Scheme changes // Color Scheme changes
nativeTheme.on('updated', () => { this._register(Event.fromNodeEventEmitter(nativeTheme, 'updated')(() => this._onDidChangeColorScheme.fire(this.getColorScheme())));
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 { 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); let background = this.stateService.getItem<string | null>(THEME_BG_STORAGE_KEY, null);
if (!background) { if (!background) {
const baseTheme = this.stateService.getItem<string>(THEME_STORAGE_KEY, 'vs-dark').split(' ')[0]; switch (this.getBaseTheme()) {
switch (baseTheme) {
case 'vs': background = DEFAULT_BG_LIGHT; break; case 'vs': background = DEFAULT_BG_LIGHT; break;
case 'hc-black': background = DEFAULT_BG_HC_BLACK; break; case 'hc-black': background = DEFAULT_BG_HC_BLACK; break;
case 'hc-light': background = DEFAULT_BG_HC_LIGHT; break; case 'hc-light': background = DEFAULT_BG_HC_LIGHT; break;
@ -102,6 +128,16 @@ export class ThemeMainService extends Disposable implements IThemeMainService {
return background; 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 { saveWindowSplash(windowId: number | undefined, splash: IPartsSplash): void {
// Update in storage // Update in storage
@ -115,6 +151,9 @@ export class ThemeMainService extends Disposable implements IThemeMainService {
if (typeof windowId === 'number') { if (typeof windowId === 'number') {
this.updateBackgroundColor(windowId, splash); this.updateBackgroundColor(windowId, splash);
} }
// Update system theme
this.updateSystemColorTheme();
} }
private updateBackgroundColor(windowId: number, splash: IPartsSplash): void { private updateBackgroundColor(windowId: number, splash: IPartsSplash): void {

View file

@ -673,6 +673,10 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo
checkProposedApiEnabled(extension, 'commentsDraftState'); checkProposedApiEnabled(extension, 'commentsDraftState');
} }
if (vscodeComment.reactions?.some(reaction => reaction.reactors !== undefined)) {
checkProposedApiEnabled(extension, 'commentReactor');
}
return { return {
mode: vscodeComment.mode, mode: vscodeComment.mode,
contextValue: vscodeComment.contextValue, contextValue: vscodeComment.contextValue,
@ -693,6 +697,7 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo
iconPath: reaction.iconPath ? extHostTypeConverter.pathOrURIToURI(reaction.iconPath) : undefined, iconPath: reaction.iconPath ? extHostTypeConverter.pathOrURIToURI(reaction.iconPath) : undefined,
count: reaction.count, count: reaction.count,
hasReacted: reaction.authorHasReacted, hasReacted: reaction.authorHasReacted,
reactors: reaction.reactors
}; };
} }
@ -701,7 +706,8 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo
label: reaction.label || '', label: reaction.label || '',
count: reaction.count || 0, count: reaction.count || 0,
iconPath: reaction.iconPath ? URI.revive(reaction.iconPath) : '', 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); this.notificationService.error(error);
} }
}, reaction.iconPath, reaction.count); }, reaction.reactors, reaction.iconPath, reaction.count);
this._reactionsActionBar?.push(action, { label: true, icon: true }); 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 { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/commentContextKeys'; import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/commentContextKeys';
import { ILogService } from 'vs/platform/log/common/log'; import { ILogService } from 'vs/platform/log/common/log';
import { CommentsModel, ICommentsModel } from 'vs/workbench/contrib/comments/browser/commentsModel';
export const ICommentService = createDecorator<ICommentService>('commentService'); export const ICommentService = createDecorator<ICommentService>('commentService');
@ -86,6 +87,7 @@ export interface ICommentService {
readonly onDidDeleteDataProvider: Event<string | undefined>; readonly onDidDeleteDataProvider: Event<string | undefined>;
readonly onDidChangeCommentingEnabled: Event<boolean>; readonly onDidChangeCommentingEnabled: Event<boolean>;
readonly isCommentingEnabled: boolean; readonly isCommentingEnabled: boolean;
readonly commentsModel: ICommentsModel;
setDocumentComments(resource: URI, commentInfos: ICommentInfo[]): void; setDocumentComments(resource: URI, commentInfos: ICommentInfo[]): void;
setWorkspaceComments(owner: string, commentsByResource: CommentThread<IRange | ICellRange>[]): void; setWorkspaceComments(owner: string, commentsByResource: CommentThread<IRange | ICellRange>[]): void;
removeWorkspaceComments(owner: string): 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 _continueOnComments = new Map<string, PendingCommentThread[]>(); // owner -> PendingCommentThread[]
private _continueOnCommentProviders = new Set<IContinueOnCommentProvider>(); private _continueOnCommentProviders = new Set<IContinueOnCommentProvider>();
private readonly _commentsModel: CommentsModel = this._register(new CommentsModel());
public readonly commentsModel: ICommentsModel = this._commentsModel;
constructor( constructor(
@IInstantiationService protected readonly instantiationService: IInstantiationService, @IInstantiationService protected readonly instantiationService: IInstantiationService,
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
@ -200,7 +205,7 @@ export class CommentService extends Disposable implements ICommentService {
removed: [], removed: [],
changed: [] changed: []
}; };
this._onDidUpdateCommentThreads.fire(evt); this.updateModelThreads(evt);
} }
})); }));
this._register(storageService.onWillSaveState(() => { this._register(storageService.onWillSaveState(() => {
@ -272,20 +277,31 @@ export class CommentService extends Disposable implements ICommentService {
this._onDidSetResourceCommentInfos.fire({ resource, commentInfos }); 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 { setWorkspaceComments(owner: string, commentsByResource: CommentThread[]): void {
if (commentsByResource.length) { if (commentsByResource.length) {
this._workspaceHasCommenting.set(true); this._workspaceHasCommenting.set(true);
} }
const control = this._commentControls.get(owner); const control = this._commentControls.get(owner);
if (control) { if (control) {
this._onDidSetAllCommentThreads.fire({ ownerId: owner, ownerLabel: control.label, commentThreads: commentsByResource }); this.setModelThreads(owner, control.label, commentsByResource);
} }
} }
removeWorkspaceComments(owner: string): void { removeWorkspaceComments(owner: string): void {
const control = this._commentControls.get(owner); const control = this._commentControls.get(owner);
if (control) { 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 { } else {
this._commentControls.clear(); this._commentControls.clear();
} }
this._commentsModel.deleteCommentsByOwner(owner);
this._onDidDeleteDataProvider.fire(owner); this._onDidDeleteDataProvider.fire(owner);
} }
@ -346,7 +363,7 @@ export class CommentService extends Disposable implements ICommentService {
const control = this._commentControls.get(ownerId); const control = this._commentControls.get(ownerId);
if (control) { if (control) {
const evt: ICommentThreadChangedEvent = Object.assign({}, event, { owner: ownerId, ownerLabel: control.label }); 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 { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { Registry } from 'vs/platform/registry/common/platform'; import { Registry } from 'vs/platform/registry/common/platform';
import 'vs/workbench/contrib/comments/browser/commentsEditorContribution'; 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 { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry';
import { ctxCommentEditorFocused } from 'vs/workbench/contrib/comments/browser/simpleCommentEditor'; import { ctxCommentEditorFocused } from 'vs/workbench/contrib/comments/browser/simpleCommentEditor';
import * as strings from 'vs/base/common/strings'; 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 { AccessibilityHelpAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; 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 { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/commentContextKeys'; import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/commentContextKeys';
import { CommentCommandId } from 'vs/workbench/contrib/comments/common/commentCommandIds'; import { CommentCommandId } from 'vs/workbench/contrib/comments/common/commentCommandIds';
import { ToggleTabFocusModeAction } from 'vs/editor/contrib/toggleTabFocusMode/browser/toggleTabFocusMode'; import { ToggleTabFocusModeAction } from 'vs/editor/contrib/toggleTabFocusMode/browser/toggleTabFocusMode';
import { getActiveElement } from 'vs/base/browser/dom'; 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({ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).registerConfiguration({
id: 'comments', id: 'comments',
@ -68,7 +73,6 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).regis
registerSingleton(ICommentService, CommentService, InstantiationType.Delayed); registerSingleton(ICommentService, CommentService, InstantiationType.Delayed);
export namespace CommentAccessibilityHelpNLS { export namespace CommentAccessibilityHelpNLS {
export const intro = nls.localize('intro', "The editor contains commentable range(s). Some useful commands include:"); 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})."); 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(); 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 { IDisposable, DisposableStore } from 'vs/base/common/lifecycle';
import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; 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 { ITreeFilter, ITreeNode, TreeFilterResult, TreeVisibility } from 'vs/base/browser/ui/tree/tree';
import { IListVirtualDelegate, IListRenderer } from 'vs/base/browser/ui/list/list'; import { IListVirtualDelegate, IListRenderer } from 'vs/base/browser/ui/list/list';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; 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 { IStyleOverride } from 'vs/platform/theme/browser/defaultStyles';
import { IListStyles } from 'vs/base/browser/ui/list/listWidget'; import { IListStyles } from 'vs/base/browser/ui/list/listWidget';
import { ILocalizedString } from 'vs/platform/action/common/action'; 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_ID = 'workbench.panel.comments';
export const COMMENTS_VIEW_STORAGE_ID = '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 { isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IThemeService } from 'vs/platform/theme/common/themeService'; 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 { IWorkspaceCommentThreadsEvent, ICommentService } from 'vs/workbench/contrib/comments/browser/commentService';
import { IEditorService, ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IEditorService, ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService';
import { CommandsRegistry } from 'vs/platform/commands/common/commands'; 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 { Memento, MementoObject } from 'vs/workbench/common/memento';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { FilterOptions } from 'vs/workbench/contrib/comments/browser/commentsFilterOptions'; 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 { CommentThreadState } from 'vs/editor/common/languages';
import { IDisposable, MutableDisposable } from 'vs/base/common/lifecycle';
import { ITreeElement } from 'vs/base/browser/ui/tree/tree'; import { ITreeElement } from 'vs/base/browser/ui/tree/tree';
import { Iterable } from 'vs/base/common/iterator'; import { Iterable } from 'vs/base/common/iterator';
import { CommentController } from 'vs/workbench/contrib/comments/browser/commentsController'; import { CommentController } from 'vs/workbench/contrib/comments/browser/commentsController';
import { Range } from 'vs/editor/common/core/range'; import { Range } from 'vs/editor/common/core/range';
import { registerNavigableContainer } from 'vs/workbench/browser/actions/widgetNavigationCommands'; 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_HAS_COMMENTS = new RawContextKey<boolean>('commentsView.hasComments', false);
const CONTEXT_KEY_SOME_COMMENTS_EXPANDED = new RawContextKey<boolean>('commentsView.someCommentsExpanded', false); const CONTEXT_KEY_SOME_COMMENTS_EXPANDED = new RawContextKey<boolean>('commentsView.someCommentsExpanded', false);
const VIEW_STORAGE_ID = 'commentsViewState'; 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 => { return Iterable.map(model.resourceCommentThreads, m => {
const CommentNodeIt = Iterable.from(m.commentThreads); const CommentNodeIt = Iterable.from(m.commentThreads);
const children = Iterable.map(CommentNodeIt, r => ({ element: r })); const children = Iterable.map(CommentNodeIt, r => ({ element: r }));
@ -62,14 +61,11 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView {
private tree: CommentsList | undefined; private tree: CommentsList | undefined;
private treeContainer!: HTMLElement; private treeContainer!: HTMLElement;
private messageBoxContainer!: HTMLElement; private messageBoxContainer!: HTMLElement;
private commentsModel!: CommentsModel;
private totalComments: number = 0; private totalComments: number = 0;
private totalUnresolved = 0;
private readonly hasCommentsContextKey: IContextKey<boolean>; private readonly hasCommentsContextKey: IContextKey<boolean>;
private readonly someCommentsExpandedContextKey: IContextKey<boolean>; private readonly someCommentsExpandedContextKey: IContextKey<boolean>;
private readonly filter: Filter; private readonly filter: Filter;
readonly filters: CommentsFilters; readonly filters: CommentsFilters;
private readonly activity = this._register(new MutableDisposable<IDisposable>());
private currentHeight = 0; private currentHeight = 0;
private currentWidth = 0; private currentWidth = 0;
@ -93,7 +89,6 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView {
@ICommentService private readonly commentService: ICommentService, @ICommentService private readonly commentService: ICommentService,
@ITelemetryService telemetryService: ITelemetryService, @ITelemetryService telemetryService: ITelemetryService,
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
@IActivityService private readonly activityService: IActivityService,
@IStorageService storageService: IStorageService @IStorageService storageService: IStorageService
) { ) {
const stateMemento = new Memento(VIEW_STORAGE_ID, storageService); 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())); 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 { override saveState(): void {
this.viewState['filter'] = this.filterWidget.getFilterText(); this.viewState['filter'] = this.filterWidget.getFilterText();
this.viewState['filterHistory'] = this.filterWidget.getHistory(); 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 = dom.append(domContainer, dom.$('.tree-container'));
this.treeContainer.classList.add('file-icon-themable-tree', 'show-file-icons'); this.treeContainer.classList.add('file-icon-themable-tree', 'show-file-icons');
this.commentsModel = new CommentsModel();
this.cachedFilterStats = undefined; this.cachedFilterStats = undefined;
this.createTree(); this.createTree();
@ -232,7 +216,7 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView {
return; return;
} }
if (!this.commentsModel.hasCommentThreads() && this.messageBoxContainer) { if (!this.commentService.commentsModel.hasCommentThreads() && this.messageBoxContainer) {
this.messageBoxContainer.focus(); this.messageBoxContainer.focus();
} else if (this.tree) { } else if (this.tree) {
this.tree.domFocus(); this.tree.domFocus();
@ -267,9 +251,9 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView {
} }
private async renderComments(): Promise<void> { private async renderComments(): Promise<void> {
this.treeContainer.classList.toggle('hidden', !this.commentsModel.hasCommentThreads()); this.treeContainer.classList.toggle('hidden', !this.commentService.commentsModel.hasCommentThreads());
this.renderMessage(); this.renderMessage();
await this.tree?.setChildren(null, createResourceCommentsIterator(this.commentsModel)); await this.tree?.setChildren(null, createResourceCommentsIterator(this.commentService.commentsModel));
} }
public collapseAll() { public collapseAll() {
@ -311,8 +295,8 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView {
} }
private renderMessage(): void { private renderMessage(): void {
this.messageBoxContainer.textContent = this.commentsModel.getMessage(); this.messageBoxContainer.textContent = this.commentService.commentsModel.getMessage();
this.messageBoxContainer.classList.toggle('hidden', this.commentsModel.hasCommentThreads()); this.messageBoxContainer.classList.toggle('hidden', this.commentService.commentsModel.hasCommentThreads());
} }
private createTree(): void { private createTree(): void {
@ -437,15 +421,15 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView {
return; return;
} }
if (this.isVisible()) { 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.cachedFilterStats = undefined;
this.renderMessage(); 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()) { if (this.tree.getSelection().length === 0 && this.commentService.commentsModel.hasCommentThreads()) {
const firstComment = this.commentsModel.resourceCommentThreads[0].commentThreads[0]; const firstComment = this.commentService.commentsModel.resourceCommentThreads[0].commentThreads[0];
if (firstComment) { if (firstComment) {
this.tree.setFocus([firstComment]); this.tree.setFocus([firstComment]);
this.tree.setSelection([firstComment]); this.tree.setSelection([firstComment]);
@ -456,7 +440,6 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView {
private onAllCommentsChanged(e: IWorkspaceCommentThreadsEvent): void { private onAllCommentsChanged(e: IWorkspaceCommentThreadsEvent): void {
this.cachedFilterStats = undefined; this.cachedFilterStats = undefined;
this.commentsModel.setCommentThreads(e.ownerId, e.ownerLabel, e.commentThreads);
this.totalComments += e.commentThreads.length; this.totalComments += e.commentThreads.length;
let unresolved = 0; let unresolved = 0;
@ -465,37 +448,28 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView {
unresolved++; unresolved++;
} }
} }
this.updateBadge(unresolved);
this.refresh(); this.refresh();
} }
private onCommentsUpdated(e: ICommentThreadChangedEvent): void { private onCommentsUpdated(e: ICommentThreadChangedEvent): void {
this.cachedFilterStats = undefined; this.cachedFilterStats = undefined;
const didUpdate = this.commentsModel.updateCommentThreads(e);
this.totalComments += e.added.length; this.totalComments += e.added.length;
this.totalComments -= e.removed.length; this.totalComments -= e.removed.length;
let unresolved = 0; let unresolved = 0;
for (const resource of this.commentsModel.resourceCommentThreads) { for (const resource of this.commentService.commentsModel.resourceCommentThreads) {
for (const thread of resource.commentThreads) { for (const thread of resource.commentThreads) {
if (thread.threadState === CommentThreadState.Unresolved) { if (thread.threadState === CommentThreadState.Unresolved) {
unresolved++; unresolved++;
} }
} }
} }
this.updateBadge(unresolved); this.refresh();
if (didUpdate) {
this.refresh();
}
} }
private onDataProviderDeleted(owner: string | undefined): void { private onDataProviderDeleted(owner: string | undefined): void {
this.cachedFilterStats = undefined; this.cachedFilterStats = undefined;
this.commentsModel.deleteCommentsByOwner(owner);
this.totalComments = 0; this.totalComments = 0;
this.refresh(); 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.', '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.'] '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); }, "{0}{1} reaction", toggleMessage, action.label);
} else if (action.count === 1) { } else if (action.reactors === undefined || action.reactors.length === 0) {
return nls.localize({ if (action.count === 1) {
key: 'comment.reactionLabelOne', comment: [ return nls.localize({
'This is a tooltip for an emoji that is a "reaction" to a comment where the count of the reactions is 1.', key: 'comment.reactionLabelOne', comment: [
'The emoji is also a button so that the current user can also toggle their own emoji reaction.', 'This is a tooltip for an emoji that is a "reaction" to a comment where the count of the reactions is 1.',
'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.'] 'The emoji is also a button so that the current user can also toggle their own emoji reaction.',
}, "{0}1 reaction with {1}", toggleMessage, action.label); '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.']
} else if (action.count > 1) { }, "{0}1 reaction with {1}", toggleMessage, action.label);
return nls.localize({ } else if (action.count > 1) {
key: 'comment.reactionLabelMany', comment: [ return nls.localize({
'This is a tooltip for an emoji that is a "reaction" to a comment where the count of the reactions is greater than 1.', key: 'comment.reactionLabelMany', comment: [
'The emoji is also a button so that the current user can also toggle their own emoji reaction.', '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 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.'] 'The emoji is also a button so that the current user can also toggle their own emoji reaction.',
}, "{0}{1} reactions with {2}", toggleMessage, action.count, action.label); '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; return undefined;
} }
} }
export class ReactionAction extends Action { export class ReactionAction extends Action {
static readonly ID = 'toolbar.toggle.reaction'; 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); super(ReactionAction.ID, label, cssClass, enabled, actionCallback);
} }
} }

View file

@ -6,8 +6,6 @@
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import { IRange } from 'vs/editor/common/core/range'; import { IRange } from 'vs/editor/common/core/range';
import { Comment, CommentThread, CommentThreadChangedEvent, CommentThreadState } from 'vs/editor/common/languages'; 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> { export interface ICommentThreadChangedEvent extends CommentThreadChangedEvent<IRange> {
owner: string; 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', description: 'breakpoint-helper-decoration',
glyphMarginClassName: ThemeIcon.asClassName(icons.debugBreakpointHint), glyphMarginClassName: ThemeIcon.asClassName(icons.debugBreakpointHint),
glyphMargin: { position: GlyphMarginLane.Right }, glyphMargin: { position: GlyphMarginLane.Right },
glyphMarginHoverMessage: new MarkdownString().appendText(nls.localize('breakpointHelper', "Click to add a breakpoint.")),
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges
}; };

View file

@ -591,7 +591,7 @@ configurationRegistry.registerConfiguration({
'*.jsx': '${capture}.js', '*.jsx': '${capture}.js',
'*.tsx': '${capture}.ts', '*.tsx': '${capture}.ts',
'tsconfig.json': 'tsconfig.*.json', '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 */ /* decoration styles */
.monaco-editor .inline-chat-inserted-range { .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 { .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 { chatAgentLeader, chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; 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 { 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 { 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'; 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) { switch (session.editMode) {
case EditMode.Live: 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; break;
case EditMode.Preview: case EditMode.Preview:
this._strategy = this._instaService.createInstance(PreviewStrategy, session, this._zone.value); this._strategy = this._instaService.createInstance(PreviewStrategy, session, this._zone.value);

View file

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

View file

@ -13,7 +13,7 @@ import { Codicon } from 'vs/base/common/codicons';
import { Emitter, Event } from 'vs/base/common/event'; import { Emitter, Event } from 'vs/base/common/event';
import { Lazy } from 'vs/base/common/lazy'; import { Lazy } from 'vs/base/common/lazy';
import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; 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 { ICodeEditor, IViewZone } from 'vs/editor/browser/editorBrowser';
import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService';
import { StableEditorScrollState } from 'vs/editor/browser/stableEditorScroll'; 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 { DetailedLineRangeMapping, LineRangeMapping } from 'vs/editor/common/diff/rangeMapping';
import { IEditorDecorationsCollection } from 'vs/editor/common/editorCommon'; import { IEditorDecorationsCollection } from 'vs/editor/common/editorCommon';
import { TextEdit } from 'vs/editor/common/languages'; 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 { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker';
import { InlineDecoration, InlineDecorationType } from 'vs/editor/common/viewModel'; 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 { InlineChatFileCreatePreviewWidget, InlineChatLivePreviewWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatLivePreviewWidget';
import { ReplyResponse, Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; import { ReplyResponse, Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession';
import { InlineChatZoneWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; 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 { 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 _store: DisposableStore = new DisposableStore();
private readonly _sessionStore: 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 () => { const listener = this._session.textModelN.onDidChangeContent(async () => {
await this._showDiff(false, false); await this._showDiff(false, false, false);
}); });
try { 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(); const diff = await this._computeDiff();
@ -668,24 +688,17 @@ export class LiveStrategy3 extends EditModeStrategy {
continue; continue;
} }
if (innerChanges) { // highlight modified lines
for (const { modifiedRange } of innerChanges) { newDecorations.push({
newDecorations.push({ range: modifiedRange,
range: modifiedRange, options: this._decoInsertedText
options: { });
description: 'inline-modified',
className: 'inline-chat-inserted-range', for (const innerChange of innerChanges ?? []) {
} newDecorations.push({
}); range: innerChange.modifiedRange,
newDecorations.push({ options: this._decoInsertedTextRange
range: modifiedRange, });
options: {
description: 'inline-modified',
className: 'inline-chat-inserted-range-linehighlight',
isWholeLine: true
}
});
}
} }
// original view zone // original view zone
@ -718,7 +731,7 @@ export class LiveStrategy3 extends EditModeStrategy {
class: ThemeIcon.asClassName(Codicon.check), class: ThemeIcon.asClassName(Codicon.check),
run: () => { run: () => {
this._modifiedRangesThatHaveBeenInteractedWith.push(id); this._modifiedRangesThatHaveBeenInteractedWith.push(id);
return this._showDiff(true, true); return this._showDiff(true, true, true);
} }
}), }),
toAction({ toAction({
@ -732,7 +745,7 @@ export class LiveStrategy3 extends EditModeStrategy {
edits.push(EditOperation.replace(innerChange.modifiedRange, originalValue)); edits.push(EditOperation.replace(innerChange.modifiedRange, originalValue));
} }
this._session.textModelN.pushEditOperations(null, edits, () => null); 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; : undefined;
this._sessionStore.add(this._session.textModelN.onDidChangeContent(e => { this._sessionStore.add(this._session.textModelN.onDidChangeContent(e => {
this._showDiff(true, true); this._showDiff(true, true, false);
})); }));
const zoneLineNumber = this._zone.position!.lineNumber; const zoneLineNumber = this._zone.position!.lineNumber;
@ -774,7 +787,9 @@ export class LiveStrategy3 extends EditModeStrategy {
if (widgetData) { if (widgetData) {
this._zone.widget.setExtraButtons(widgetData.actions); this._zone.widget.setExtraButtons(widgetData.actions);
this._zone.updatePositionAndHeight(widgetData.position); this._zone.updatePositionAndHeight(widgetData.position);
this._editor.revealPositionInCenterIfOutsideViewport(widgetData.position); if (revealWidget) {
this._editor.revealPositionInCenterIfOutsideViewport(widgetData.position);
}
this._updateSummaryMessage(diff.changes, widgetData.index); this._updateSummaryMessage(diff.changes, widgetData.index);
@ -807,7 +822,7 @@ export class LiveStrategy3 extends EditModeStrategy {
this._previewZone.value.hide(); this._previewZone.value.hide();
} }
return await this._showDiff(true, false); return await this._showDiff(true, false, true);
} }
protected _updateSummaryMessage(mappings: readonly LineRangeMapping[], index: number) { 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 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 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 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 // settings

View file

@ -1047,7 +1047,7 @@ const overviewRulerDeletedForeground = registerColor('editorOverviewRuler.delete
class DirtyDiffDecorator extends Disposable { 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 = { const decorationOptions: IModelDecorationOptions = {
description: 'dirty-diff-decoration', description: 'dirty-diff-decoration',
isWholeLine: options.isWholeLine, isWholeLine: options.isWholeLine,
@ -1055,6 +1055,7 @@ class DirtyDiffDecorator extends Disposable {
if (options.gutter) { if (options.gutter) {
decorationOptions.linesDecorationsClassName = `dirty-diff-glyph ${className}`; decorationOptions.linesDecorationsClassName = `dirty-diff-glyph ${className}`;
decorationOptions.linesDecorationsTooltip = tooltip;
} }
if (options.overview.active) { if (options.overview.active) {
@ -1096,31 +1097,33 @@ class DirtyDiffDecorator extends Disposable {
const overview = decorations === 'all' || decorations === 'overview'; const overview = decorations === 'all' || decorations === 'overview';
const minimap = decorations === 'all' || decorations === 'minimap'; 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, gutter,
overview: { active: overview, color: overviewRulerAddedForeground }, overview: { active: overview, color: overviewRulerAddedForeground },
minimap: { active: minimap, color: minimapGutterAddedBackground }, minimap: { active: minimap, color: minimapGutterAddedBackground },
isWholeLine: true isWholeLine: true
}); });
this.addedPatternOptions = DirtyDiffDecorator.createDecoration('dirty-diff-added-pattern', { this.addedPatternOptions = DirtyDiffDecorator.createDecoration('dirty-diff-added-pattern', diffAdded, {
gutter, gutter,
overview: { active: overview, color: overviewRulerAddedForeground }, overview: { active: overview, color: overviewRulerAddedForeground },
minimap: { active: minimap, color: minimapGutterAddedBackground }, minimap: { active: minimap, color: minimapGutterAddedBackground },
isWholeLine: true 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, gutter,
overview: { active: overview, color: overviewRulerModifiedForeground }, overview: { active: overview, color: overviewRulerModifiedForeground },
minimap: { active: minimap, color: minimapGutterModifiedBackground }, minimap: { active: minimap, color: minimapGutterModifiedBackground },
isWholeLine: true isWholeLine: true
}); });
this.modifiedPatternOptions = DirtyDiffDecorator.createDecoration('dirty-diff-modified-pattern', { this.modifiedPatternOptions = DirtyDiffDecorator.createDecoration('dirty-diff-modified-pattern', diffModified, {
gutter, gutter,
overview: { active: overview, color: overviewRulerModifiedForeground }, overview: { active: overview, color: overviewRulerModifiedForeground },
minimap: { active: minimap, color: minimapGutterModifiedBackground }, minimap: { active: minimap, color: minimapGutterModifiedBackground },
isWholeLine: true isWholeLine: true
}); });
this.deletedOptions = DirtyDiffDecorator.createDecoration('dirty-diff-deleted', { this.deletedOptions = DirtyDiffDecorator.createDecoration('dirty-diff-deleted', nls.localize('diffDeleted', 'Removed lines'), {
gutter, gutter,
overview: { active: overview, color: overviewRulerDeletedForeground }, overview: { active: overview, color: overviewRulerDeletedForeground },
minimap: { active: minimap, color: minimapGutterDeletedBackground }, 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."), markdownDescription: localize('inputFontSize', "Controls the font size for the input message in pixels."),
default: 13 default: 13
}, },
'scm.inputMaxLines': { 'scm.inputMaxLineCount': {
type: 'number', type: 'number',
markdownDescription: localize('inputMaxLines', "Controls the maximum number of lines that the input will auto-grow to."), markdownDescription: localize('inputMaxLines', "Controls the maximum number of lines that the input will auto-grow to."),
minimum: 1, minimum: 1,
maximum: 50, maximum: 50,
default: 10 default: 10
}, },
'scm.inputMinLines': { 'scm.inputMinLineCount': {
type: 'number', type: 'number',
markdownDescription: localize('inputMinLines', "Controls the minimum number of lines that the input will auto-grow from."), markdownDescription: localize('inputMinLines', "Controls the minimum number of lines that the input will auto-grow from."),
minimum: 1, 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 { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView';
import { CodeDataTransfers } from 'vs/platform/dnd/browser/dnd'; import { CodeDataTransfers } from 'vs/platform/dnd/browser/dnd';
import { FormatOnType } from 'vs/editor/contrib/format/browser/formatActions'; 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 { IAsyncDataTreeViewState, ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree';
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
import { EditOperation } from 'vs/editor/common/core/editOperation'; import { EditOperation } from 'vs/editor/common/core/editOperation';
@ -2142,7 +2142,8 @@ class SCMInputWidget {
overflowWidgetsDomNode, overflowWidgetsDomNode,
formatOnType: true, formatOnType: true,
renderWhitespace: 'none', renderWhitespace: 'none',
dropIntoEditor: { enabled: true } dropIntoEditor: { enabled: true },
...this.getInputEditorLanguageConfiguration(),
}; };
const codeEditorWidgetOptions: ICodeEditorWidgetOptions = { const codeEditorWidgetOptions: ICodeEditorWidgetOptions = {
@ -2208,7 +2209,9 @@ class SCMInputWidget {
'editor.fontFamily', // When `scm.inputFontFamily` is 'editor', we use it as an effective value 'editor.fontFamily', // When `scm.inputFontFamily` is 'editor', we use it as an effective value
'scm.inputFontSize', 'scm.inputFontSize',
'editor.accessibilitySupport', 'editor.accessibilitySupport',
'editor.cursorBlinking' 'editor.cursorBlinking',
'editor.rulers',
'editor.wordWrap'
]; ];
const onRelevantSettingChanged = Event.filter( const onRelevantSettingChanged = Event.filter(
@ -2235,7 +2238,8 @@ class SCMInputWidget {
fontSize: fontSize, fontSize: fontSize,
lineHeight: lineHeight, lineHeight: lineHeight,
accessibilitySupport, accessibilitySupport,
cursorBlinking cursorBlinking,
...this.getInputEditorLanguageConfiguration()
}); });
this.setPlaceholderFontStyles(fontFamily, fontSize, lineHeight); this.setPlaceholderFontStyles(fontFamily, fontSize, lineHeight);
@ -2267,11 +2271,11 @@ class SCMInputWidget {
const lineHeight = this.computeLineHeight(fontSize); const lineHeight = this.computeLineHeight(fontSize);
const { top, bottom } = this.inputEditor.getOption(EditorOption.padding); 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 inputMinLines = typeof inputMinLinesConfig === 'number' ? clamp(inputMinLinesConfig, 1, 50) : 1;
const editorMinHeight = inputMinLines * lineHeight + top + bottom; 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 inputMaxLines = typeof inputMaxLinesConfig === 'number' ? clamp(inputMaxLinesConfig, 1, 50) : 10;
const editorMaxHeight = inputMaxLines * lineHeight + top + bottom; const editorMaxHeight = inputMaxLines * lineHeight + top + bottom;
@ -2412,6 +2416,16 @@ class SCMInputWidget {
return this.configurationService.getValue<number>('scm.inputFontSize'); 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 { private getToolbarWidth(): number {
const showInputActionButton = this.configurationService.getValue<boolean>('scm.showInputActionButton'); const showInputActionButton = this.configurationService.getValue<boolean>('scm.showInputActionButton');
if (!this.toolbar || !showInputActionButton || this.toolbar?.isEmpty() === true) { if (!this.toolbar || !showInputActionButton || this.toolbar?.isEmpty() === true) {
@ -2648,8 +2662,8 @@ export class SCMViewPane extends ViewPane {
e.affectsConfiguration('scm.alwaysShowRepositories') || e.affectsConfiguration('scm.alwaysShowRepositories') ||
e.affectsConfiguration('scm.showIncomingChanges') || e.affectsConfiguration('scm.showIncomingChanges') ||
e.affectsConfiguration('scm.showOutgoingChanges') || e.affectsConfiguration('scm.showOutgoingChanges') ||
e.affectsConfiguration('scm.inputMinLines') || e.affectsConfiguration('scm.inputMinLineCount') ||
e.affectsConfiguration('scm.inputMaxLines')) { e.affectsConfiguration('scm.inputMaxLineCount')) {
this._showActionButton = this.configurationService.getValue<boolean>('scm.showActionButton'); this._showActionButton = this.configurationService.getValue<boolean>('scm.showActionButton');
this._alwaysShowRepositories = this.configurationService.getValue<boolean>('scm.alwaysShowRepositories'); this._alwaysShowRepositories = this.configurationService.getValue<boolean>('scm.alwaysShowRepositories');
this._showIncomingChanges = this.configurationService.getValue<ShowChangesSetting>('scm.showIncomingChanges'); this._showIncomingChanges = this.configurationService.getValue<ShowChangesSetting>('scm.showIncomingChanges');
@ -2854,6 +2868,12 @@ export class SCMViewPane extends ViewPane {
return; 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.revealResourceThrottler.queue(
() => this.treeOperationSequencer.queue( () => this.treeOperationSequencer.queue(
async () => { 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}`)); 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; let processStartedSignaled = false;
terminal.processReady.then(() => { terminal.processReady.then(() => {
if (!processStartedSignaled) { if (!processStartedSignaled) {
@ -969,13 +978,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem {
}, (_error) => { }, (_error) => {
// The process never got ready. Need to think how to handle this. // 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) => { const onData = terminal.onLineData((line) => {
startStopProblemMatcher.processLine(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', 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', 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', 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', 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', 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', 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 initializeColorTheme = async () => {
const devThemes = this.colorThemeRegistry.findThemeByExtensionLocation(extDevLoc); const devThemes = this.colorThemeRegistry.findThemeByExtensionLocation(extDevLoc);
if (devThemes.length) { 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(); 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 // Terminal
import 'vs/workbench/contrib/terminal/electron-sandbox/terminal.contribution'; import 'vs/workbench/contrib/terminal/electron-sandbox/terminal.contribution';
// Themes Support // Themes
import 'vs/workbench/contrib/themes/browser/themes.test.contribution'; import 'vs/workbench/contrib/themes/browser/themes.test.contribution';
import 'vs/workbench/services/themes/electron-sandbox/themes.contribution';
// User Data Sync // User Data Sync
import 'vs/workbench/contrib/userDataSync/electron-sandbox/userDataSync.contribution'; 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, * 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 * ```ts
* { * {
* 'Images': ['png', 'jpg'] * 'Images': ['png', 'jpg'],
* 'TypeScript': ['ts', 'tsx'] * '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, * 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 * ```ts
* { * {
* 'Images': ['png', 'jpg'] * 'Images': ['png', 'jpg'],
* 'TypeScript': ['ts', 'tsx'] * '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[];
}
}