diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json index cbcde9bb10933..fdd03c400cc01 100644 --- a/.devcontainer/devcontainer-lock.json +++ b/.devcontainer/devcontainer-lock.json @@ -4,6 +4,11 @@ "version": "1.0.8", "resolved": "ghcr.io/devcontainers/features/desktop-lite@sha256:e7dc4d37ab9e3d6e7ebb221bac741f5bfe07dae47025399d038b17af2ed8ddb7", "integrity": "sha256:e7dc4d37ab9e3d6e7ebb221bac741f5bfe07dae47025399d038b17af2ed8ddb7" + }, + "ghcr.io/devcontainers/features/rust:1": { + "version": "1.1.3", + "resolved": "ghcr.io/devcontainers/features/rust@sha256:aba6f47303b197976902bf544c786b5efecc03c238ff593583e5e74dfa9c7ccb", + "integrity": "sha256:aba6f47303b197976902bf544c786b5efecc03c238ff593583e5e74dfa9c7ccb" } } } \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index f5adc4e1c46dd..75076a0f8b657 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -4,7 +4,8 @@ "dockerfile": "Dockerfile" }, "features": { - "ghcr.io/devcontainers/features/desktop-lite:1": {} + "ghcr.io/devcontainers/features/desktop-lite:1": {}, + "ghcr.io/devcontainers/features/rust:1": {} }, "containerEnv": { "DISPLAY": "" // Allow the Dev Containers extension to set DISPLAY, post-create.sh will add it back in ~/.bashrc and ~/.zshrc if not set. diff --git a/.github/workflows/on-open.yml b/.github/workflows/on-open.yml index 2a26794c6b01b..62cdbff683b74 100644 --- a/.github/workflows/on-open.yml +++ b/.github/workflows/on-open.yml @@ -27,8 +27,8 @@ jobs: with: appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - owner: VSCodeTriageBot - repo: testissues + destinationOwner: VSCodeTriageBot + destinationRepo: testissues - name: Run New Release if: github.event.issue.user.login != 'ghost' diff --git a/build/lib/bundle.js b/build/lib/bundle.js index a0989638f7cc7..df7c10fa37ee8 100644 --- a/build/lib/bundle.js +++ b/build/lib/bundle.js @@ -5,6 +5,7 @@ *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); exports.bundle = bundle; +exports.removeDuplicateTSBoilerplate = removeDuplicateTSBoilerplate; const fs = require("fs"); const path = require("path"); const vm = require("vm"); @@ -127,7 +128,7 @@ function emitEntryPoints(modules, entryPoints) { }); return { // TODO@TS 2.1.2 - files: extractStrings(removeDuplicateTSBoilerplate(result)), + files: extractStrings(removeAllDuplicateTSBoilerplate(result)), bundleData: bundleData }; } @@ -216,7 +217,16 @@ function extractStrings(destFiles) { }); return destFiles; } -function removeDuplicateTSBoilerplate(destFiles) { +function removeAllDuplicateTSBoilerplate(destFiles) { + destFiles.forEach((destFile) => { + const SEEN_BOILERPLATE = []; + destFile.sources.forEach((source) => { + source.contents = removeDuplicateTSBoilerplate(source.contents, SEEN_BOILERPLATE); + }); + }); + return destFiles; +} +function removeDuplicateTSBoilerplate(source, SEEN_BOILERPLATE = []) { // Taken from typescript compiler => emitFiles const BOILERPLATE = [ { start: /^var __extends/, end: /^}\)\(\);$/ }, @@ -230,45 +240,39 @@ function removeDuplicateTSBoilerplate(destFiles) { { start: /^var __setModuleDefault/, end: /^}\);$/ }, { start: /^var __importStar/, end: /^};$/ }, ]; - destFiles.forEach((destFile) => { - const SEEN_BOILERPLATE = []; - destFile.sources.forEach((source) => { - const lines = source.contents.split(/\r\n|\n|\r/); - const newLines = []; - let IS_REMOVING_BOILERPLATE = false, END_BOILERPLATE; - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - if (IS_REMOVING_BOILERPLATE) { - newLines.push(''); - if (END_BOILERPLATE.test(line)) { - IS_REMOVING_BOILERPLATE = false; - } - } - else { - for (let j = 0; j < BOILERPLATE.length; j++) { - const boilerplate = BOILERPLATE[j]; - if (boilerplate.start.test(line)) { - if (SEEN_BOILERPLATE[j]) { - IS_REMOVING_BOILERPLATE = true; - END_BOILERPLATE = boilerplate.end; - } - else { - SEEN_BOILERPLATE[j] = true; - } - } - } - if (IS_REMOVING_BOILERPLATE) { - newLines.push(''); + const lines = source.split(/\r\n|\n|\r/); + const newLines = []; + let IS_REMOVING_BOILERPLATE = false, END_BOILERPLATE; + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (IS_REMOVING_BOILERPLATE) { + newLines.push(''); + if (END_BOILERPLATE.test(line)) { + IS_REMOVING_BOILERPLATE = false; + } + } + else { + for (let j = 0; j < BOILERPLATE.length; j++) { + const boilerplate = BOILERPLATE[j]; + if (boilerplate.start.test(line)) { + if (SEEN_BOILERPLATE[j]) { + IS_REMOVING_BOILERPLATE = true; + END_BOILERPLATE = boilerplate.end; } else { - newLines.push(line); + SEEN_BOILERPLATE[j] = true; } } } - source.contents = newLines.join('\n'); - }); - }); - return destFiles; + if (IS_REMOVING_BOILERPLATE) { + newLines.push(''); + } + else { + newLines.push(line); + } + } + } + return newLines.join('\n'); } function emitEntryPoint(modulesMap, deps, entryPoint, includedModules, prepend, append, dest) { if (!dest) { diff --git a/build/lib/bundle.ts b/build/lib/bundle.ts index 692100ff51581..74d37fcce558d 100644 --- a/build/lib/bundle.ts +++ b/build/lib/bundle.ts @@ -251,7 +251,7 @@ function emitEntryPoints(modules: IBuildModuleInfo[], entryPoints: IEntryPointMa return { // TODO@TS 2.1.2 - files: extractStrings(removeDuplicateTSBoilerplate(result)), + files: extractStrings(removeAllDuplicateTSBoilerplate(result)), bundleData: bundleData }; } @@ -350,7 +350,19 @@ function extractStrings(destFiles: IConcatFile[]): IConcatFile[] { return destFiles; } -function removeDuplicateTSBoilerplate(destFiles: IConcatFile[]): IConcatFile[] { +function removeAllDuplicateTSBoilerplate(destFiles: IConcatFile[]): IConcatFile[] { + destFiles.forEach((destFile) => { + const SEEN_BOILERPLATE: boolean[] = []; + destFile.sources.forEach((source) => { + source.contents = removeDuplicateTSBoilerplate(source.contents, SEEN_BOILERPLATE); + }); + }); + + return destFiles; +} + +export function removeDuplicateTSBoilerplate(source: string, SEEN_BOILERPLATE: boolean[] = []): string { + // Taken from typescript compiler => emitFiles const BOILERPLATE = [ { start: /^var __extends/, end: /^}\)\(\);$/ }, @@ -365,44 +377,37 @@ function removeDuplicateTSBoilerplate(destFiles: IConcatFile[]): IConcatFile[] { { start: /^var __importStar/, end: /^};$/ }, ]; - destFiles.forEach((destFile) => { - const SEEN_BOILERPLATE: boolean[] = []; - destFile.sources.forEach((source) => { - const lines = source.contents.split(/\r\n|\n|\r/); - const newLines: string[] = []; - let IS_REMOVING_BOILERPLATE = false, END_BOILERPLATE: RegExp; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - if (IS_REMOVING_BOILERPLATE) { - newLines.push(''); - if (END_BOILERPLATE!.test(line)) { - IS_REMOVING_BOILERPLATE = false; - } - } else { - for (let j = 0; j < BOILERPLATE.length; j++) { - const boilerplate = BOILERPLATE[j]; - if (boilerplate.start.test(line)) { - if (SEEN_BOILERPLATE[j]) { - IS_REMOVING_BOILERPLATE = true; - END_BOILERPLATE = boilerplate.end; - } else { - SEEN_BOILERPLATE[j] = true; - } - } - } - if (IS_REMOVING_BOILERPLATE) { - newLines.push(''); + const lines = source.split(/\r\n|\n|\r/); + const newLines: string[] = []; + let IS_REMOVING_BOILERPLATE = false, END_BOILERPLATE: RegExp; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (IS_REMOVING_BOILERPLATE) { + newLines.push(''); + if (END_BOILERPLATE!.test(line)) { + IS_REMOVING_BOILERPLATE = false; + } + } else { + for (let j = 0; j < BOILERPLATE.length; j++) { + const boilerplate = BOILERPLATE[j]; + if (boilerplate.start.test(line)) { + if (SEEN_BOILERPLATE[j]) { + IS_REMOVING_BOILERPLATE = true; + END_BOILERPLATE = boilerplate.end; } else { - newLines.push(line); + SEEN_BOILERPLATE[j] = true; } } } - source.contents = newLines.join('\n'); - }); - }); - - return destFiles; + if (IS_REMOVING_BOILERPLATE) { + newLines.push(''); + } else { + newLines.push(line); + } + } + } + return newLines.join('\n'); } interface IPluginMap { diff --git a/extensions/git/package.json b/extensions/git/package.json index ccac6222db925..124648c87488f 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -34,7 +34,8 @@ "scmValidation", "tabInputMultiDiff", "tabInputTextMerge", - "timeline" + "timeline", + "quickInputButtonLocation" ], "categories": [ "Other" diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index dc391ce61db3c..a8f537950c786 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -5,7 +5,7 @@ import * as os from 'os'; import * as path from 'path'; -import { Command, commands, Disposable, LineChange, MessageOptions, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection, TextDocumentContentProvider, InputBoxValidationSeverity, TabInputText, TabInputTextMerge, QuickPickItemKind, TextDocument, LogOutputChannel, l10n, Memento, UIKind, QuickInputButton, ThemeIcon, SourceControlHistoryItem, SourceControl, InputBoxValidationMessage, Tab, TabInputNotebook } from 'vscode'; +import { Command, commands, Disposable, LineChange, MessageOptions, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection, TextDocumentContentProvider, InputBoxValidationSeverity, TabInputText, TabInputTextMerge, QuickPickItemKind, TextDocument, LogOutputChannel, l10n, Memento, UIKind, QuickInputButton, ThemeIcon, SourceControlHistoryItem, SourceControl, InputBoxValidationMessage, Tab, TabInputNotebook, QuickInputButtonLocation } from 'vscode'; import TelemetryReporter from '@vscode/extension-telemetry'; import { uniqueNamesGenerator, adjectives, animals, colors, NumberDictionary } from '@joaomoreno/unique-names-generator'; import { ForcePushMode, GitErrorCodes, Ref, RefType, Status, CommitOptions, RemoteSourcePublisher, Remote } from './api/git'; @@ -2703,6 +2703,7 @@ export class CommandCenter { { iconPath: new ThemeIcon('refresh'), tooltip: l10n.t('Regenerate Branch Name'), + location: QuickInputButtonLocation.Inline } ] : []; diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index b7ebb00e9806e..cb94816874fc9 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -83,13 +83,16 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec this.currentHistoryItemGroup = { id: `refs/heads/${this.repository.HEAD.name ?? ''}`, name: this.repository.HEAD.name ?? '', + revision: this.repository.HEAD.commit ?? '', remote: this.repository.HEAD.upstream ? { id: `refs/remotes/${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`, name: `${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`, + revision: this.repository.HEAD.upstream.commit ?? '' } : undefined, base: mergeBase ? { id: `refs/remotes/${mergeBase.remote}/${mergeBase.name}`, name: `${mergeBase.remote}/${mergeBase.name}`, + revision: mergeBase.commit ?? '' } : undefined }; @@ -132,26 +135,18 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec } async provideHistoryItems2(options: SourceControlHistoryOptions): Promise { - if (!this.currentHistoryItemGroup || !options.historyItemGroupIds) { + if (!this.currentHistoryItemGroup || !options.historyItemGroupIds || typeof options.limit === 'number' || !options.limit?.id) { return []; } // Deduplicate refNames const refNames = Array.from(new Set(options.historyItemGroupIds)); - // Get the merge base of the refNames - const refsMergeBase = await this.resolveHistoryItemGroupsMergeBase(refNames); - if (!refsMergeBase) { - return []; - } - - const historyItems: SourceControlHistoryItem[] = []; - try { // Get the common ancestor commit, and commits const [mergeBaseCommit, commits] = await Promise.all([ - this.repository.getCommit(refsMergeBase), - this.repository.log({ range: `${refsMergeBase}..`, refNames, shortStats: true }) + this.repository.getCommit(options.limit.id), + this.repository.log({ range: `${options.limit.id}..`, refNames, shortStats: true }) ]); // Add common ancestor commit @@ -161,7 +156,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec await ensureEmojis(); - historyItems.push(...commits.map(commit => { + return commits.map(commit => { const newLineIndex = commit.message.indexOf('\n'); const subject = newLineIndex !== -1 ? commit.message.substring(0, newLineIndex) : commit.message; @@ -177,12 +172,11 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec statistics: commit.shortStat ?? { files: 0, insertions: 0, deletions: 0 }, labels: labels.length !== 0 ? labels : undefined }; - })); + }); } catch (err) { - this.logger.error(`[GitHistoryProvider][provideHistoryItems2] Failed to get history items '${refsMergeBase}..': ${err}`); + this.logger.error(`[GitHistoryProvider][provideHistoryItems2] Failed to get history items '${options.limit.id}..': ${err}`); + return []; } - - return historyItems; } async provideHistoryItemSummary(historyItemId: string, historyItemParentId: string | undefined): Promise { @@ -260,17 +254,39 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec return undefined; } - provideFileDecoration(uri: Uri): FileDecoration | undefined { - return this.historyItemDecorations.get(uri.toString()); - } + async resolveHistoryItemGroupCommonAncestor2(historyItemGroupIds: string[]): Promise { + try { + if (historyItemGroupIds.length === 0) { + // TODO@lszomoru - log + return undefined; + } else if (historyItemGroupIds.length === 1 && historyItemGroupIds[0] === this.currentHistoryItemGroup?.id) { + // Remote + if (this.currentHistoryItemGroup.remote) { + const ancestor = await this.repository.getMergeBase(historyItemGroupIds[0], this.currentHistoryItemGroup.remote.id); + return ancestor; + } - private async resolveHistoryItemGroupsMergeBase(refNames: string[]): Promise { - if (refNames.length < 2) { - return undefined; + // Base + if (this.currentHistoryItemGroup.base) { + const ancestor = await this.repository.getMergeBase(historyItemGroupIds[0], this.currentHistoryItemGroup.base.id); + return ancestor; + } + + // TODO@lszomoru - Return first commit + } else if (historyItemGroupIds.length > 1) { + const ancestor = await this.repository.getMergeBase(historyItemGroupIds[0], historyItemGroupIds[1], ...historyItemGroupIds.slice(2)); + return ancestor; + } } + catch (err) { + this.logger.error(`[GitHistoryProvider][resolveHistoryItemGroupCommonAncestor2] Failed to resolve common ancestor for ${historyItemGroupIds.join(',')}: ${err}`); + } + + return undefined; + } - const refsMergeBase = await this.repository.getMergeBase(refNames[0], refNames[1], ...refNames.slice(2)); - return refsMergeBase; + provideFileDecoration(uri: Uri): FileDecoration | undefined { + return this.historyItemDecorations.get(uri.toString()); } private resolveHistoryItemLabels(commit: Commit, refNames: string[]): SourceControlHistoryItemLabel[] { diff --git a/extensions/git/tsconfig.json b/extensions/git/tsconfig.json index 6ca99fecf1b58..75fb8217df75a 100644 --- a/extensions/git/tsconfig.json +++ b/extensions/git/tsconfig.json @@ -21,6 +21,7 @@ "../../src/vscode-dts/vscode.proposed.tabInputMultiDiff.d.ts", "../../src/vscode-dts/vscode.proposed.tabInputTextMerge.d.ts", "../../src/vscode-dts/vscode.proposed.timeline.d.ts", + "../../src/vscode-dts/vscode.proposed.quickInputButtonLocation.d.ts", "../types/lib.textEncoder.d.ts" ] } diff --git a/src/cli.js b/src/cli.js index 898145f2ae31d..6d2a313332267 100644 --- a/src/cli.js +++ b/src/cli.js @@ -6,6 +6,13 @@ //@ts-check 'use strict'; +// Delete `VSCODE_CWD` very early. We have seen +// reports where `code .` would use the wrong +// current working directory due to our variable +// somehow escaping to the parent shell +// (https://github.com/microsoft/vscode/issues/126399) +delete process.env['VSCODE_CWD']; + // ESM-comment-begin const bootstrap = require('./bootstrap'); const bootstrapNode = require('./bootstrap-node'); @@ -25,13 +32,6 @@ const product = require('./bootstrap-meta').product; // const __dirname = path.dirname(fileURLToPath(import.meta.url)); // ESM-uncomment-end -// Delete `VSCODE_CWD` very early. We have seen -// reports where `code .` would use the wrong -// current working directory due to our variable -// somehow escaping to the parent shell -// (https://github.com/microsoft/vscode/issues/126399) -delete process.env['VSCODE_CWD']; - async function start() { // NLS diff --git a/src/main.js b/src/main.js index f9a93b0584466..041d9d6003754 100644 --- a/src/main.js +++ b/src/main.js @@ -15,7 +15,6 @@ const path = require('path'); const fs = require('original-fs'); const os = require('os'); -const minimist = require('minimist'); const bootstrap = require('./bootstrap'); const bootstrapNode = require('./bootstrap-node'); const bootstrapAmd = require('./bootstrap-amd'); @@ -31,12 +30,11 @@ const { app, protocol, crashReporter, Menu, contentTracing } = require('electron // import * as path from 'path'; // import * as fs from 'original-fs'; // import * as os from 'os'; -// import { fileURLToPath } from 'url'; -// import { app, protocol, crashReporter, Menu, contentTracing } from 'electron'; -// import minimist from 'minimist'; // import * as bootstrap from './bootstrap.js'; // import * as bootstrapNode from './bootstrap-node.js'; // import * as bootstrapAmd from './bootstrap-amd.js'; +// import { fileURLToPath } from 'url'; +// import { app, protocol, crashReporter, Menu, contentTracing } from 'electron'; // import { product } from './bootstrap-meta.js'; // import { parse } from './vs/base/common/jsonc.js'; // import { getUserDataPath } from './vs/platform/environment/node/userDataPath.js'; @@ -55,6 +53,13 @@ const portable = bootstrapNode.configurePortable(product); // Enable ASAR support bootstrap.enableASARSupport(); +// ESM-comment-begin +const minimist = require('minimist'); // !!! IMPORTANT: MUST come after bootstrap#enableASARSupport +// ESM-comment-end +// ESM-uncomment-begin +// import minimist from 'minimist'; // !!! IMPORTANT: MUST come after bootstrap#enableASARSupport +// ESM-uncomment-end + const args = parseCLIArgs(); // Configure static command line arguments const argvConfig = configureCommandlineSwitchesSync(args); diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 7a52f0818e4ef..a6acbe7f3a5e4 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -129,16 +129,6 @@ export function clearNode(node: HTMLElement): void { } } - -export function clearNodeRecursively(domNode: ChildNode) { - while (domNode.firstChild) { - const element = domNode.firstChild; - element.remove(); - clearNodeRecursively(element); - } -} - - class DomListener implements IDisposable { private _handler: (e: any) => void; diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index cd04dd2dcc639..5558cf28cd4b6 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -399,14 +399,6 @@ export class View extends ViewEventHandler { } super.dispose(); - - // See https://github.com/microsoft/vscode/pull/219297 - // Reduces the impact of detached dom node memory leaks - // E.g when a memory leak occurs in a child component - // of the editor (e.g. Minimap, GlyphMarginWidgets,...) - // the editor dom (linked to the child dom) should not be - // leaked as well. - dom.clearNodeRecursively(this.domNode.domNode); } private _scheduleRender(): void { diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionController.ts b/src/vs/editor/contrib/codeAction/browser/codeActionController.ts index 612fd4d292305..5ac2deed51646 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionController.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeActionController.ts @@ -157,11 +157,12 @@ export class CodeActionController extends Disposable implements IEditorContribut public hideLightBulbWidget(): void { this._lightBulbWidget.rawValue?.hide(); + this._lightBulbWidget.rawValue?.gutterHide(); } private async update(newState: CodeActionsState.State): Promise { if (newState.type !== CodeActionsState.Type.Triggered) { - this._lightBulbWidget.rawValue?.hide(); + this.hideLightBulbWidget(); return; } @@ -186,7 +187,7 @@ export class CodeActionController extends Disposable implements IEditorContribut const validActionToApply = this.tryGetValidActionToApply(newState.trigger, actions); if (validActionToApply) { try { - this._lightBulbWidget.value?.hide(); + this.hideLightBulbWidget(); await this._applyCodeAction(validActionToApply, false, false, ApplyCodeActionReason.FromCodeActions); } finally { actions.dispose(); diff --git a/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.css b/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.css index cbadb2348ef61..c51e488c0d05c 100644 --- a/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.css +++ b/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.css @@ -43,3 +43,25 @@ opacity: 0.3; z-index: 1; } + +/* gutter decoration */ +.monaco-editor .glyph-margin-widgets .cgmr { + display: block; + cursor: pointer; + opacity: 1; + transition: opacity .2s ease-in-out; +} + +.monaco-editor .glyph-margin-widgets .cgmr.codicon-gutter-lightbulb, +.monaco-editor .glyph-margin-widgets .cgmr.codicon-gutter-lightbulb-sparkle { + color: var(--vscode-editorLightBulb-foreground); +} + +.monaco-editor .glyph-margin-widgets .cgmr.codicon-gutter-lightbulb-auto-fix, +.monaco-editor .glyph-margin-widgets .cgmr.codicon-gutter-lightbulb-aifix-auto-fix { + color: var(--vscode-editorLightBulbAutoFix-foreground, var(--vscode-editorLightBulb-foreground)); +} + +.monaco-editor .glyph-margin-widgets .cgmr.codicon-gutter-lightbulb-sparkle-filled { + color: var(--vscode-editorLightBulbAi-foreground, var(--vscode-icon-foreground)); +} diff --git a/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.ts b/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.ts index 45fc9aa9cb77d..6bc8eb0c1b3ed 100644 --- a/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.ts +++ b/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.ts @@ -10,14 +10,24 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { ThemeIcon } from 'vs/base/common/themables'; import 'vs/css!./lightBulbWidget'; -import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; +import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition, IEditorMouseEvent } from 'vs/editor/browser/editorBrowser'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IPosition } from 'vs/editor/common/core/position'; +import { GlyphMarginLane, IModelDecorationsChangeAccessor, TrackedRangeStickiness } from 'vs/editor/common/model'; +import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { computeIndentLevel } from 'vs/editor/common/model/utils'; import { autoFixCommandId, quickFixCommandId } from 'vs/editor/contrib/codeAction/browser/codeAction'; import { CodeActionSet, CodeActionTrigger } from 'vs/editor/contrib/codeAction/common/types'; import * as nls from 'vs/nls'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; +import { Range } from 'vs/editor/common/core/range'; + +const GUTTER_LIGHTBULB_ICON = registerIcon('gutter-lightbulb', Codicon.lightBulb, nls.localize('gutterLightbulbWidget', 'Icon which spawns code actions menu from the gutter when there is no space in the editor.')); +const GUTTER_LIGHTBULB_AUTO_FIX_ICON = registerIcon('gutter-lightbulb-auto-fix', Codicon.lightbulbAutofix, nls.localize('gutterLightbulbAutoFixWidget', 'Icon which spawns code actions menu from the gutter when there is no space in the editor and a quick fix is available.')); +const GUTTER_LIGHTBULB_AIFIX_ICON = registerIcon('gutter-lightbulb-sparkle', Codicon.lightbulbSparkle, nls.localize('gutterLightbulbAIFixWidget', 'Icon which spawns code actions menu from the gutter when there is no space in the editor and an AI fix is available.')); +const GUTTER_LIGHTBULB_AIFIX_AUTO_FIX_ICON = registerIcon('gutter-lightbulb-aifix-auto-fix', Codicon.lightbulbSparkleAutofix, nls.localize('gutterLightbulbAIFixAutoFixWidget', 'Icon which spawns code actions menu from the gutter when there is no space in the editor and an AI fix and a quick fix is available.')); +const GUTTER_SPARKLE_FILLED_ICON = registerIcon('gutter-lightbulb-sparkle-filled', Codicon.sparkleFilled, nls.localize('gutterLightbulbSparkleFilledWidget', 'Icon which spawns code actions menu from the gutter when there is no space in the editor and an AI fix and a quick fix is available.')); namespace LightBulbState { @@ -43,6 +53,14 @@ namespace LightBulbState { } export class LightBulbWidget extends Disposable implements IContentWidget { + private _gutterDecorationID: string | undefined; + + private static readonly GUTTER_DECORATION = ModelDecorationOptions.register({ + description: 'codicon-gutter-lightbulb-decoration', + glyphMarginClassName: ThemeIcon.asClassName(Codicon.lightBulb), + glyphMargin: { position: GlyphMarginLane.Left }, + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + }); public static readonly ID = 'editor.contrib.lightbulbWidget'; @@ -54,11 +72,14 @@ export class LightBulbWidget extends Disposable implements IContentWidget { public readonly onClick = this._onClick.event; private _state: LightBulbState.State = LightBulbState.Hidden; + private _gutterState: LightBulbState.State = LightBulbState.Hidden; private _iconClasses: string[] = []; private _preferredKbLabel?: string; private _quickFixKbLabel?: string; + private gutterDecoration: ModelDecorationOptions = LightBulbWidget.GUTTER_DECORATION; + constructor( private readonly _editor: ICodeEditor, @IKeybindingService private readonly _keybindingService: IKeybindingService @@ -77,6 +98,10 @@ export class LightBulbWidget extends Disposable implements IContentWidget { if (this.state.type !== LightBulbState.Type.Showing || !editorModel || this.state.editorPosition.lineNumber >= editorModel.getLineCount()) { this.hide(); } + + if (this.gutterState.type !== LightBulbState.Type.Showing || !editorModel || this.gutterState.editorPosition.lineNumber >= editorModel.getLineCount()) { + this.gutterHide(); + } })); this._register(dom.addStandardDisposableGenericMouseDownListener(this._domNode, e => { @@ -119,14 +144,54 @@ export class LightBulbWidget extends Disposable implements IContentWidget { this._register(Event.runAndSubscribe(this._keybindingService.onDidUpdateKeybindings, () => { this._preferredKbLabel = this._keybindingService.lookupKeybinding(autoFixCommandId)?.getLabel() ?? undefined; this._quickFixKbLabel = this._keybindingService.lookupKeybinding(quickFixCommandId)?.getLabel() ?? undefined; - this._updateLightBulbTitleAndIcon(); })); + + this._register(this._editor.onMouseDown(async (e: IEditorMouseEvent) => { + const lightbulbClasses = [ + 'codicon-' + GUTTER_LIGHTBULB_ICON.id, + 'codicon-' + GUTTER_LIGHTBULB_AIFIX_AUTO_FIX_ICON.id, + 'codicon-' + GUTTER_LIGHTBULB_AUTO_FIX_ICON.id, + 'codicon-' + GUTTER_LIGHTBULB_AIFIX_ICON.id, + 'codicon-' + GUTTER_SPARKLE_FILLED_ICON.id + ]; + + if (!e.target.element || !lightbulbClasses.some(cls => e.target.element && e.target.element.classList.contains(cls))) { + return; + } + + if (this.gutterState.type !== LightBulbState.Type.Showing) { + return; + } + + // Make sure that focus / cursor location is not lost when clicking widget icon + this._editor.focus(); + + // a bit of extra work to make sure the menu + // doesn't cover the line-text + const { top, height } = dom.getDomNodePagePosition(e.target.element); + const lineHeight = this._editor.getOption(EditorOption.lineHeight); + + let pad = Math.floor(lineHeight / 3); + if (this.gutterState.widgetPosition.position !== null && this.gutterState.widgetPosition.position.lineNumber < this.gutterState.editorPosition.lineNumber) { + pad += lineHeight; + } + + this._onClick.fire({ + x: e.event.posx, + y: top + height + pad, + actions: this.gutterState.actions, + trigger: this.gutterState.trigger, + }); + })); } override dispose(): void { super.dispose(); this._editor.removeContentWidget(this); + if (this._gutterDecorationID) { + this._removeGutterDecoration(this._gutterDecorationID); + } } getId(): string { @@ -143,17 +208,20 @@ export class LightBulbWidget extends Disposable implements IContentWidget { public update(actions: CodeActionSet, trigger: CodeActionTrigger, atPosition: IPosition) { if (actions.validActions.length <= 0) { + this.gutterHide(); return this.hide(); } const options = this._editor.getOptions(); if (!options.get(EditorOption.lightbulb).enabled) { + this.gutterHide(); return this.hide(); } const model = this._editor.getModel(); if (!model) { + this.gutterHide(); return this.hide(); } @@ -171,7 +239,6 @@ export class LightBulbWidget extends Disposable implements IContentWidget { let effectiveLineNumber = lineNumber; let effectiveColumnNumber = 1; if (!lineHasSpace) { - // Checks if line is empty or starts with any amount of whitespace const isLineEmptyOrIndented = (lineNumber: number): boolean => { const lineContent = model.getLineContent(lineNumber); @@ -186,12 +253,37 @@ export class LightBulbWidget extends Disposable implements IContentWidget { const currLineEmptyOrIndented = isLineEmptyOrIndented(lineNumber); const notEmpty = !nextLineEmptyOrIndented && !prevLineEmptyOrIndented; - // check above and below. if both are blocked, display lightbulb below. - if (prevLineEmptyOrIndented || endLine || (notEmpty && !currLineEmptyOrIndented)) { + let hasDecoration = false; + const currLineDecorations = this._editor.getLineDecorations(lineNumber); + if (currLineDecorations) { + for (const decoration of currLineDecorations) { + if (decoration.options.glyphMarginClassName?.includes(Codicon.debugBreakpoint.id)) { + hasDecoration = true; + } + } + } + + // check above and below. if both are blocked, display lightbulb in the gutter. + if (!nextLineEmptyOrIndented && !prevLineEmptyOrIndented && !hasDecoration) { + this.gutterState = new LightBulbState.Showing(actions, trigger, atPosition, { + position: { lineNumber: effectiveLineNumber, column: effectiveColumnNumber }, + preference: LightBulbWidget._posPref + }); + this.renderGutterLightbub(); + return this.hide(); + } else if (prevLineEmptyOrIndented || endLine || (notEmpty && !currLineEmptyOrIndented)) { effectiveLineNumber -= 1; } else if (nextLineEmptyOrIndented || (notEmpty && currLineEmptyOrIndented)) { effectiveLineNumber += 1; } + } else if (lineNumber === 1 && (lineNumber === model.getLineCount() || !isLineEmptyOrIndented(lineNumber + 1) && !isLineEmptyOrIndented(lineNumber))) { + // special checks for first line blocked vs. not blocked. + this.gutterState = new LightBulbState.Showing(actions, trigger, atPosition, { + position: { lineNumber: effectiveLineNumber, column: effectiveColumnNumber }, + preference: LightBulbWidget._posPref + }); + this.renderGutterLightbub(); + return this.hide(); } else if ((lineNumber < model.getLineCount()) && !isFolded(lineNumber + 1)) { effectiveLineNumber += 1; } else if (column * fontInfo.spaceWidth < 22) { @@ -207,6 +299,11 @@ export class LightBulbWidget extends Disposable implements IContentWidget { preference: LightBulbWidget._posPref }); + if (this._gutterDecorationID) { + this._removeGutterDecoration(this._gutterDecorationID); + this.gutterHide(); + } + const validActions = actions.validActions; const actionKind = actions.validActions[0].action.kind; if (validActions.length !== 1 || !actionKind) { @@ -214,7 +311,6 @@ export class LightBulbWidget extends Disposable implements IContentWidget { return; } - this._editor.layoutContentWidget(this); } @@ -227,6 +323,18 @@ export class LightBulbWidget extends Disposable implements IContentWidget { this._editor.layoutContentWidget(this); } + public gutterHide(): void { + if (this.gutterState === LightBulbState.Hidden) { + return; + } + + if (this._gutterDecorationID) { + this._removeGutterDecoration(this._gutterDecorationID); + } + + this.gutterState = LightBulbState.Hidden; + } + private get state(): LightBulbState.State { return this._state; } private set state(value) { @@ -234,6 +342,13 @@ export class LightBulbWidget extends Disposable implements IContentWidget { this._updateLightBulbTitleAndIcon(); } + private get gutterState(): LightBulbState.State { return this._gutterState; } + + private set gutterState(value) { + this._gutterState = value; + this._updateGutterLightBulbTitleAndIcon(); + } + private _updateLightBulbTitleAndIcon(): void { this._domNode.classList.remove(...this._iconClasses); this._iconClasses = []; @@ -263,6 +378,74 @@ export class LightBulbWidget extends Disposable implements IContentWidget { this._domNode.classList.add(...this._iconClasses); } + private _updateGutterLightBulbTitleAndIcon(): void { + if (this.gutterState.type !== LightBulbState.Type.Showing) { + return; + } + let icon: ThemeIcon; + let autoRun = false; + if (this.gutterState.actions.allAIFixes) { + icon = GUTTER_SPARKLE_FILLED_ICON; + if (this.gutterState.actions.validActions.length === 1) { + autoRun = true; + } + } else if (this.gutterState.actions.hasAutoFix) { + if (this.gutterState.actions.hasAIFix) { + icon = GUTTER_LIGHTBULB_AIFIX_AUTO_FIX_ICON; + } else { + icon = GUTTER_LIGHTBULB_AUTO_FIX_ICON; + } + } else if (this.gutterState.actions.hasAIFix) { + icon = GUTTER_LIGHTBULB_AIFIX_ICON; + } else { + icon = GUTTER_LIGHTBULB_ICON; + } + this._updateLightbulbTitle(this.gutterState.actions.hasAutoFix, autoRun); + + const GUTTER_DECORATION = ModelDecorationOptions.register({ + description: 'codicon-gutter-lightbulb-decoration', + glyphMarginClassName: ThemeIcon.asClassName(icon), + glyphMargin: { position: GlyphMarginLane.Left }, + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + }); + + this.gutterDecoration = GUTTER_DECORATION; + } + + /* Gutter Helper Functions */ + private renderGutterLightbub(): void { + const selection = this._editor.getSelection(); + if (!selection) { + return; + } + + if (this._gutterDecorationID === undefined) { + this._addGutterDecoration(selection.startLineNumber); + } else { + this._updateGutterDecoration(this._gutterDecorationID, selection.startLineNumber); + } + } + + private _addGutterDecoration(lineNumber: number) { + this._editor.changeDecorations((accessor: IModelDecorationsChangeAccessor) => { + this._gutterDecorationID = accessor.addDecoration(new Range(lineNumber, 0, lineNumber, 0), this.gutterDecoration); + }); + } + + private _removeGutterDecoration(decorationId: string) { + this._editor.changeDecorations((accessor: IModelDecorationsChangeAccessor) => { + accessor.removeDecoration(decorationId); + this._gutterDecorationID = undefined; + }); + } + + private _updateGutterDecoration(decorationId: string, lineNumber: number) { + this._editor.changeDecorations((accessor: IModelDecorationsChangeAccessor) => { + accessor.changeDecoration(decorationId, new Range(lineNumber, 0, lineNumber, 0)); + accessor.changeDecorationOptions(decorationId, this.gutterDecoration); + }); + } + private _updateLightbulbTitle(autoFix: boolean, autoRun: boolean): void { if (this.state.type !== LightBulbState.Type.Showing) { return; diff --git a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts index 2d30e5925ab13..39c98256f3062 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts @@ -55,7 +55,8 @@ const enum State { Empty, Open, Frozen, - Details + Details, + onDetailsKeyDown } export interface ISelectedSuggestion { diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index ba6c8c81533e1..68a3a6c77db9b 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -300,7 +300,10 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi } // validate manifest - await getManifest(location.fsPath); + const manifest = await getManifest(location.fsPath); + if (!new ExtensionKey(gallery.identifier, gallery.version).equals(new ExtensionKey({ id: getGalleryExtensionId(manifest.publisher, manifest.name) }, manifest.version))) { + throw new ExtensionManagementError(nls.localize('invalidManifest', "Cannot install '{0}' extension because of manifest mismatch with Marketplace", gallery.identifier.id), ExtensionManagementErrorCode.Invalid); + } const local = await this.extensionsScanner.extractUserExtension( extensionKey, diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index 0429fdd1510f4..62e06494baf0a 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -278,6 +278,9 @@ const _allApiProposals = { quickDiffProvider: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts', }, + quickInputButtonLocation: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickInputButtonLocation.d.ts', + }, quickPickItemTooltip: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickPickItemTooltip.d.ts', }, diff --git a/src/vs/platform/quickinput/browser/media/quickInput.css b/src/vs/platform/quickinput/browser/media/quickInput.css index 3afa06e58881f..469f896ea768a 100644 --- a/src/vs/platform/quickinput/browser/media/quickInput.css +++ b/src/vs/platform/quickinput/browser/media/quickInput.css @@ -16,7 +16,8 @@ .quick-input-titlebar { display: flex; align-items: center; - border-radius: inherit; + border-top-right-radius: 5px; + border-top-left-radius: 5px; } .quick-input-left-action-bar { @@ -25,6 +26,10 @@ flex: 1; } +.quick-input-inline-action-bar { + margin: 2px 0 0 5px; +} + .quick-input-title { padding: 3px 0px; text-align: center; diff --git a/src/vs/platform/quickinput/browser/quickInput.ts b/src/vs/platform/quickinput/browser/quickInput.ts index 74414a631a107..69f430f128921 100644 --- a/src/vs/platform/quickinput/browser/quickInput.ts +++ b/src/vs/platform/quickinput/browser/quickInput.ts @@ -25,7 +25,7 @@ import Severity from 'vs/base/common/severity'; import { ThemeIcon } from 'vs/base/common/themables'; import 'vs/css!./media/quickInput'; import { localize } from 'vs/nls'; -import { IInputBox, IKeyMods, IQuickInput, IQuickInputButton, IQuickInputHideEvent, IQuickInputToggle, IQuickNavigateConfiguration, IQuickPick, IQuickPickDidAcceptEvent, IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator, IQuickPickSeparatorButtonEvent, IQuickPickWillAcceptEvent, IQuickWidget, ItemActivation, NO_KEY_MODS, QuickInputHideReason, QuickInputType } from 'vs/platform/quickinput/common/quickInput'; +import { IInputBox, IKeyMods, IQuickInput, IQuickInputButton, IQuickInputHideEvent, IQuickInputToggle, IQuickNavigateConfiguration, IQuickPick, IQuickPickDidAcceptEvent, IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator, IQuickPickSeparatorButtonEvent, IQuickPickWillAcceptEvent, IQuickWidget, ItemActivation, NO_KEY_MODS, QuickInputButtonLocation, QuickInputHideReason, QuickInputType } from 'vs/platform/quickinput/common/quickInput'; import { QuickInputBox } from './quickInputBox'; import { quickInputButtonToAction, renderQuickInputDescription } from './quickInputUtils'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -100,6 +100,7 @@ export interface QuickInputUI { description2: HTMLElement; widget: HTMLElement; rightActionBar: ActionBar; + inlineActionBar: ActionBar; checkAll: HTMLInputElement; inputContainer: HTMLElement; filterContainer: HTMLElement; @@ -157,7 +158,9 @@ abstract class QuickInput extends Disposable implements IQuickInput { private _contextKey: string | undefined; private _busy = false; private _ignoreFocusOut = false; - private _buttons: IQuickInputButton[] = []; + private _leftButtons: IQuickInputButton[] = []; + private _rightButtons: IQuickInputButton[] = []; + private _inlineButtons: IQuickInputButton[] = []; private buttonsUpdated = false; private _toggles: IQuickInputToggle[] = []; private togglesUpdated = false; @@ -273,12 +276,24 @@ abstract class QuickInput extends Disposable implements IQuickInput { } } + protected get titleButtons() { + return this._leftButtons.length + ? [...this._leftButtons, this._rightButtons] + : this._rightButtons; + } + get buttons() { - return this._buttons; + return [ + ...this._leftButtons, + ...this._rightButtons, + ...this._inlineButtons + ]; } set buttons(buttons: IQuickInputButton[]) { - this._buttons = buttons; + this._leftButtons = buttons.filter(b => b === backButton); + this._rightButtons = buttons.filter(b => b !== backButton && b.location !== QuickInputButtonLocation.Inline); + this._inlineButtons = buttons.filter(b => b.location === QuickInputButtonLocation.Inline); this.buttonsUpdated = true; this.update(); } @@ -407,8 +422,7 @@ abstract class QuickInput extends Disposable implements IQuickInput { if (this.buttonsUpdated) { this.buttonsUpdated = false; this.ui.leftActionBar.clear(); - const leftButtons = this.buttons - .filter(button => button === backButton) + const leftButtons = this._leftButtons .map((button, index) => quickInputButtonToAction( button, `id-${index}`, @@ -416,14 +430,21 @@ abstract class QuickInput extends Disposable implements IQuickInput { )); this.ui.leftActionBar.push(leftButtons, { icon: true, label: false }); this.ui.rightActionBar.clear(); - const rightButtons = this.buttons - .filter(button => button !== backButton) + const rightButtons = this._rightButtons .map((button, index) => quickInputButtonToAction( button, `id-${index}`, async () => this.onDidTriggerButtonEmitter.fire(button) )); this.ui.rightActionBar.push(rightButtons, { icon: true, label: false }); + this.ui.inlineActionBar.clear(); + const inlineButtons = this._inlineButtons + .map((button, index) => quickInputButtonToAction( + button, + `id-${index}`, + async () => this.onDidTriggerButtonEmitter.fire(button) + )); + this.ui.inlineActionBar.push(inlineButtons, { icon: true, label: false }); } if (this.togglesUpdated) { this.togglesUpdated = false; @@ -986,7 +1007,7 @@ export class QuickPick extends QuickInput implements I const scrollTopBefore = this.keepScrollPosition ? this.scrollTop : 0; const hasDescription = !!this.description; const visibilities: Visibilities = { - title: !!this.title || !!this.step || !!this.buttons.length, + title: !!this.title || !!this.step || !!this.titleButtons.length, description: hasDescription, checkAll: this.canSelectMany && !this._hideCheckAll, checkBox: this.canSelectMany, @@ -1213,7 +1234,7 @@ export class InputBox extends QuickInput implements IInputBox { this.ui.container.classList.remove('hidden-input'); const visibilities: Visibilities = { - title: !!this.title || !!this.step || !!this.buttons.length, + title: !!this.title || !!this.step || !!this.titleButtons.length, description: !!this.description || !!this.step, inputBox: true, message: true, @@ -1247,7 +1268,7 @@ export class QuickWidget extends QuickInput implements IQuickWidget { } const visibilities: Visibilities = { - title: !!this.title || !!this.step || !!this.buttons.length, + title: !!this.title || !!this.step || !!this.titleButtons.length, description: !!this.description || !!this.step }; diff --git a/src/vs/platform/quickinput/browser/quickInputController.ts b/src/vs/platform/quickinput/browser/quickInputController.ts index 8999c5e84ab8a..2fb617451aa0b 100644 --- a/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/src/vs/platform/quickinput/browser/quickInputController.ts @@ -157,6 +157,9 @@ export class QuickInputController extends Disposable { countContainer.setAttribute('aria-live', 'polite'); const count = new CountBadge(countContainer, { countFormat: localize({ key: 'quickInput.countSelected', comment: ['This tells the user how many items are selected in a list of items to select from. The items can be anything.'] }, "{0} Selected") }, this.styles.countBadge); + const inlineActionBar = this._register(new ActionBar(headerContainer, { hoverDelegate: this.options.hoverDelegate })); + inlineActionBar.domNode.classList.add('quick-input-inline-action-bar'); + const okContainer = dom.append(headerContainer, $('.quick-input-action')); const ok = this._register(new Button(okContainer, this.styles.button)); ok.label = localize('ok', "OK"); @@ -321,6 +324,7 @@ export class QuickInputController extends Disposable { description2, widget, rightActionBar, + inlineActionBar, checkAll, inputContainer, filterContainer, @@ -571,6 +575,7 @@ export class QuickInputController extends Disposable { ui.description2.textContent = ''; dom.reset(ui.widget); ui.rightActionBar.clear(); + ui.inlineActionBar.clear(); ui.checkAll.checked = false; // ui.inputBox.value = ''; Avoid triggering an event. ui.inputBox.placeholder = ''; diff --git a/src/vs/platform/quickinput/common/quickInput.ts b/src/vs/platform/quickinput/common/quickInput.ts index f893e58fbe6c0..b4bfac22752cb 100644 --- a/src/vs/platform/quickinput/common/quickInput.ts +++ b/src/vs/platform/quickinput/common/quickInput.ts @@ -705,6 +705,18 @@ export interface IInputBox extends IQuickInput { severity: Severity; } +export enum QuickInputButtonLocation { + /** + * In the title bar. + */ + Title = 1, + + /** + * To the right of the input box. + */ + Inline = 2 +} + /** * Represents a button in the quick input UI. */ @@ -728,6 +740,11 @@ export interface IQuickInputButton { * By default, buttons are only visible when hovering over them with the mouse. */ alwaysVisible?: boolean; + /** + * Where the button should be rendered. The default is {@link QuickInputButtonLocation.Title}. + * @note This property is ignored if the button was added to a QuickPickItem. + */ + location?: QuickInputButtonLocation; } /** diff --git a/src/vs/workbench/api/browser/mainThreadComments.ts b/src/vs/workbench/api/browser/mainThreadComments.ts index a0f00ce4416a6..91d33fc9f71e1 100644 --- a/src/vs/workbench/api/browser/mainThreadComments.ts +++ b/src/vs/workbench/api/browser/mainThreadComments.ts @@ -642,7 +642,7 @@ export class MainThreadComments extends Disposable implements MainThreadComments provider.updateCommentingRanges(resourceHints); } - async $revealCommentThread(handle: number, commentThreadHandle: number, options: languages.CommentThreadRevealOptions): Promise { + async $revealCommentThread(handle: number, commentThreadHandle: number, commentUniqueIdInThread: number, options: languages.CommentThreadRevealOptions): Promise { const provider = this._commentControllers.get(handle); if (!provider) { @@ -654,7 +654,24 @@ export class MainThreadComments extends Disposable implements MainThreadComments return Promise.resolve(); } - revealCommentThread(this._commentService, this._editorService, this._uriIdentityService, thread, undefined, options.focusReply, undefined, options.preserveFocus); + const comment = thread.comments?.find(comment => comment.uniqueIdInThread === commentUniqueIdInThread); + + revealCommentThread(this._commentService, this._editorService, this._uriIdentityService, thread, comment, options.focusReply, undefined, options.preserveFocus); + } + + async $hideCommentThread(handle: number, commentThreadHandle: number): Promise { + const provider = this._commentControllers.get(handle); + + if (!provider) { + return Promise.resolve(); + } + + const thread = provider.getAllComments().find(thread => thread.commentThreadHandle === commentThreadHandle); + if (!thread || !thread.isDocumentCommentThread()) { + return Promise.resolve(); + } + + thread.collapsibleState = languages.CommentThreadCollapsibleState.Collapsed; } private registerView(commentsViewAlreadyRegistered: boolean) { diff --git a/src/vs/workbench/api/browser/mainThreadSCM.ts b/src/vs/workbench/api/browser/mainThreadSCM.ts index af9d3f401cfb9..d35131056be37 100644 --- a/src/vs/workbench/api/browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/browser/mainThreadSCM.ts @@ -6,7 +6,7 @@ import { Barrier } from 'vs/base/common/async'; import { URI, UriComponents } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; -import { observableValue } from 'vs/base/common/observable'; +import { derivedOpts, observableValue, observableValueOpts } from 'vs/base/common/observable'; import { IDisposable, DisposableStore, combinedDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; import { ISCMService, ISCMRepository, ISCMProvider, ISCMResource, ISCMResourceGroup, ISCMResourceDecorations, IInputValidation, ISCMViewService, InputValidationType, ISCMActionButtonDescriptor } from 'vs/workbench/contrib/scm/common/scm'; import { ExtHostContext, MainThreadSCMShape, ExtHostSCMShape, SCMProviderFeatures, SCMRawResourceSplices, SCMGroupFeatures, MainContext, SCMHistoryItemGroupDto, SCMHistoryItemDto } from '../common/extHost.protocol'; @@ -17,7 +17,7 @@ import { MarshalledId } from 'vs/base/common/marshallingIds'; import { ThemeIcon } from 'vs/base/common/themables'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IQuickDiffService, QuickDiffProvider } from 'vs/workbench/contrib/scm/common/quickDiff'; -import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryItemGroup, ISCMHistoryOptions, ISCMHistoryProvider } from 'vs/workbench/contrib/scm/common/history'; +import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryItemGroup, ISCMHistoryItemGroupWithRevision, ISCMHistoryOptions, ISCMHistoryProvider } from 'vs/workbench/contrib/scm/common/history'; import { ResourceTree } from 'vs/base/common/resourceTree'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; @@ -27,6 +27,7 @@ import { IModelService } from 'vs/editor/common/services/model'; import { ITextModelContentProvider, ITextModelService } from 'vs/editor/common/services/resolverService'; import { Schemas } from 'vs/base/common/network'; import { ITextModel } from 'vs/editor/common/model'; +import { structuralEquals } from 'vs/base/common/equals'; function getIconFromIconDto(iconDto?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon): URI | { light: URI; dark: URI } | ThemeIcon | undefined { if (iconDto === undefined) { @@ -48,6 +49,12 @@ function toISCMHistoryItem(historyItemDto: SCMHistoryItemDto): ISCMHistoryItem { return { ...historyItemDto, icon, labels }; } +function historyItemGroupEquals(a: ISCMHistoryItemGroup | undefined, b: ISCMHistoryItemGroup | undefined): boolean { + return a?.id === b?.id && a?.name === b?.name && + a?.base?.id === b?.base?.id && a?.base?.name === b?.base?.name && + a?.remote?.id === b?.remote?.id && a?.remote?.name === b?.remote?.name; +} + class SCMInputBoxContentProvider extends Disposable implements ITextModelContentProvider { constructor( textModelService: ITextModelService, @@ -164,22 +171,37 @@ class MainThreadSCMHistoryProvider implements ISCMHistoryProvider { private _onDidChangeCurrentHistoryItemGroup = new Emitter(); readonly onDidChangeCurrentHistoryItemGroup = this._onDidChangeCurrentHistoryItemGroup.event; - private _currentHistoryItemGroup: ISCMHistoryItemGroup | undefined; - get currentHistoryItemGroup(): ISCMHistoryItemGroup | undefined { return this._currentHistoryItemGroup; } - set currentHistoryItemGroup(historyItemGroup: ISCMHistoryItemGroup | undefined) { + private _currentHistoryItemGroup: ISCMHistoryItemGroupWithRevision | undefined; + get currentHistoryItemGroup(): ISCMHistoryItemGroupWithRevision | undefined { return this._currentHistoryItemGroup; } + set currentHistoryItemGroup(historyItemGroup: ISCMHistoryItemGroupWithRevision | undefined) { this._currentHistoryItemGroup = historyItemGroup; this._onDidChangeCurrentHistoryItemGroup.fire(); } - private readonly _currentHistoryItemGroupObs = observableValue(this, undefined); + /** + * Changes when the id/name changes for the current, remote, or base history item group + */ + private readonly _currentHistoryItemGroupObs = derivedOpts({ + owner: this, equalsFn: historyItemGroupEquals, + }, reader => this._currentHistoryItemGroupWithRevisionObs.read(reader)); get currentHistoryItemGroupObs() { return this._currentHistoryItemGroupObs; } + /** + * Changes when the id/name/revision changes for the current, remote, or base history item group + */ + private readonly _currentHistoryItemGroupWithRevisionObs = observableValueOpts({ owner: this, equalsFn: structuralEquals }, undefined); + get currentHistoryItemGroupWithRevisionObs() { return this._currentHistoryItemGroupWithRevisionObs; } + constructor(private readonly proxy: ExtHostSCMShape, private readonly handle: number) { } async resolveHistoryItemGroupCommonAncestor(historyItemGroupId1: string, historyItemGroupId2: string | undefined): Promise<{ id: string; ahead: number; behind: number } | undefined> { return this.proxy.$resolveHistoryItemGroupCommonAncestor(this.handle, historyItemGroupId1, historyItemGroupId2, CancellationToken.None); } + async resolveHistoryItemGroupCommonAncestor2(historyItemGroupIds: string[]): Promise { + return this.proxy.$resolveHistoryItemGroupCommonAncestor2(this.handle, historyItemGroupIds, CancellationToken.None); + } + async provideHistoryItems(historyItemGroupId: string, options: ISCMHistoryOptions): Promise { const historyItems = await this.proxy.$provideHistoryItems(this.handle, historyItemGroupId, options, CancellationToken.None); return historyItems?.map(historyItem => toISCMHistoryItem(historyItem)); @@ -205,8 +227,8 @@ class MainThreadSCMHistoryProvider implements ISCMHistoryProvider { })); } - $onDidChangeCurrentHistoryItemGroup(historyItemGroup: ISCMHistoryItemGroup | undefined): void { - this._currentHistoryItemGroupObs.set(historyItemGroup, undefined); + $onDidChangeCurrentHistoryItemGroup(historyItemGroup: ISCMHistoryItemGroupWithRevision | undefined): void { + this._currentHistoryItemGroupWithRevisionObs.set(historyItemGroup, undefined); } } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 7847141b2f167..fdc2c217f9c69 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1539,6 +1539,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I CommentThreadCollapsibleState: extHostTypes.CommentThreadCollapsibleState, CommentThreadState: extHostTypes.CommentThreadState, CommentThreadApplicability: extHostTypes.CommentThreadApplicability, + CommentThreadFocus: extHostTypes.CommentThreadFocus, CompletionItem: extHostTypes.CompletionItem, CompletionItemKind: extHostTypes.CompletionItemKind, CompletionItemTag: extHostTypes.CompletionItemTag, @@ -1598,6 +1599,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I Position: extHostTypes.Position, ProcessExecution: extHostTypes.ProcessExecution, ProgressLocation: extHostTypes.ProgressLocation, + QuickInputButtonLocation: extHostTypes.QuickInputButtonLocation, QuickInputButtons: extHostTypes.QuickInputButtons, Range: extHostTypes.Range, RelativePattern: extHostTypes.RelativePattern, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 7bbab68a618c7..70cd8cb371a6f 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -149,7 +149,8 @@ export interface MainThreadCommentsShape extends IDisposable { $updateCommentThread(handle: number, commentThreadHandle: number, threadId: string, resource: UriComponents, changes: CommentThreadChanges): void; $deleteCommentThread(handle: number, commentThreadHandle: number): void; $updateCommentingRanges(handle: number, resourceHints?: languages.CommentingRangeResourceHint): void; - $revealCommentThread(handle: number, commentThreadHandle: number, options: languages.CommentThreadRevealOptions): Promise; + $revealCommentThread(handle: number, commentThreadHandle: number, commentUniqueIdInThread: number, options: languages.CommentThreadRevealOptions): Promise; + $hideCommentThread(handle: number, commentThreadHandle: number): void; } export interface AuthenticationForceNewSessionOptions { @@ -1515,6 +1516,7 @@ export type SCMRawResourceSplices = [ export interface SCMHistoryItemGroupDto { readonly id: string; readonly name: string; + readonly revision: string; readonly base?: Omit, 'remote'>; readonly remote?: Omit, 'remote'>; } @@ -2332,6 +2334,7 @@ export interface ExtHostSCMShape { $provideHistoryItemSummary(sourceControlHandle: number, historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): Promise; $provideHistoryItemChanges(sourceControlHandle: number, historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): Promise; $resolveHistoryItemGroupCommonAncestor(sourceControlHandle: number, historyItemGroupId1: string, historyItemGroupId2: string | undefined, token: CancellationToken): Promise<{ id: string; ahead: number; behind: number } | undefined>; + $resolveHistoryItemGroupCommonAncestor2(sourceControlHandle: number, historyItemGroupIds: string[], token: CancellationToken): Promise; } export interface ExtHostQuickDiffShape { diff --git a/src/vs/workbench/api/common/extHostComments.ts b/src/vs/workbench/api/common/extHostComments.ts index c84fb4782f9eb..b9742fa976cd7 100644 --- a/src/vs/workbench/api/common/extHostComments.ts +++ b/src/vs/workbench/api/common/extHostComments.ts @@ -463,7 +463,8 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo set label(value: string | undefined) { that.label = value; }, get state(): vscode.CommentThreadState | { resolved?: vscode.CommentThreadState; applicability?: vscode.CommentThreadApplicability } | undefined { return that.state; }, set state(value: vscode.CommentThreadState | { resolved?: vscode.CommentThreadState; applicability?: vscode.CommentThreadApplicability }) { that.state = value; }, - reveal: (options?: vscode.CommentThreadRevealOptions) => that.reveal(options), + reveal: (comment?: vscode.Comment | vscode.CommentThreadRevealOptions, options?: vscode.CommentThreadRevealOptions) => that.reveal(comment, options), + hide: () => that.hide(), dispose: () => { that.dispose(); } @@ -547,9 +548,29 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo return; } - async reveal(options?: vscode.CommentThreadRevealOptions): Promise { + async reveal(commentOrOptions?: vscode.Comment | vscode.CommentThreadRevealOptions, options?: vscode.CommentThreadRevealOptions): Promise { checkProposedApiEnabled(this.extensionDescription, 'commentReveal'); - return proxy.$revealCommentThread(this._commentControllerHandle, this.handle, { preserveFocus: false, focusReply: false, ...options }); + let comment: vscode.Comment | undefined; + if (commentOrOptions && (commentOrOptions as vscode.Comment).body !== undefined) { + comment = commentOrOptions as vscode.Comment; + } else { + options = options ?? commentOrOptions as vscode.CommentThreadRevealOptions; + } + let commentToReveal = comment ? this._commentsMap.get(comment) : undefined; + commentToReveal ??= this._commentsMap.get(this._comments[0])!; + let preserveFocus: boolean = true; + let focusReply: boolean = false; + if (options?.focus === types.CommentThreadFocus.Reply) { + focusReply = true; + preserveFocus = false; + } else if (options?.focus === types.CommentThreadFocus.Comment) { + preserveFocus = false; + } + return proxy.$revealCommentThread(this._commentControllerHandle, this.handle, commentToReveal, { preserveFocus, focusReply }); + } + + async hide(): Promise { + return proxy.$hideCommentThread(this._commentControllerHandle, this.handle); } dispose() { diff --git a/src/vs/workbench/api/common/extHostQuickOpen.ts b/src/vs/workbench/api/common/extHostQuickOpen.ts index b850be83f8642..3a9075f953b48 100644 --- a/src/vs/workbench/api/common/extHostQuickOpen.ts +++ b/src/vs/workbench/api/common/extHostQuickOpen.ts @@ -13,7 +13,7 @@ import { ExtHostQuickOpenShape, IMainContext, MainContext, TransferQuickInput, T import { URI } from 'vs/base/common/uri'; import { ThemeIcon, QuickInputButtons, QuickPickItemKind, InputBoxValidationSeverity } from 'vs/workbench/api/common/extHostTypes'; import { isCancellationError } from 'vs/base/common/errors'; -import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { coalesce } from 'vs/base/common/arrays'; import Severity from 'vs/base/common/severity'; import { ThemeIcon as ThemeIconUtils } from 'vs/base/common/themables'; @@ -301,7 +301,7 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx this._onDidChangeValueEmitter ]; - constructor(protected _extensionId: ExtensionIdentifier, private _onDidDispose: () => void) { + constructor(protected _extension: IExtensionDescription, private _onDidDispose: () => void) { } get title() { @@ -385,6 +385,10 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx } set buttons(buttons: QuickInputButton[]) { + const allowedButtonLocation = isProposedApiEnabled(this._extension, 'quickInputButtonLocation'); + if (!allowedButtonLocation && buttons.some(button => button.location)) { + console.warn(`Extension '${this._extension.identifier.value}' uses a button location which is proposed API that is only available when running out of dev or with the following command line switch: --enable-proposed-api ${this._extension.identifier.value}`); + } this._buttons = buttons.slice(); this._handlesToButtons.clear(); buttons.forEach((button, i) => { @@ -397,6 +401,7 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx ...getIconPathOrClass(button.iconPath), tooltip: button.tooltip, handle: button === QuickInputButtons.Back ? -1 : i, + location: allowedButtonLocation ? button.location : undefined }; }) }); @@ -546,8 +551,8 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx private readonly _onDidChangeSelectionEmitter = new Emitter(); private readonly _onDidTriggerItemButtonEmitter = new Emitter>(); - constructor(private extension: IExtensionDescription, onDispose: () => void) { - super(extension.identifier, onDispose); + constructor(extension: IExtensionDescription, onDispose: () => void) { + super(extension, onDispose); this._disposables.push( this._onDidChangeActiveEmitter, this._onDidChangeSelectionEmitter, @@ -569,7 +574,7 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx this._itemsToHandles.set(item, i); }); - const allowedTooltips = isProposedApiEnabled(this.extension, 'quickPickItemTooltip'); + const allowedTooltips = isProposedApiEnabled(this._extension, 'quickPickItemTooltip'); const pickItems: TransferQuickPickItemOrSeparator[] = []; for (let handle = 0; handle < items.length; handle++) { @@ -578,7 +583,7 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx pickItems.push({ type: 'separator', label: item.label }); } else { if (item.tooltip && !allowedTooltips) { - console.warn(`Extension '${this.extension.identifier.value}' uses a tooltip which is proposed API that is only available when running out of dev or with the following command line switch: --enable-proposed-api ${this.extension.identifier.value}`); + console.warn(`Extension '${this._extension.identifier.value}' uses a tooltip which is proposed API that is only available when running out of dev or with the following command line switch: --enable-proposed-api ${this._extension.identifier.value}`); } const icon = (item.iconPath) ? getIconPathOrClass(item.iconPath) : undefined; @@ -712,7 +717,7 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx private _validationMessage: string | InputBoxValidationMessage | undefined; constructor(extension: IExtensionDescription, onDispose: () => void) { - super(extension.identifier, onDispose); + super(extension, onDispose); this.update({ type: 'inputBox' }); } diff --git a/src/vs/workbench/api/common/extHostSCM.ts b/src/vs/workbench/api/common/extHostSCM.ts index 46f25cb7dd276..b07e8f498b2cd 100644 --- a/src/vs/workbench/api/common/extHostSCM.ts +++ b/src/vs/workbench/api/common/extHostSCM.ts @@ -972,6 +972,11 @@ export class ExtHostSCM implements ExtHostSCMShape { return await historyProvider?.resolveHistoryItemGroupCommonAncestor(historyItemGroupId1, historyItemGroupId2, token) ?? undefined; } + async $resolveHistoryItemGroupCommonAncestor2(sourceControlHandle: number, historyItemGroupIds: string[], token: CancellationToken): Promise { + const historyProvider = this._sourceControls.get(sourceControlHandle)?.historyProvider; + return await historyProvider?.resolveHistoryItemGroupCommonAncestor2(historyItemGroupIds, token) ?? undefined; + } + async $provideHistoryItems(sourceControlHandle: number, historyItemGroupId: string, options: any, token: CancellationToken): Promise { const historyProvider = this._sourceControls.get(sourceControlHandle)?.historyProvider; const historyItems = await historyProvider?.provideHistoryItems(historyItemGroupId, options, token); diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 772070502e63c..2ba4c07168866 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -1236,18 +1236,18 @@ export class Hover { @es5ClassCompat export class VerboseHover extends Hover { - public canIncreaseHover: boolean | undefined; - public canDecreaseHover: boolean | undefined; + public canIncreaseVerbosity: boolean | undefined; + public canDecreaseVerbosity: boolean | undefined; constructor( contents: vscode.MarkdownString | vscode.MarkedString | (vscode.MarkdownString | vscode.MarkedString)[], range?: Range, - canIncreaseHover?: boolean, - canDecreaseHover?: boolean, + canIncreaseVerbosity?: boolean, + canDecreaseVerbosity?: boolean, ) { super(contents, range); - this.canIncreaseHover = canIncreaseHover; - this.canDecreaseHover = canDecreaseHover; + this.canIncreaseVerbosity = canIncreaseVerbosity; + this.canDecreaseVerbosity = canDecreaseVerbosity; } } @@ -3341,6 +3341,11 @@ export enum CommentThreadApplicability { Outdated = 1 } +export enum CommentThreadFocus { + Reply = 1, + Comment = 2 +} + //#endregion //#region Semantic Coloring @@ -3592,6 +3597,11 @@ export class DebugVisualization { //#endregion +export enum QuickInputButtonLocation { + Title = 1, + Inline = 2 +} + @es5ClassCompat export class QuickInputButtons { diff --git a/src/vs/workbench/browser/parts/media/paneCompositePart.css b/src/vs/workbench/browser/parts/media/paneCompositePart.css index ecb5cad2d8b68..52f116455cf58 100644 --- a/src/vs/workbench/browser/parts/media/paneCompositePart.css +++ b/src/vs/workbench/browser/parts/media/paneCompositePart.css @@ -225,9 +225,9 @@ right: 0px; font-size: 9px; font-weight: 600; - min-width: 12px; - height: 12px; - line-height: 12px; + min-width: 13px; + height: 13px; + line-height: 13px; padding: 0 2px; border-radius: 16px; text-align: center; diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 6bb4cc041f845..42512bdad6315 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -406,10 +406,8 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { - const toUpdate = this.outdated.filter(e => !e.local?.pinned && this.shouldAutoUpdateExtension(e)); + const toUpdate = this.outdated.filter(e => this.shouldAutoUpdateExtension(e)); if (!toUpdate.length) { return; } @@ -1881,10 +1881,6 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (isString(extensionOrPublisher)) { throw new Error('Expected extension, found publisher string'); } - if (!extensionOrPublisher.local) { - throw new Error('Only installed extensions can be pinned'); - } - const disabledAutoUpdateExtensions = this.getDisabledAutoUpdateExtensions(); const extensionId = extensionOrPublisher.identifier.id.toLowerCase(); const extensionIndex = disabledAutoUpdateExtensions.indexOf(extensionId); @@ -1899,7 +1895,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } } this.setDisabledAutoUpdateExtensions(disabledAutoUpdateExtensions); - if (enable && extensionOrPublisher.pinned) { + if (enable && extensionOrPublisher.local && extensionOrPublisher.pinned) { await this.extensionManagementService.updateMetadata(extensionOrPublisher.local, { pinned: false }); } this._onChange.fire(extensionOrPublisher); diff --git a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts index b910fb3f2eef2..64ec809594661 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts @@ -417,7 +417,7 @@ MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { }); MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { - group: '3_compare', + group: '1_compare', order: 30, command: compareSelectedCommand, when: ContextKeyExpr.and(ResourceContextKey.HasResource, TwoEditorsSelectedInGroupContext, SelectedEditorsInGroupFileOrUntitledResourceContextKey) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts index 92ba84eb48e70..7f9546dec1731 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts @@ -26,6 +26,7 @@ import { MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; import { MenuItemAction } from 'vs/platform/actions/common/actions'; import { TextOnlyMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; export class InlineChatContentWidget implements IContentWidget { @@ -53,7 +54,8 @@ export class InlineChatContentWidget implements IContentWidget { private readonly _editor: ICodeEditor, @IInstantiationService instaService: IInstantiationService, @IContextKeyService contextKeyService: IContextKeyService, - @IConfigurationService configurationService: IConfigurationService + @IConfigurationService configurationService: IConfigurationService, + @IQuickInputService quickInputService: IQuickInputService ) { this._defaultChatModel = this._store.add(instaService.createInstance(ChatModel, undefined, ChatAgentLocation.Editor)); @@ -120,7 +122,7 @@ export class InlineChatContentWidget implements IContentWidget { const tracker = dom.trackFocus(this._domNode); this._store.add(tracker.onDidBlur(() => { - if (this._visible && this._widget.inputEditor.getModel()?.getValueLength() === 0) { + if (this._visible && this._widget.inputEditor.getModel()?.getValueLength() === 0 && !quickInputService.currentQuickInput) { this._onDidBlur.fire(); } })); @@ -193,6 +195,7 @@ export class InlineChatContentWidget implements IContentWidget { this._position = wordInfo ? new Position(position.lineNumber, wordInfo.startColumn) : position; this._editor.addContentWidget(this); + this._widget.setContext(true); this._widget.setVisible(true); } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index e920c34947bef..76d5e63818398 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -490,6 +490,7 @@ export class InlineChatWidget { } reset() { + this._chatWidget.setContext(true); this._chatWidget.saveState(); this.updateChatMessage(undefined); diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 7faf93b5efa5f..04ae69dc457a4 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -2920,6 +2920,8 @@ export class SCMViewPane extends ViewPane { private readonly revealResourceThrottler = new Throttler(); private readonly updateChildrenThrottler = new Throttler(); + private historyProviderCache!: SCMTreeHistoryProviderCache; + private viewModeContextKey: IContextKey; private viewSortKeyContextKey: IContextKey; private areAllRepositoriesCollapsedContextKey: IContextKey; @@ -3056,13 +3058,19 @@ export class SCMViewPane extends ViewPane { e.affectsConfiguration('scm.inputMinLineCount') || e.affectsConfiguration('scm.inputMaxLineCount') || e.affectsConfiguration('scm.showActionButton') || - e.affectsConfiguration('scm.showChangesSummary') || e.affectsConfiguration('scm.showIncomingChanges') || e.affectsConfiguration('scm.showOutgoingChanges') || e.affectsConfiguration('scm.experimental.showHistoryGraph'), this.visibilityDisposables) (() => this.updateChildren(), this, this.visibilityDisposables); + Event.filter(this.configurationService.onDidChangeConfiguration, + e => e.affectsConfiguration('scm.showChangesSummary'), this.visibilityDisposables) + (() => { + this.historyProviderCache.clear(); + this.updateChildren(); + }, this, this.visibilityDisposables); + // Add visible repositories this.editorService.onDidActiveEditorChange(this.onDidActiveEditorChange, this, this.visibilityDisposables); this.scmViewService.onDidChangeVisibleRepositories(this.onDidChangeVisibleRepositories, this, this.visibilityDisposables); @@ -3123,7 +3131,10 @@ export class SCMViewPane extends ViewPane { const historyItemHoverDelegate = this.instantiationService.createInstance(HistoryItemHoverDelegate, this.viewDescriptorService.getViewLocationById(this.id), this.layoutService.getSideBarPosition()); this.disposables.add(historyItemHoverDelegate); - const treeDataSource = this.instantiationService.createInstance(SCMTreeDataSource, () => this.viewMode); + this.historyProviderCache = this.instantiationService.createInstance(SCMTreeHistoryProviderCache); + this.disposables.add(this.historyProviderCache); + + const treeDataSource = this.instantiationService.createInstance(SCMTreeDataSource, () => this.viewMode, this.historyProviderCache); this.disposables.add(treeDataSource); this.tree = this.instantiationService.createInstance( @@ -3359,6 +3370,7 @@ export class SCMViewPane extends ViewPane { } repositoryDisposables.add(repository.provider.historyProvider.onDidChangeCurrentHistoryItemGroup(() => { + this.historyProviderCache.delete(repository); this.updateChildren(repository); this.logService.debug('SCMViewPane:onDidChangeCurrentHistoryItemGroup - update children'); })); @@ -3394,6 +3406,7 @@ export class SCMViewPane extends ViewPane { // Removed repositories for (const repository of removed) { + this.historyProviderCache.delete(repository); this.items.deleteAndDispose(repository); } @@ -3742,27 +3755,52 @@ export class SCMViewPane extends ViewPane { } } -class SCMTreeDataSource implements IAsyncDataSource { +class SCMTreeHistoryProviderCache extends Disposable { + private readonly _cache = new Map(); - private readonly historyProviderCache = new Map(); - private readonly repositoryDisposables = new DisposableMap(); - private readonly disposables = new DisposableStore(); + clear(): void { + this._cache.clear(); + } + + delete(repository: ISCMRepository): void { + this._cache.delete(repository); + } + + get(repository: ISCMRepository): ISCMHistoryProviderCacheEntry { + let entry = this._cache.get(repository); + + if (!entry) { + entry = { + incomingHistoryItemGroup: undefined, + outgoingHistoryItemGroup: undefined, + historyItems: new Map(), + historyItems2: new Map(), + historyItemChanges: new Map() + } satisfies ISCMHistoryProviderCacheEntry; + + this._cache.set(repository, entry); + } + return entry; + } + + update(repository: ISCMRepository, entry: Partial): void { + this._cache.set(repository, { + ...this.get(repository), + ...entry + }); + } +} + +class SCMTreeDataSource extends Disposable implements IAsyncDataSource { constructor( private readonly viewMode: () => ViewMode, + private readonly historyProviderCache: SCMTreeHistoryProviderCache, @IConfigurationService private readonly configurationService: IConfigurationService, - @ILogService private readonly logService: ILogService, @ISCMViewService private readonly scmViewService: ISCMViewService, @IUriIdentityService private uriIdentityService: IUriIdentityService, ) { - const onDidChangeConfiguration = Event.filter( - this.configurationService.onDidChangeConfiguration, - e => e.affectsConfiguration('scm.showChangesSummary'), - this.disposables); - this.disposables.add(onDidChangeConfiguration(() => this.historyProviderCache.clear())); - - this.scmViewService.onDidChangeVisibleRepositories(this.onDidChangeVisibleRepositories, this, this.disposables); - this.onDidChangeVisibleRepositories({ added: this.scmViewService.visibleRepositories, removed: Iterable.empty() }); + super(); } hasChildren(inputOrElement: ISCMViewService | TreeElement): boolean { @@ -3908,7 +3946,7 @@ class SCMTreeDataSource implements IAsyncDataSource 0 ? element.parentIds[0] : undefined; @@ -4084,9 +4125,7 @@ class SCMTreeDataSource implements IAsyncDataSource 0 ? element.parentIds[0] : undefined; historyItemChanges = await historyProvider.provideHistoryItemChanges(element.id, historyItemParentId) ?? []; - - this.historyProviderCache.set(repository, { - ...historyProviderCacheEntry, + this.historyProviderCache.update(repository, { historyItemChanges: historyItemChangesMap.set(`${element.id}/${historyItemParentId}`, historyItemChanges) }); } @@ -4175,50 +4214,6 @@ class SCMTreeDataSource implements IAsyncDataSource('scm.experimental.showHistoryGraph') }; } - - private onDidChangeVisibleRepositories({ added, removed }: ISCMViewVisibleRepositoryChangeEvent): void { - // Added repositories - for (const repository of added) { - const repositoryDisposables = new DisposableStore(); - - repositoryDisposables.add(Event.runAndSubscribe(repository.provider.onDidChangeHistoryProvider, () => { - if (!repository.provider.historyProvider) { - this.logService.debug('SCMTreeDataSource:onDidChangeVisibleRepositories - no history provider present'); - return; - } - - repositoryDisposables.add(repository.provider.historyProvider.onDidChangeCurrentHistoryItemGroup(() => { - this.historyProviderCache.delete(repository); - this.logService.debug('SCMTreeDataSource:onDidChangeCurrentHistoryItemGroup - cache cleared'); - })); - - this.logService.debug('SCMTreeDataSource:onDidChangeVisibleRepositories - onDidChangeCurrentHistoryItemGroup listener added'); - })); - - this.repositoryDisposables.set(repository, repositoryDisposables); - } - - // Removed repositories - for (const repository of removed) { - this.repositoryDisposables.deleteAndDispose(repository); - this.historyProviderCache.delete(repository); - } - } - - private getHistoryProviderCacheEntry(repository: ISCMRepository): ISCMHistoryProviderCacheEntry { - return this.historyProviderCache.get(repository) ?? { - incomingHistoryItemGroup: undefined, - outgoingHistoryItemGroup: undefined, - historyItems: new Map(), - historyItems2: new Map(), - historyItemChanges: new Map() - }; - } - - dispose(): void { - this.repositoryDisposables.dispose(); - this.disposables.dispose(); - } } export class SCMActionButton implements IDisposable { diff --git a/src/vs/workbench/contrib/scm/common/history.ts b/src/vs/workbench/contrib/scm/common/history.ts index 83d983824f3ed..f708d8617b87b 100644 --- a/src/vs/workbench/contrib/scm/common/history.ts +++ b/src/vs/workbench/contrib/scm/common/history.ts @@ -21,15 +21,17 @@ export interface ISCMHistoryProvider { readonly onDidChangeCurrentHistoryItemGroup: Event; - get currentHistoryItemGroup(): ISCMHistoryItemGroup | undefined; - set currentHistoryItemGroup(historyItemGroup: ISCMHistoryItemGroup | undefined); + get currentHistoryItemGroup(): ISCMHistoryItemGroupWithRevision | undefined; + set currentHistoryItemGroup(historyItemGroup: ISCMHistoryItemGroupWithRevision | undefined); readonly currentHistoryItemGroupObs: IObservable; + readonly currentHistoryItemGroupWithRevisionObs: IObservable; provideHistoryItems(historyItemGroupId: string, options: ISCMHistoryOptions): Promise; provideHistoryItems2(options: ISCMHistoryOptions): Promise; provideHistoryItemSummary(historyItemId: string, historyItemParentId: string | undefined): Promise; provideHistoryItemChanges(historyItemId: string, historyItemParentId: string | undefined): Promise; resolveHistoryItemGroupCommonAncestor(historyItemGroupId1: string, historyItemGroupId2: string | undefined): Promise<{ id: string; ahead: number; behind: number } | undefined>; + resolveHistoryItemGroupCommonAncestor2(historyItemGroupIds: string[]): Promise; } export interface ISCMHistoryProviderCacheEntry { @@ -53,6 +55,14 @@ export interface ISCMHistoryItemGroup { readonly remote?: Omit, 'remote'>; } +export interface ISCMHistoryItemGroupWithRevision { + readonly id: string; + readonly name: string; + readonly revision: string; + readonly base?: Omit, 'remote'>; + readonly remote?: Omit, 'remote'>; +} + export interface SCMHistoryItemGroupTreeElement { readonly id: string; readonly label: string; diff --git a/src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts b/src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts index aa71685ae2136..bf17d3c47fced 100644 --- a/src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts @@ -17,7 +17,7 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { WorkbenchCompressibleObjectTree, getSelectionKeyboardEvent } from 'vs/platform/list/browser/listService'; import { FastAndSlowPicks, IPickerQuickAccessItem, IPickerQuickAccessSeparator, PickerQuickAccessProvider, Picks, TriggerAction } from 'vs/platform/quickinput/browser/pickerQuickAccess'; import { DefaultQuickAccessFilterValue, IQuickAccessProviderRunOptions } from 'vs/platform/quickinput/common/quickAccess'; -import { IKeyMods, IQuickPick, IQuickPickItem, QuickInputHideReason } from 'vs/platform/quickinput/common/quickInput'; +import { IKeyMods, IQuickPick, IQuickPickItem, QuickInputButtonLocation, QuickInputHideReason } from 'vs/platform/quickinput/common/quickInput'; import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor'; import { searchDetailsIcon, searchOpenInFileIcon, searchActivityBarIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; @@ -32,6 +32,7 @@ import { PickerEditorState } from 'vs/workbench/browser/quickaccess'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; import { Sequencer } from 'vs/base/common/async'; import { URI } from 'vs/base/common/uri'; +import { Codicon } from 'vs/base/common/codicons'; export const TEXT_SEARCH_QUICK_ACCESS_PREFIX = '%'; @@ -104,10 +105,13 @@ export class TextSearchQuickAccess extends PickerQuickAccessProvider { + disposables.add(picker.onDidTriggerButton(() => { if (this.searchModel.searchResult.count() > 0) { this.moveToSearchViewlet(undefined); } else { diff --git a/src/vscode-dts/vscode.proposed.commentReveal.d.ts b/src/vscode-dts/vscode.proposed.commentReveal.d.ts index 168c4691de5de..3aa005d0a993d 100644 --- a/src/vscode-dts/vscode.proposed.commentReveal.d.ts +++ b/src/vscode-dts/vscode.proposed.commentReveal.d.ts @@ -7,26 +7,39 @@ declare module 'vscode' { // @alexr00 https://github.com/microsoft/vscode/issues/167253 + export enum CommentThreadFocus { + /** + * Focus the comment editor if the thread supports replying. + */ + Reply = 1, + /** + * Focus the revealed comment. + */ + Comment = 2 + } + /** * Options to reveal a comment thread in an editor. */ export interface CommentThreadRevealOptions { + /** - * By default, the comment thread will be focused. Set `preserveFocus` to `true` to maintain the original focus. + * Where to move the focus to when revealing the comment thread. + * If undefined, the focus will not be changed. */ - preserveFocus?: boolean; + focus?: CommentThreadFocus; + } + export interface CommentThread2 { /** - * Focus the comment thread reply editor, if the thread supports replying. + * Reveal the comment thread in an editor. If no comment is provided, the first comment in the thread will be revealed. */ - focusReply?: boolean; - } + reveal(comment?: Comment, options?: CommentThreadRevealOptions): Thenable; - export interface CommentThread { /** - * Reveal the comment thread in an editor. + * Collapse the comment thread in an editor. */ - reveal(options?: CommentThreadRevealOptions): Thenable; + hide(): Thenable; } } diff --git a/src/vscode-dts/vscode.proposed.commentThreadApplicability.d.ts b/src/vscode-dts/vscode.proposed.commentThreadApplicability.d.ts index 547ee182227ea..fb99abb48bd16 100644 --- a/src/vscode-dts/vscode.proposed.commentThreadApplicability.d.ts +++ b/src/vscode-dts/vscode.proposed.commentThreadApplicability.d.ts @@ -36,7 +36,5 @@ declare module 'vscode' { contextValue?: string; label?: string; dispose(): void; - // Part of the comment reveal proposal - reveal(options?: CommentThreadRevealOptions): Thenable; } } diff --git a/src/vscode-dts/vscode.proposed.quickInputButtonLocation.d.ts b/src/vscode-dts/vscode.proposed.quickInputButtonLocation.d.ts new file mode 100644 index 0000000000000..5113c5864ff04 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.quickInputButtonLocation.d.ts @@ -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. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/175662 + + export enum QuickInputButtonLocation { + /** + * In the title bar. + */ + Title = 1, + + /** + * To the right of the input box. + */ + Inline = 2 + } + + export interface QuickInputButton { + /** + * Where the button should be rendered. The default is {@link QuickInputButtonLocation.Title}. + * @note This property is ignored if the button was added to a QuickPickItem. + */ + location?: QuickInputButtonLocation; + } +} diff --git a/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts b/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts index 9d3151733aa99..f79fa1885b9b1 100644 --- a/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts @@ -30,6 +30,7 @@ declare module 'vscode' { provideHistoryItemChanges(historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): ProviderResult; resolveHistoryItemGroupCommonAncestor(historyItemGroupId1: string, historyItemGroupId2: string | undefined, token: CancellationToken): ProviderResult<{ id: string; ahead: number; behind: number }>; + resolveHistoryItemGroupCommonAncestor2(historyItemGroupIds: string[], token: CancellationToken): ProviderResult; } export interface SourceControlHistoryOptions { @@ -41,6 +42,7 @@ declare module 'vscode' { export interface SourceControlHistoryItemGroup { readonly id: string; readonly name: string; + readonly revision: string; readonly base?: Omit, 'remote'>; readonly remote?: Omit, 'remote'>; }