diff options
author | Sandeep Somavarapu <sasomava@microsoft.com> | 2022-07-27 14:18:51 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-07-27 14:18:51 +0300 |
commit | eca4972002906eff6e936e6f4625b51725e37761 (patch) | |
tree | 3576236c8e4c1ed7a2661654e61f25e21ae2e908 /src/vs | |
parent | 2705b50c359351e60a17c3bf24724e281e360a2e (diff) | |
parent | cb48abb0c0470c6060ba34586aeb7053f9684ee8 (diff) |
Merge branch 'main' into patch-1
Diffstat (limited to 'src/vs')
127 files changed, 2034 insertions, 940 deletions
diff --git a/src/vs/base/common/event.ts b/src/vs/base/common/event.ts index d395e4a9f32..5ceab5b92a0 100644 --- a/src/vs/base/common/event.ts +++ b/src/vs/base/common/event.ts @@ -8,7 +8,7 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { once as onceFn } from 'vs/base/common/functional'; import { combinedDisposable, Disposable, DisposableStore, IDisposable, SafeDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { LinkedList } from 'vs/base/common/linkedList'; -import { IObservable, IObserver } from 'vs/base/common/observableImpl/base'; +import { IObservable, IObserver } from 'vs/base/common/observable'; import { StopWatch } from 'vs/base/common/stopwatch'; diff --git a/src/vs/base/common/network.ts b/src/vs/base/common/network.ts index 63d6a15b49e..20f6b38e629 100644 --- a/src/vs/base/common/network.ts +++ b/src/vs/base/common/network.ts @@ -101,6 +101,11 @@ export namespace Schemas { * Scheme used vs live share */ export const vsls = 'vsls'; + + /** + * Scheme used for the Source Control commit input's text document + */ + export const vscodeSourceControl = 'vscode-scm'; } export const connectionTokenCookieName = 'vscode-tkn'; diff --git a/src/vs/base/common/types.ts b/src/vs/base/common/types.ts index e3844cd39e8..3e1cb2a7354 100644 --- a/src/vs/base/common/types.ts +++ b/src/vs/base/common/types.ts @@ -275,9 +275,3 @@ export type UriDto<T> = { [K in keyof T]: T[K] extends URI export function assertNever(value: never, message = 'Unreachable'): never { throw new Error(message); } - -/** - * Given an object with all optional properties, requires at least one to be defined. - * i.e. AtLeastOne<MyObject>; - */ -export type AtLeastOne<T, U = { [K in keyof T]: Pick<T, K> }> = Partial<T> & U[keyof U]; diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 7f2a4965b81..d959a938e93 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -62,7 +62,7 @@ import { ICustomEndpointTelemetryService, ITelemetryService } from 'vs/platform/ import { TelemetryAppenderChannel } from 'vs/platform/telemetry/common/telemetryIpc'; import { TelemetryLogAppender } from 'vs/platform/telemetry/common/telemetryLogAppender'; import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService'; -import { supportsTelemetry, ITelemetryAppender, NullAppender, NullTelemetryService, getPiiPathsFromEnvironment } from 'vs/platform/telemetry/common/telemetryUtils'; +import { supportsTelemetry, ITelemetryAppender, NullAppender, NullTelemetryService, getPiiPathsFromEnvironment, isInternalTelemetry } from 'vs/platform/telemetry/common/telemetryUtils'; import { CustomEndpointTelemetryService } from 'vs/platform/telemetry/node/customEndpointTelemetryService'; import { LocalReconnectConstants, TerminalIpcChannels, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; import { ILocalPtyService } from 'vs/platform/terminal/electron-sandbox/terminal'; @@ -277,19 +277,20 @@ class SharedProcessMain extends Disposable { // Telemetry let telemetryService: ITelemetryService; const appenders: ITelemetryAppender[] = []; + const internalTelemetry = isInternalTelemetry(productService, configurationService); if (supportsTelemetry(productService, environmentService)) { const logAppender = new TelemetryLogAppender(loggerService, environmentService); appenders.push(logAppender); const { installSourcePath } = environmentService; if (productService.aiConfig?.ariaKey) { - const collectorAppender = new OneDataSystemWebAppender(configurationService, 'monacoworkbench', null, productService.aiConfig.ariaKey); + const collectorAppender = new OneDataSystemWebAppender(internalTelemetry, 'monacoworkbench', null, productService.aiConfig.ariaKey); this._register(toDisposable(() => collectorAppender.flush())); // Ensure the 1DS appender is disposed so that it flushes remaining data appenders.push(collectorAppender); } telemetryService = new TelemetryService({ appenders, - commonProperties: resolveCommonProperties(fileService, release(), hostname(), process.arch, productService.commit, productService.version, this.configuration.machineId, productService.msftInternalDomains, installSourcePath), + commonProperties: resolveCommonProperties(fileService, release(), hostname(), process.arch, productService.commit, productService.version, this.configuration.machineId, internalTelemetry, installSourcePath), sendErrorTelemetry: true, piiPaths: getPiiPathsFromEnvironment(environmentService), }, configurationService, productService); diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 670685ffd9d..b13a6098ddd 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -75,7 +75,7 @@ import { resolveCommonProperties } from 'vs/platform/telemetry/common/commonProp import { ITelemetryService, machineIdKey, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; import { TelemetryAppenderClient } from 'vs/platform/telemetry/common/telemetryIpc'; import { ITelemetryServiceConfig, TelemetryService } from 'vs/platform/telemetry/common/telemetryService'; -import { getPiiPathsFromEnvironment, getTelemetryLevel, NullTelemetryService, supportsTelemetry } from 'vs/platform/telemetry/common/telemetryUtils'; +import { getPiiPathsFromEnvironment, getTelemetryLevel, isInternalTelemetry, NullTelemetryService, supportsTelemetry } from 'vs/platform/telemetry/common/telemetryUtils'; import { IUpdateService } from 'vs/platform/update/common/update'; import { UpdateChannel } from 'vs/platform/update/common/updateIpc'; import { DarwinUpdateService } from 'vs/platform/update/electron-main/updateService.darwin'; @@ -680,9 +680,10 @@ export class CodeApplication extends Disposable { // Telemetry if (supportsTelemetry(this.productService, this.environmentMainService)) { + const isInternal = isInternalTelemetry(this.productService, this.configurationService); const channel = getDelayedChannel(sharedProcessReady.then(client => client.getChannel('telemetryAppender'))); const appender = new TelemetryAppenderClient(channel); - const commonProperties = resolveCommonProperties(this.fileService, release(), hostname(), process.arch, this.productService.commit, this.productService.version, machineId, this.productService.msftInternalDomains, this.environmentMainService.installSourcePath); + const commonProperties = resolveCommonProperties(this.fileService, release(), hostname(), process.arch, this.productService.commit, this.productService.version, machineId, isInternal, this.environmentMainService.installSourcePath); const piiPaths = getPiiPathsFromEnvironment(this.environmentMainService); const config: ITelemetryServiceConfig = { appenders: [appender], commonProperties, piiPaths, sendErrorTelemetry: true }; diff --git a/src/vs/code/electron-sandbox/workbench/workbench.html b/src/vs/code/electron-sandbox/workbench/workbench.html index 47066f520be..16e089df650 100644 --- a/src/vs/code/electron-sandbox/workbench/workbench.html +++ b/src/vs/code/electron-sandbox/workbench/workbench.html @@ -4,7 +4,7 @@ <head> <meta charset="utf-8" /> <meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src 'self' https: data: blob: vscode-remote-resource:; media-src 'self'; frame-src 'self' vscode-webview:; object-src 'self'; script-src 'self' 'unsafe-eval' blob:; style-src 'self' 'unsafe-inline'; connect-src 'self' https: ws:; font-src 'self' https: vscode-remote-resource:;"> - <meta http-equiv="Content-Security-Policy" content="require-trusted-types-for 'script'; trusted-types amdLoader cellRendererEditorText defaultWorkerFactory diffEditorWidget editorGhostText domLineBreaksComputer editorViewLayer diffReview dompurify notebookRenderer safeInnerHtml standaloneColorizer tokenizeToString;"> + <meta http-equiv="Content-Security-Policy" content="require-trusted-types-for 'script'; trusted-types amdLoader cellRendererEditorText defaultWorkerFactory diffEditorWidget stickyScrollViewLayer editorGhostText domLineBreaksComputer editorViewLayer diffReview dompurify notebookRenderer safeInnerHtml standaloneColorizer tokenizeToString;"> </head> <body aria-label=""> </body> diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index c64e6fe0503..3a006e635a3 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -63,10 +63,6 @@ export async function main(argv: string[]): Promise<any> { // Shell integration else if (args['locate-shell-integration-path']) { - // Silently fail when the terminal is not VS Code's integrated terminal - if (process.env['TERM_PROGRAM'] !== 'vscode') { - return; - } let file: string; switch (args['locate-shell-integration-path']) { // Usage: `[[ "$TERM_PROGRAM" == "vscode" ]] && . "$(code --locate-shell-integration-path bash)"` diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index 79c5b02e686..eda61ad1689 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -52,7 +52,7 @@ import { StateService } from 'vs/platform/state/node/stateService'; import { resolveCommonProperties } from 'vs/platform/telemetry/common/commonProperties'; import { ITelemetryService, machineIdKey } from 'vs/platform/telemetry/common/telemetry'; import { ITelemetryServiceConfig, TelemetryService } from 'vs/platform/telemetry/common/telemetryService'; -import { supportsTelemetry, NullTelemetryService, getPiiPathsFromEnvironment } from 'vs/platform/telemetry/common/telemetryUtils'; +import { supportsTelemetry, NullTelemetryService, getPiiPathsFromEnvironment, isInternalTelemetry } from 'vs/platform/telemetry/common/telemetryUtils'; import { OneDataSystemAppender } from 'vs/platform/telemetry/node/1dsAppender'; import { buildTelemetryMessage } from 'vs/platform/telemetry/node/telemetry'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; @@ -187,9 +187,10 @@ class CliMain extends Disposable { // Telemetry const appenders: OneDataSystemAppender[] = []; + const isInternal = isInternalTelemetry(productService, configurationService); if (supportsTelemetry(productService, environmentService)) { if (productService.aiConfig && productService.aiConfig.ariaKey) { - appenders.push(new OneDataSystemAppender(configurationService, 'monacoworkbench', null, productService.aiConfig.ariaKey)); + appenders.push(new OneDataSystemAppender(isInternal, 'monacoworkbench', null, productService.aiConfig.ariaKey)); } const { installSourcePath } = environmentService; @@ -208,7 +209,7 @@ class CliMain extends Disposable { } } - return resolveCommonProperties(fileService, release(), hostname(), process.arch, productService.commit, productService.version, machineId, productService.msftInternalDomains, installSourcePath); + return resolveCommonProperties(fileService, release(), hostname(), process.arch, productService.commit, productService.version, machineId, isInternal, installSourcePath); })(), piiPaths: getPiiPathsFromEnvironment(environmentService) }; diff --git a/src/vs/editor/browser/coreCommands.ts b/src/vs/editor/browser/coreCommands.ts index ecc72a1a366..b5ab2a699e1 100644 --- a/src/vs/editor/browser/coreCommands.ts +++ b/src/vs/editor/browser/coreCommands.ts @@ -84,7 +84,7 @@ export namespace EditorScroll_ { \`\`\` * 'by': Unit to move. Default is computed based on 'to' value. \`\`\` - 'line', 'wrappedLine', 'page', 'halfPage' + 'line', 'wrappedLine', 'page', 'halfPage', 'editor' \`\`\` * 'value': Number of units to move. Default is '1'. * 'revealCursor': If 'true' reveals the cursor if it is outside view port. @@ -100,7 +100,7 @@ export namespace EditorScroll_ { }, 'by': { 'type': 'string', - 'enum': ['line', 'wrappedLine', 'page', 'halfPage'] + 'enum': ['line', 'wrappedLine', 'page', 'halfPage', 'editor'] }, 'value': { 'type': 'number', @@ -130,7 +130,8 @@ export namespace EditorScroll_ { Line: 'line', WrappedLine: 'wrappedLine', Page: 'page', - HalfPage: 'halfPage' + HalfPage: 'halfPage', + Editor: 'editor' }; /** @@ -172,6 +173,9 @@ export namespace EditorScroll_ { case RawUnit.HalfPage: unit = Unit.HalfPage; break; + case RawUnit.Editor: + unit = Unit.Editor; + break; default: unit = Unit.WrappedLine; } @@ -205,7 +209,8 @@ export namespace EditorScroll_ { Line = 1, WrappedLine = 2, Page = 3, - HalfPage = 4 + HalfPage = 4, + Editor = 5 } } @@ -1279,6 +1284,14 @@ export namespace CoreNavigationCommands { return viewModel.viewLayout.getVerticalOffsetForLineNumber(viewPosition.lineNumber); } + if (args.unit === EditorScroll_.Unit.Editor) { + let desiredTopModelLineNumber = 0; + if (args.direction === EditorScroll_.Direction.Down) { + desiredTopModelLineNumber = viewModel.model.getLineCount() - viewModel.cursorConfig.pageSize; + } + return viewModel.viewLayout.getVerticalOffsetForLineNumber(desiredTopModelLineNumber); + } + let noOfLines: number; if (args.unit === EditorScroll_.Unit.Page) { noOfLines = viewModel.cursorConfig.pageSize * args.value; @@ -1345,6 +1358,29 @@ export namespace CoreNavigationCommands { } }); + export const ScrollEditorTop: CoreEditorCommand = registerEditorCommand(new class extends CoreEditorCommand { + constructor() { + super({ + id: 'scrollEditorTop', + precondition: undefined, + kbOpts: { + weight: CORE_WEIGHT, + kbExpr: EditorContextKeys.textInputFocus, + } + }); + } + + runCoreEditorCommand(viewModel: IViewModel, args: any): void { + EditorScroll._runEditorScroll(viewModel, args.source, { + direction: EditorScroll_.Direction.Up, + unit: EditorScroll_.Unit.Editor, + value: 1, + revealCursor: false, + select: false + }); + } + }); + export const ScrollLineDown: CoreEditorCommand = registerEditorCommand(new class extends CoreEditorCommand { constructor() { super({ @@ -1396,6 +1432,29 @@ export namespace CoreNavigationCommands { } }); + export const ScrollEditorBottom: CoreEditorCommand = registerEditorCommand(new class extends CoreEditorCommand { + constructor() { + super({ + id: 'scrollEditorBottom', + precondition: undefined, + kbOpts: { + weight: CORE_WEIGHT, + kbExpr: EditorContextKeys.textInputFocus, + } + }); + } + + runCoreEditorCommand(viewModel: IViewModel, args: any): void { + EditorScroll._runEditorScroll(viewModel, args.source, { + direction: EditorScroll_.Direction.Down, + unit: EditorScroll_.Unit.Editor, + value: 1, + revealCursor: false, + select: false + }); + } + }); + class WordCommand extends CoreEditorCommand { private readonly _inSelectionMode: boolean; diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index 0cc2cfa02af..bed9912d2d3 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -371,7 +371,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE this._register(new dom.DragAndDropObserver(this._domElement, { onDragEnter: () => undefined, onDragOver: e => { - if (!this._configuration.options.get(EditorOption.enableDropIntoEditor)) { + if (!this._configuration.options.get(EditorOption.dropIntoEditor).enabled) { return; } @@ -381,7 +381,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE } }, onDrop: async e => { - if (!this._configuration.options.get(EditorOption.enableDropIntoEditor)) { + if (!this._configuration.options.get(EditorOption.dropIntoEditor).enabled) { return; } diff --git a/src/vs/editor/browser/widget/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditorWidget.ts index ee1a5a107b9..ef1aa278e53 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget.ts @@ -1189,7 +1189,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE result.ariaLabel = options.originalAriaLabel; } result.readOnly = !this._options.originalEditable; - result.enableDropIntoEditor = !result.readOnly; + result.dropIntoEditor = { enabled: !result.readOnly }; result.extraEditorClassName = 'original-in-monaco-diff-editor'; return { ...result, diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 31e39bf1cf4..066080e9ae3 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -170,6 +170,10 @@ export interface IEditorOptions { */ scrollbar?: IEditorScrollbarOptions; /** + * Control the behavior of the sticky scroll + */ + stickyScroll?: IEditorStickyScrollOptions; + /** * Control the behavior and rendering of the minimap. */ minimap?: IEditorMinimapOptions; @@ -661,11 +665,11 @@ export interface IEditorOptions { bracketPairColorization?: IBracketPairColorizationOptions; /** - * Enables dropping into the editor from an external source. + * Controls dropping into the editor from an external source. * - * This shows a preview of the drop location and triggers an `onDropIntoEditor` event. + * When enabled, this shows a preview of the drop location and triggers an `onDropIntoEditor` event. */ - enableDropIntoEditor?: boolean; + dropIntoEditor?: IDropIntoEditorOptions; } /** @@ -2504,6 +2508,54 @@ class EditorLightbulb extends BaseEditorOption<EditorOption.lightbulb, IEditorLi //#endregion +//#region sticky scroll + +/** + * Configuration options for editor sticky scroll + */ + +export interface IEditorStickyScrollOptions { + /** + * Enable the sticky scroll + */ + enabled?: boolean; +} + +/** + * @internal + */ + +export type EditorStickyScrollOptions = Readonly<Required<IEditorStickyScrollOptions>>; + +class EditorStickyScroll extends BaseEditorOption<EditorOption.stickyScroll, IEditorStickyScrollOptions, EditorStickyScrollOptions> { + + constructor() { + const defaults: EditorStickyScrollOptions = { enabled: false }; + super( + EditorOption.stickyScroll, 'stickyScroll', defaults, + { + 'editor.stickyScroll.enabled': { + type: 'boolean', + default: defaults.enabled, + description: nls.localize('editor.stickyScroll', "Enables the sticky scroll in the editor.") + }, + } + ); + } + + public validate(_input: any): EditorStickyScrollOptions { + if (!_input || typeof _input !== 'object') { + return this.defaultValue; + } + const input = _input as IEditorStickyScrollOptions; + return { + enabled: boolean(input.enabled, this.defaultValue.enabled) + }; + } +} + +//#endregion + //#region inlayHints /** @@ -4387,6 +4439,53 @@ class EditorWrappingInfoComputer extends ComputedEditorOption<EditorOption.wrapp //#endregion +//#region dropIntoEditor + +/** + * Configuration options for editor drop into behavior + */ +export interface IDropIntoEditorOptions { + /** + * Enable the dropping into editor. + * Defaults to true. + */ + enabled?: boolean; +} + +/** + * @internal + */ +export type EditorDropIntoEditorOptions = Readonly<Required<IDropIntoEditorOptions>>; + +class EditorDropIntoEditor extends BaseEditorOption<EditorOption.dropIntoEditor, IDropIntoEditorOptions, EditorDropIntoEditorOptions> { + + constructor() { + const defaults: EditorDropIntoEditorOptions = { enabled: true }; + super( + EditorOption.dropIntoEditor, 'dropIntoEditor', defaults, + { + 'editor.dropIntoEditor.enabled': { + type: 'boolean', + default: defaults.enabled, + markdownDescription: nls.localize('dropIntoEditor.enabled', "Controls whether you can drag and drop a file into a text editor by holding down `shift` (instead of opening the file in an editor)."), + }, + } + ); + } + + public validate(_input: any): EditorDropIntoEditorOptions { + if (!_input || typeof _input !== 'object') { + return this.defaultValue; + } + const input = _input as IDropIntoEditorOptions; + return { + enabled: boolean(input.enabled, this.defaultValue.enabled) + }; + } +} + +//#endregion + const DEFAULT_WINDOWS_FONT_FAMILY = 'Consolas, \'Courier New\', monospace'; const DEFAULT_MAC_FONT_FAMILY = 'Menlo, Monaco, \'Courier New\', monospace'; const DEFAULT_LINUX_FONT_FAMILY = '\'Droid Sans Mono\', \'monospace\', monospace'; @@ -4449,7 +4548,7 @@ export const enum EditorOption { disableMonospaceOptimizations, domReadOnly, dragAndDrop, - enableDropIntoEditor, + dropIntoEditor, emptySelectionClipboard, extraEditorClassName, fastScrollSensitivity, @@ -4523,6 +4622,7 @@ export const enum EditorOption { smartSelect, smoothScrolling, stickyTabStops, + stickyScroll, stopRenderingLineAfter, suggest, suggestFontSize, @@ -4758,9 +4858,7 @@ export const EditorOptions = { { description: nls.localize('dragAndDrop', "Controls whether the editor should allow moving selections via drag and drop.") } )), emptySelectionClipboard: register(new EditorEmptySelectionClipboard()), - enableDropIntoEditor: register(new EditorBooleanOption( - EditorOption.enableDropIntoEditor, 'enableDropIntoEditor', true - )), + dropIntoEditor: register(new EditorDropIntoEditor()), extraEditorClassName: register(new EditorStringOption( EditorOption.extraEditorClassName, 'extraEditorClassName', '', )), @@ -5079,6 +5177,7 @@ export const EditorOptions = { EditorOption.smoothScrolling, 'smoothScrolling', false, { description: nls.localize('smoothScrolling', "Controls whether the editor will scroll using an animation.") } )), + stickyScroll: register(new EditorStickyScroll()), stopRenderingLineAfter: register(new EditorIntOption( EditorOption.stopRenderingLineAfter, 'stopRenderingLineAfter', 10000, -1, Constants.MAX_SAFE_SMALL_INTEGER, diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 10baf2667c9..a3d57455de4 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -669,9 +669,6 @@ export interface CodeAction { disabled?: string; } -/** - * @internal - */ export const enum CodeActionTriggerType { Invoke = 1, Auto = 2, @@ -1132,7 +1129,11 @@ export interface DocumentSymbolProvider { provideDocumentSymbols(model: model.ITextModel, token: CancellationToken): ProviderResult<DocumentSymbol[]>; } -export type TextEdit = { range: IRange; text: string; eol?: model.EndOfLineSequence }; +export interface TextEdit { + range: IRange; + text: string; + eol?: model.EndOfLineSequence; +} /** * Interface used to format a model diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index 354026fdcb5..b6361586bd8 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -528,14 +528,19 @@ export const enum TrackedRangeStickiness { * Text snapshot that works like an iterator. * Will try to return chunks of roughly ~64KB size. * Will return null when finished. - * - * @internal */ export interface ITextSnapshot { read(): string | null; } /** + * @internal + */ +export function isITextSnapshot(obj: any): obj is ITextSnapshot { + return (obj && typeof obj.read === 'function'); +} + +/** * A model. */ export interface ITextModel { @@ -610,7 +615,7 @@ export interface ITextModel { /** * Replace the entire text buffer value contained in this model. */ - setValue(newValue: string): void; + setValue(newValue: string | ITextSnapshot): void; /** * Get the text stored in this model. @@ -624,7 +629,6 @@ export interface ITextModel { * Get the text stored in this model. * @param preserverBOM Preserve a BOM character if it was detected when the model was constructed. * @return The text snapshot (it is safe to consume it asynchronously). - * @internal */ createSnapshot(preserveBOM?: boolean): ITextSnapshot; diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 690a2eab09a..59675e6d0c6 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -44,12 +44,8 @@ import { ITokenizationTextModelPart } from 'vs/editor/common/tokenizationTextMod import { IColorTheme, ThemeColor } from 'vs/platform/theme/common/themeService'; import { IUndoRedoService, ResourceEditStackSnapshot } from 'vs/platform/undoRedo/common/undoRedo'; -function createTextBufferBuilder() { - return new PieceTreeTextBufferBuilder(); -} - export function createTextBufferFactory(text: string): model.ITextBufferFactory { - const builder = createTextBufferBuilder(); + const builder = new PieceTreeTextBufferBuilder(); builder.acceptChunk(text); return builder.finish(); } @@ -65,7 +61,7 @@ export function createTextBufferFactoryFromStream(stream: ITextStream): Promise< export function createTextBufferFactoryFromStream(stream: VSBufferReadableStream): Promise<model.ITextBufferFactory>; export function createTextBufferFactoryFromStream(stream: ITextStream | VSBufferReadableStream): Promise<model.ITextBufferFactory> { return new Promise<model.ITextBufferFactory>((resolve, reject) => { - const builder = createTextBufferBuilder(); + const builder = new PieceTreeTextBufferBuilder(); let done = false; @@ -90,7 +86,7 @@ export function createTextBufferFactoryFromStream(stream: ITextStream | VSBuffer } export function createTextBufferFactoryFromSnapshot(snapshot: model.ITextSnapshot): model.ITextBufferFactory { - const builder = createTextBufferBuilder(); + const builder = new PieceTreeTextBufferBuilder(); let chunk: string | null; while (typeof (chunk = snapshot.read()) === 'string') { @@ -100,8 +96,15 @@ export function createTextBufferFactoryFromSnapshot(snapshot: model.ITextSnapsho return builder.finish(); } -export function createTextBuffer(value: string | model.ITextBufferFactory, defaultEOL: model.DefaultEndOfLine): { textBuffer: model.ITextBuffer; disposable: IDisposable } { - const factory = (typeof value === 'string' ? createTextBufferFactory(value) : value); +export function createTextBuffer(value: string | model.ITextBufferFactory | model.ITextSnapshot, defaultEOL: model.DefaultEndOfLine): { textBuffer: model.ITextBuffer; disposable: IDisposable } { + let factory: model.ITextBufferFactory; + if (typeof value === 'string') { + factory = createTextBufferFactory(value); + } else if (model.isITextSnapshot(value)) { + factory = createTextBufferFactoryFromSnapshot(value); + } else { + factory = value; + } return factory.create(defaultEOL); } @@ -424,7 +427,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati this._eventEmitter.fire(new InternalModelContentChangeEvent(rawChange, change)); } - public setValue(value: string): void { + public setValue(value: string | model.ITextSnapshot): void { this._assertNotDisposed(); if (value === null) { // There's nothing to do diff --git a/src/vs/editor/common/services/getIconClasses.ts b/src/vs/editor/common/services/getIconClasses.ts index 57a63980ce7..38f13bf83c7 100644 --- a/src/vs/editor/common/services/getIconClasses.ts +++ b/src/vs/editor/common/services/getIconClasses.ts @@ -71,7 +71,6 @@ export function getIconClasses(modelService: IModelService, languageService: ILa return classes; } - export function getIconClassesForLanguageId(languageId: string): string[] { return ['file-icon', `${cssEscape(languageId)}-lang-file-icon`]; } diff --git a/src/vs/editor/common/standalone/standaloneEnums.ts b/src/vs/editor/common/standalone/standaloneEnums.ts index 8d709286d5c..c62e7fc404f 100644 --- a/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/src/vs/editor/common/standalone/standaloneEnums.ts @@ -15,6 +15,11 @@ export enum AccessibilitySupport { Enabled = 2 } +export enum CodeActionTriggerType { + Invoke = 1, + Auto = 2 +} + export enum CompletionItemInsertTextRule { /** * Adjust whitespace/indentation of multiline insert texts to @@ -199,7 +204,7 @@ export enum EditorOption { disableMonospaceOptimizations = 29, domReadOnly = 30, dragAndDrop = 31, - enableDropIntoEditor = 32, + dropIntoEditor = 32, emptySelectionClipboard = 33, extraEditorClassName = 34, fastScrollSensitivity = 35, @@ -273,34 +278,35 @@ export enum EditorOption { smartSelect = 103, smoothScrolling = 104, stickyTabStops = 105, - stopRenderingLineAfter = 106, - suggest = 107, - suggestFontSize = 108, - suggestLineHeight = 109, - suggestOnTriggerCharacters = 110, - suggestSelection = 111, - tabCompletion = 112, - tabIndex = 113, - unicodeHighlighting = 114, - unusualLineTerminators = 115, - useShadowDOM = 116, - useTabStops = 117, - wordSeparators = 118, - wordWrap = 119, - wordWrapBreakAfterCharacters = 120, - wordWrapBreakBeforeCharacters = 121, - wordWrapColumn = 122, - wordWrapOverride1 = 123, - wordWrapOverride2 = 124, - wrappingIndent = 125, - wrappingStrategy = 126, - showDeprecated = 127, - inlayHints = 128, - editorClassName = 129, - pixelRatio = 130, - tabFocusMode = 131, - layoutInfo = 132, - wrappingInfo = 133 + stickyScroll = 106, + stopRenderingLineAfter = 107, + suggest = 108, + suggestFontSize = 109, + suggestLineHeight = 110, + suggestOnTriggerCharacters = 111, + suggestSelection = 112, + tabCompletion = 113, + tabIndex = 114, + unicodeHighlighting = 115, + unusualLineTerminators = 116, + useShadowDOM = 117, + useTabStops = 118, + wordSeparators = 119, + wordWrap = 120, + wordWrapBreakAfterCharacters = 121, + wordWrapBreakBeforeCharacters = 122, + wordWrapColumn = 123, + wordWrapOverride1 = 124, + wordWrapOverride2 = 125, + wrappingIndent = 126, + wrappingStrategy = 127, + showDeprecated = 128, + inlayHints = 129, + editorClassName = 130, + pixelRatio = 131, + tabFocusMode = 132, + layoutInfo = 133, + wrappingInfo = 134 } /** diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionMenu.ts b/src/vs/editor/contrib/codeAction/browser/codeActionMenu.ts index 69f3ec06536..f9b5b7b8547 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionMenu.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeActionMenu.ts @@ -90,7 +90,7 @@ export interface ICodeActionMenuTemplateData { } const TEMPLATE_ID = 'codeActionWidget'; -const codeActionLineHeight = 27; +const codeActionLineHeight = 26; class CodeMenuRenderer implements IListRenderer<ICodeActionMenuItem, ICodeActionMenuTemplateData> { get templateId(): string { return TEMPLATE_ID; } @@ -146,6 +146,7 @@ export class CodeActionMenu extends Disposable implements IEditorContribution { private focusedEnabledItem: number | undefined; private currSelectedItem: number = 0; private hasSeperator: boolean = false; + private block?: HTMLElement; public static readonly ID: string = 'editor.contrib.codeActionMenu'; @@ -203,6 +204,20 @@ export class CodeActionMenu extends Disposable implements IEditorContribution { const renderDisposables = new DisposableStore(); const renderMenu = document.createElement('div'); + // Render invisible div to block mouse interaction in the rest of the UI + const menuBlock = document.createElement('div'); + this.block = element.appendChild(menuBlock); + this.block.classList.add('context-view-block'); + this.block.style.position = 'fixed'; + this.block.style.cursor = 'initial'; + this.block.style.left = '0'; + this.block.style.top = '0'; + this.block.style.width = '100%'; + this.block.style.height = '100%'; + this.block.style.zIndex = '-1'; + + renderDisposables.add(dom.addDisposableListener(this.block, dom.EventType.MOUSE_DOWN, e => e.stopPropagation())); + renderMenu.id = 'codeActionMenuWidget'; renderMenu.classList.add('codeActionMenuWidget'); @@ -222,6 +237,8 @@ export class CodeActionMenu extends Disposable implements IEditorContribution { ); renderDisposables.add(this.codeActionList.value.onDidChangeSelection(e => this._onListSelection(e))); + renderDisposables.add(this._editor.onDidLayoutChange(e => this.hideCodeActionWidget())); + // Populating the list widget and tracking enabled options. inputArray.forEach((item, index) => { @@ -257,13 +274,17 @@ export class CodeActionMenu extends Disposable implements IEditorContribution { const maxWidth = Math.max(...arr); // 40 is the additional padding for the list widget (20 left, 20 right) - renderMenu.style.width = maxWidth + 40 + 'px'; + renderMenu.style.width = maxWidth + 52 + 'px'; this.codeActionList.value?.layout(height, maxWidth); // List selection - this.focusedEnabledItem = 0; - this.currSelectedItem = this.viewItems[0].index; - this.codeActionList.value.setFocus([this.currSelectedItem]); + if (this.viewItems.length < 1) { + this.currSelectedItem = 0; + } else { + this.focusedEnabledItem = 0; + this.currSelectedItem = this.viewItems[0].index; + this.codeActionList.value.setFocus([this.currSelectedItem]); + } // List Focus this.codeActionList.value.domFocus(); @@ -282,7 +303,7 @@ export class CodeActionMenu extends Disposable implements IEditorContribution { protected focusPrevious() { if (typeof this.focusedEnabledItem === 'undefined') { this.focusedEnabledItem = this.viewItems[0].index; - } else if (this.viewItems.length <= 1) { + } else if (this.viewItems.length < 1) { return false; } @@ -305,7 +326,7 @@ export class CodeActionMenu extends Disposable implements IEditorContribution { protected focusNext() { if (typeof this.focusedEnabledItem === 'undefined') { this.focusedEnabledItem = this.viewItems.length - 1; - } else if (this.viewItems.length <= 1) { + } else if (this.viewItems.length < 1) { return false; } diff --git a/src/vs/editor/contrib/codeAction/browser/media/action.css b/src/vs/editor/contrib/codeAction/browser/media/action.css index 504955b5844..a32d77522f4 100644 --- a/src/vs/editor/contrib/codeAction/browser/media/action.css +++ b/src/vs/editor/contrib/codeAction/browser/media/action.css @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ .codeActionMenuWidget { - padding: 10px 0px 10px 0px; + padding: 8px 0px 8px 0px; overflow: auto; font-size: 13px; border-radius: 5px; @@ -14,10 +14,11 @@ /* flex-direction: column; flex: 0 1 auto; */ width: 100%; - border-style: solid; - border-width: 1px; - border-color: var(--vscode-editorSuggestWidget-border); - background-color: var(--vscode-editorSuggestWidget-background); + border-width: 0px; + border-color: none; + background-color: var(--vscode-menu-background); + color: var(--vscode-menu-foreground); + box-shadow: rgb(0,0,0, 16%) 0px 2px 8px; } .codeActionMenuWidget .monaco-list:not(.element-focused):focus:before { @@ -41,7 +42,6 @@ -ms-user-select: none; border: none !important; border-width: 0px !important; - color: var(red) !important; } /* .codeActionMenuWidget .monaco-list:not(.element-focus) { @@ -62,7 +62,7 @@ display: flex; -mox-box-sizing: border-box; box-sizing: border-box; - padding: 0px 20px 0px 20px; + padding: 0px 26px 0px 26px; background-repeat: no-repeat; background-position: 2px 2px; white-space: nowrap; @@ -74,6 +74,7 @@ .codeActionMenuWidget .monaco-list .monaco-list-row:hover:not(.option-disabled), .codeActionMenuWidget .monaco-list .moncao-list-row.focused:not(.option-disabled) { + color: var(--vscode-menu-selectionForeground) !important; background-color: var(--vscode-menu-selectionBackground) !important; } @@ -86,7 +87,7 @@ -moz-user-select: none; -ms-user-select: none; user-select: none; - color: var(--vscode-list-inactiveSelectionBackground) !important; + color: var(--vscode-disabledForeground) !important; } .codeActionMenuWidget .monaco-list .separator { diff --git a/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts b/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts index 0cd4c1e73b0..650488e797f 100644 --- a/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts +++ b/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts @@ -311,7 +311,7 @@ export class ContextMenuController implements IEditorContribution { const actions: IAction[] = []; actions.push(createAction({ - label: nls.localize('context.minimap.showMinimap', "Show Minimap"), + label: nls.localize('context.minimap.minimap', "Minimap"), checked: minimapOptions.enabled, run: () => { this._configurationService.updateValue(`editor.minimap.enabled`, !minimapOptions.enabled); diff --git a/src/vs/editor/contrib/dropIntoEditor/browser/dropIntoEditorContribution.ts b/src/vs/editor/contrib/dropIntoEditor/browser/dropIntoEditorContribution.ts index 10ed525fc05..83f8ad1e314 100644 --- a/src/vs/editor/contrib/dropIntoEditor/browser/dropIntoEditorContribution.ts +++ b/src/vs/editor/contrib/dropIntoEditor/browser/dropIntoEditorContribution.ts @@ -25,7 +25,6 @@ import { CodeEditorStateFlag, EditorStateCancellationTokenSource } from 'vs/edit import { performSnippetEdit } from 'vs/editor/contrib/snippet/browser/snippetController2'; import { SnippetParser } from 'vs/editor/contrib/snippet/browser/snippetParser'; import { localize } from 'vs/nls'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; @@ -37,7 +36,6 @@ export class DropIntoEditorController extends Disposable implements IEditorContr constructor( editor: ICodeEditor, @IBulkEditService private readonly _bulkEditService: IBulkEditService, - @IConfigurationService private readonly _configurationService: IConfigurationService, @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, @IProgressService private readonly _progressService: IProgressService, @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, @@ -46,22 +44,7 @@ export class DropIntoEditorController extends Disposable implements IEditorContr this._register(editor.onDropIntoEditor(e => this.onDropIntoEditor(editor, e.position, e.event))); - this._languageFeaturesService.documentOnDropEditProvider.register('*', new DefaultOnDropProvider(workspaceContextService)); - - this._register(this._configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('workbench.editor.dropIntoEditor.enabled')) { - this.updateEditorOptions(editor); - } - })); - - this.updateEditorOptions(editor); - } - - private updateEditorOptions(editor: ICodeEditor) { - editor.updateOptions({ - enableDropIntoEditor: this._configurationService.getValue('workbench.editor.dropIntoEditor.enabled') - }); } private async onDropIntoEditor(editor: ICodeEditor, position: IPosition, dragEvent: DragEvent) { diff --git a/src/vs/editor/contrib/folding/browser/folding.css b/src/vs/editor/contrib/folding/browser/folding.css index a68daff8155..232384ce3d5 100644 --- a/src/vs/editor/contrib/folding/browser/folding.css +++ b/src/vs/editor/contrib/folding/browser/folding.css @@ -2,7 +2,8 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - +.monaco-editor .margin-view-overlays .codicon-folding-manual-collapsed, +.monaco-editor .margin-view-overlays .codicon-folding-manual-expanded, .monaco-editor .margin-view-overlays .codicon-folding-expanded, .monaco-editor .margin-view-overlays .codicon-folding-collapsed { cursor: pointer; @@ -17,6 +18,7 @@ .monaco-editor .margin-view-overlays:hover .codicon, .monaco-editor .margin-view-overlays .codicon.codicon-folding-collapsed, +.monaco-editor .margin-view-overlays .codicon.codicon-folding-manual-collapsed, .monaco-editor .margin-view-overlays .codicon.alwaysShowFoldIcons { opacity: 1; } @@ -29,3 +31,7 @@ line-height: 1em; cursor: pointer; } + +.monaco-editor .margin-view-overlays .codicon.codicon-folding-manual-expanded::before { + transform: rotate(90deg); +} diff --git a/src/vs/editor/contrib/folding/browser/folding.ts b/src/vs/editor/contrib/folding/browser/folding.ts index 64339450d95..279ab870eb5 100644 --- a/src/vs/editor/contrib/folding/browser/folding.ts +++ b/src/vs/editor/contrib/folding/browser/folding.ts @@ -31,8 +31,8 @@ import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/cont import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { editorSelectionBackground, iconForeground, registerColor, transparent } from 'vs/platform/theme/common/colorRegistry'; import { registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { foldingCollapsedIcon, FoldingDecorationProvider, foldingExpandedIcon, foldingManualIcon } from './foldingDecorations'; -import { FoldingRegion, FoldingRegions, FoldRange } from './foldingRanges'; +import { foldingCollapsedIcon, FoldingDecorationProvider, foldingExpandedIcon, foldingManualCollapsedIcon, foldingManualExpandedIcon } from './foldingDecorations'; +import { FoldingRegion, FoldingRegions, FoldRange, ILineRange } from './foldingRanges'; import { SyntaxRangeProvider } from './syntaxRangeProvider'; import { INotificationService } from 'vs/platform/notification/common/notification'; import Severity from 'vs/base/common/severity'; @@ -222,7 +222,7 @@ export class FoldingController extends Disposable implements IEditorContribution } this._currentModelHasFoldedImports = false; - this.foldingModel = new FoldingModel(model, this.foldingDecorationProvider, this.triggerFoldingModelChanged.bind(this)); + this.foldingModel = new FoldingModel(model, this.foldingDecorationProvider); this.localToDispose.add(this.foldingModel); this.hiddenRangeModel = new HiddenRangeModel(this.foldingModel); @@ -293,7 +293,7 @@ export class FoldingController extends Disposable implements IEditorContribution this.triggerFoldingModelChanged(); } - private triggerFoldingModelChanged() { + public triggerFoldingModelChanged() { if (this.updateScheduler) { if (this.foldingRegionPromise) { this.foldingRegionPromise.cancel(); @@ -1066,13 +1066,13 @@ class GotoNextFoldAction extends FoldingAction<void> { } } -class FoldSelectedAction extends FoldingAction<void> { +class FoldRangeFromSelectionAction extends FoldingAction<void> { constructor() { super({ - id: 'editor.foldSelected', - label: nls.localize('foldSelectedAction.label', "Fold Selected Lines"), - alias: 'Fold Selected Lines', + id: 'editor.createFoldingRangeFromSelection', + label: nls.localize('createManualFoldRange.label', "Create Manual Folding Range from Selection"), + alias: 'Create Folding Range from Selection', precondition: CONTEXT_FOLDING_ENABLED, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, @@ -1097,7 +1097,8 @@ class FoldSelectedAction extends FoldingAction<void> { endLineNumber: endLineNumber, type: undefined, isCollapsed: true, - isManualSelection: true + isUserDefined: true, + isRecovered: false }); editor.setSelection({ startLineNumber: selection.startLineNumber, @@ -1118,6 +1119,40 @@ class FoldSelectedAction extends FoldingAction<void> { } } +class RemoveFoldRangeFromSelectionAction extends FoldingAction<void> { + + constructor() { + super({ + id: 'editor.removeManualFoldingRanges', + label: nls.localize('removeManualFoldingRanges.label', "Remove Manual Folding Ranges"), + alias: 'Remove Manual Folding Ranges', + precondition: CONTEXT_FOLDING_ENABLED, + kbOpts: { + kbExpr: EditorContextKeys.editorTextFocus, + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.Period), + weight: KeybindingWeight.EditorContrib + } + }); + } + + invoke(foldingController: FoldingController, foldingModel: FoldingModel, editor: ICodeEditor): void { + const selections = editor.getSelections(); + if (selections) { + const ranges: ILineRange[] = []; + for (const selection of selections) { + let endLineNumber = selection.endLineNumber; + if (selection.endColumn === 1) { + --endLineNumber; + } + const startLineNumber = selection.startLineNumber; + ranges.push(endLineNumber >= selection.startLineNumber ? { startLineNumber, endLineNumber } : { endLineNumber, startLineNumber }); + } + foldingModel.removeManualRanges(ranges); + foldingController.triggerFoldingModelChanged(); + } + } +} + registerEditorContribution(FoldingController.ID, FoldingController); registerEditorAction(UnfoldAction); @@ -1135,7 +1170,8 @@ registerEditorAction(ToggleFoldAction); registerEditorAction(GotoParentFoldAction); registerEditorAction(GotoPreviousFoldAction); registerEditorAction(GotoNextFoldAction); -registerEditorAction(FoldSelectedAction); +registerEditorAction(FoldRangeFromSelectionAction); +registerEditorAction(RemoveFoldRangeFromSelectionAction); for (let i = 1; i <= 7; i++) { registerInstantiatedEditorAction( @@ -1167,7 +1203,8 @@ registerThemingParticipant((theme, collector) => { collector.addRule(` .monaco-editor .cldr${ThemeIcon.asCSSSelector(foldingExpandedIcon)}, .monaco-editor .cldr${ThemeIcon.asCSSSelector(foldingCollapsedIcon)}, - .monaco-editor .cldr${ThemeIcon.asCSSSelector(foldingManualIcon)} { + .monaco-editor .cldr${ThemeIcon.asCSSSelector(foldingManualExpandedIcon)}, + .monaco-editor .cldr${ThemeIcon.asCSSSelector(foldingManualCollapsedIcon)} { color: ${editorFoldColor} !important; } `); diff --git a/src/vs/editor/contrib/folding/browser/foldingDecorations.ts b/src/vs/editor/contrib/folding/browser/foldingDecorations.ts index 132dd19a9fa..9ad6cb77605 100644 --- a/src/vs/editor/contrib/folding/browser/foldingDecorations.ts +++ b/src/vs/editor/contrib/folding/browser/foldingDecorations.ts @@ -14,12 +14,14 @@ import { ThemeIcon } from 'vs/platform/theme/common/themeService'; export const foldingExpandedIcon = registerIcon('folding-expanded', Codicon.chevronDown, localize('foldingExpandedIcon', 'Icon for expanded ranges in the editor glyph margin.')); export const foldingCollapsedIcon = registerIcon('folding-collapsed', Codicon.chevronRight, localize('foldingCollapsedIcon', 'Icon for collapsed ranges in the editor glyph margin.')); -export const foldingManualIcon = registerIcon('folding-manual', Codicon.ellipsis, localize('foldingManualIcon', 'Icon for manually collapsed ranges in the editor glyph margin.')); +export const foldingManualCollapsedIcon = registerIcon('folding-manual-collapsed', Codicon.ellipsis, localize('foldingManualCollapedIcon', 'Icon for manually collapsed ranges in the editor glyph margin.')); +export const foldingManualExpandedIcon = registerIcon('folding-manual-expanded', Codicon.ellipsis, localize('foldingManualExpandedIcon', 'Icon for manually expanded ranges in the editor glyph margin.')); + export class FoldingDecorationProvider implements IDecorationProvider { private static readonly COLLAPSED_VISUAL_DECORATION = ModelDecorationOptions.register({ description: 'folding-collapsed-visual-decoration', - stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, afterContentClassName: 'inline-folded', isWholeLine: true, firstLineDecorationClassName: ThemeIcon.asClassName(foldingCollapsedIcon) @@ -27,7 +29,7 @@ export class FoldingDecorationProvider implements IDecorationProvider { private static readonly COLLAPSED_HIGHLIGHTED_VISUAL_DECORATION = ModelDecorationOptions.register({ description: 'folding-collapsed-highlighted-visual-decoration', - stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, afterContentClassName: 'inline-folded', className: 'folded-background', isWholeLine: true, @@ -36,19 +38,19 @@ export class FoldingDecorationProvider implements IDecorationProvider { private static readonly MANUALLY_COLLAPSED_VISUAL_DECORATION = ModelDecorationOptions.register({ description: 'folding-manually-collapsed-visual-decoration', - stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, afterContentClassName: 'inline-folded', isWholeLine: true, - firstLineDecorationClassName: ThemeIcon.asClassName(foldingManualIcon) + firstLineDecorationClassName: 'alwaysShowFoldIcons ' + ThemeIcon.asClassName(foldingExpandedIcon) }); private static readonly MANUALLY_COLLAPSED_HIGHLIGHTED_VISUAL_DECORATION = ModelDecorationOptions.register({ description: 'folding-manually-collapsed-highlighted-visual-decoration', - stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, afterContentClassName: 'inline-folded', className: 'folded-background', isWholeLine: true, - firstLineDecorationClassName: ThemeIcon.asClassName(foldingManualIcon) + firstLineDecorationClassName: ThemeIcon.asClassName(foldingManualCollapsedIcon) }); private static readonly EXPANDED_AUTO_HIDE_VISUAL_DECORATION = ModelDecorationOptions.register({ @@ -65,6 +67,21 @@ export class FoldingDecorationProvider implements IDecorationProvider { firstLineDecorationClassName: 'alwaysShowFoldIcons ' + ThemeIcon.asClassName(foldingExpandedIcon) }); + private static readonly MANUALLY_EXPANDED_VISUAL_DECORATION = ModelDecorationOptions.register({ + description: 'folding-manually-expanded-visual-decoration', + stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, + isWholeLine: true, + firstLineDecorationClassName: 'alwaysShowFoldIcons ' + ThemeIcon.asClassName(foldingManualExpandedIcon) + }); + + private static readonly MANUALLY_EXPANDED_AUTO_HIDE_VISUAL_DECORATION = ModelDecorationOptions.register({ + description: 'folding-manually-expanded-visual-decoration', + stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, + isWholeLine: true, + firstLineDecorationClassName: ThemeIcon.asClassName(foldingManualExpandedIcon) + }); + + private static readonly HIDDEN_RANGE_DECORATION = ModelDecorationOptions.register({ description: 'folding-hidden-range-decoration', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges @@ -77,19 +94,19 @@ export class FoldingDecorationProvider implements IDecorationProvider { constructor(private readonly editor: ICodeEditor) { } - getDecorationOption(isCollapsed: boolean, isHidden: boolean, isManualSelection: boolean): IModelDecorationOptions { + getDecorationOption(isCollapsed: boolean, isHidden: boolean, isManual: boolean): IModelDecorationOptions { if (isHidden // is inside another collapsed region - || this.showFoldingControls === 'never' || (isManualSelection && !isCollapsed)) { // + || this.showFoldingControls === 'never') { return FoldingDecorationProvider.HIDDEN_RANGE_DECORATION; } if (isCollapsed) { - return isManualSelection ? + return isManual ? (this.showFoldingHighlights ? FoldingDecorationProvider.MANUALLY_COLLAPSED_HIGHLIGHTED_VISUAL_DECORATION : FoldingDecorationProvider.MANUALLY_COLLAPSED_VISUAL_DECORATION) : (this.showFoldingHighlights ? FoldingDecorationProvider.COLLAPSED_HIGHLIGHTED_VISUAL_DECORATION : FoldingDecorationProvider.COLLAPSED_VISUAL_DECORATION); } else if (this.showFoldingControls === 'mouseover') { - return FoldingDecorationProvider.EXPANDED_AUTO_HIDE_VISUAL_DECORATION; + return isManual ? FoldingDecorationProvider.MANUALLY_EXPANDED_AUTO_HIDE_VISUAL_DECORATION : FoldingDecorationProvider.EXPANDED_AUTO_HIDE_VISUAL_DECORATION; } else { - return FoldingDecorationProvider.EXPANDED_VISUAL_DECORATION; + return isManual ? FoldingDecorationProvider.MANUALLY_EXPANDED_VISUAL_DECORATION : FoldingDecorationProvider.EXPANDED_VISUAL_DECORATION; } } diff --git a/src/vs/editor/contrib/folding/browser/foldingModel.ts b/src/vs/editor/contrib/folding/browser/foldingModel.ts index 59083ae1d3e..f3bc09b1ce3 100644 --- a/src/vs/editor/contrib/folding/browser/foldingModel.ts +++ b/src/vs/editor/contrib/folding/browser/foldingModel.ts @@ -9,7 +9,7 @@ import { FoldingRegion, FoldingRegions, ILineRange, FoldRange } from './foldingR import { hash } from 'vs/base/common/hash'; export interface IDecorationProvider { - getDecorationOption(isCollapsed: boolean, isHidden: boolean, isManualSelection: boolean): IModelDecorationOptions; + getDecorationOption(isCollapsed: boolean, isHidden: boolean, isManual: boolean): IModelDecorationOptions; changeDecorations<T>(callback: (changeAccessor: IModelDecorationsChangeAccessor) => T): T | null; removeDecorations(decorationIds: string[]): void; } @@ -21,6 +21,8 @@ export interface FoldingModelChangeEvent { interface ILineMemento extends ILineRange { checksum?: number; + isCollapsed?: boolean; + isUserDefined?: boolean; } export type CollapseMemento = ILineMemento[]; @@ -28,7 +30,6 @@ export type CollapseMemento = ILineMemento[]; export class FoldingModel { private readonly _textModel: ITextModel; private readonly _decorationProvider: IDecorationProvider; - private readonly _triggerRecomputeRanges: (() => void) | undefined; private _regions: FoldingRegions; private _editorDecorationIds: string[]; @@ -40,10 +41,9 @@ export class FoldingModel { public get textModel() { return this._textModel; } public get decorationProvider() { return this._decorationProvider; } - constructor(textModel: ITextModel, decorationProvider: IDecorationProvider, triggerRecomputeRanges?: () => void) { + constructor(textModel: ITextModel, decorationProvider: IDecorationProvider) { this._textModel = textModel; this._decorationProvider = decorationProvider; - this._triggerRecomputeRanges = triggerRecomputeRanges; this._regions = new FoldingRegions(new Uint32Array(0), new Uint32Array(0)); this._editorDecorationIds = []; } @@ -55,7 +55,6 @@ export class FoldingModel { toggledRegions = toggledRegions.sort((r1, r2) => r1.regionIndex - r2.regionIndex); const processed: { [key: string]: boolean | undefined } = {}; - const manualExpanded = false; this._decorationProvider.changeDecorations(accessor => { let k = 0; // index from [0 ... this.regions.length] let dirtyRegionEndLine = -1; // end of the range where decorations need to be updated @@ -64,9 +63,9 @@ export class FoldingModel { while (k < index) { const endLineNumber = this._regions.getEndLineNumber(k); const isCollapsed = this._regions.isCollapsed(k); - const isManualSelection = this.regions.isManualSelection(k); if (endLineNumber <= dirtyRegionEndLine) { - accessor.changeDecorationOptions(this._editorDecorationIds[k], this._decorationProvider.getDecorationOption(isCollapsed, endLineNumber <= lastHiddenLine, isManualSelection)); + const isManual = this.regions.isUserDefined(k) || this.regions.isRecovered(k); + accessor.changeDecorationOptions(this._editorDecorationIds[k], this._decorationProvider.getDecorationOption(isCollapsed, endLineNumber <= lastHiddenLine, isManual)); } if (isCollapsed && endLineNumber > lastHiddenLine) { lastHiddenLine = endLineNumber; @@ -91,16 +90,30 @@ export class FoldingModel { updateDecorationsUntil(this._regions.length); }); this._updateEventEmitter.fire({ model: this, collapseStateChanged: toggledRegions }); - if (manualExpanded && this._triggerRecomputeRanges) { - // expanding a range which didn't originate from range provider might now enable ranges - // from the provider which were previously dropped due to the collapsed range - this._triggerRecomputeRanges(); + } + + public removeManualRanges(ranges: ILineRange[]) { + const newFoldingRanges: FoldRange[] = new Array(); + const intersects = (foldRange: FoldRange) => { + for (const range of ranges) { + if (!(range.startLineNumber > foldRange.endLineNumber || foldRange.startLineNumber > range.endLineNumber)) { + return true; + } + } + return false; + }; + for (let i = 0; i < this._regions.length; i++) { + const foldRange = this._regions.toFoldRange(i); + if (!foldRange.isUserDefined && !foldRange.isRecovered || !intersects(foldRange)) { + newFoldingRanges.push(foldRange); + } } + this.updatePost(FoldingRegions.fromFoldRanges(newFoldingRanges)); } public update(newRegions: FoldingRegions, blockedLineNumers: number[] = []): void { - const hiddenRanges = this._currentHiddenRegions(blockedLineNumers); - const newRanges = FoldingRegions.sanitizeAndMerge(newRegions, hiddenRanges, this._textModel.getLineCount()); + const foldedOrManualRanges = this._currentFoldedOrManualRanges(blockedLineNumers); + const newRanges = FoldingRegions.sanitizeAndMerge(newRegions, foldedOrManualRanges, this._textModel.getLineCount()); this.updatePost(FoldingRegions.fromFoldRanges(newRanges)); } @@ -111,14 +124,14 @@ export class FoldingModel { const startLineNumber = newRegions.getStartLineNumber(index); const endLineNumber = newRegions.getEndLineNumber(index); const isCollapsed = newRegions.isCollapsed(index); - const isManualSelection = newRegions.isManualSelection(index); + const isManual = newRegions.isUserDefined(index) || newRegions.isRecovered(index); const decorationRange = { startLineNumber: startLineNumber, startColumn: this._textModel.getLineMaxColumn(startLineNumber), endLineNumber: endLineNumber, endColumn: this._textModel.getLineMaxColumn(endLineNumber) + 1 }; - newEditorDecorations.push({ range: decorationRange, options: this._decorationProvider.getDecorationOption(isCollapsed, endLineNumber <= lastHiddenLine, isManualSelection) }); + newEditorDecorations.push({ range: decorationRange, options: this._decorationProvider.getDecorationOption(isCollapsed, endLineNumber <= lastHiddenLine, isManual) }); if (isCollapsed && endLineNumber > lastHiddenLine) { lastHiddenLine = endLineNumber; } @@ -128,7 +141,7 @@ export class FoldingModel { this._updateEventEmitter.fire({ model: this }); } - private _currentHiddenRegions(blockedLineNumers: number[] = []): FoldRange[] { + private _currentFoldedOrManualRanges(blockedLineNumers: number[] = []): FoldRange[] { const isBlocked = (startLineNumber: number, endLineNumber: number) => { for (const blockedLineNumber of blockedLineNumers) { @@ -139,42 +152,47 @@ export class FoldingModel { return false; }; - const hiddenRanges: FoldRange[] = []; + const foldedRanges: FoldRange[] = []; for (let i = 0, limit = this._regions.length; i < limit; i++) { - if (this.regions.isCollapsed(i)) { - const hiddenRange = this._regions.toFoldRange(i); + let isCollapsed = this.regions.isCollapsed(i); + const isUserDefined = this.regions.isUserDefined(i); + const isRecovered = this.regions.isRecovered(i); + if (isCollapsed || isUserDefined || isRecovered) { + const foldRange = this._regions.toFoldRange(i); const decRange = this._textModel.getDecorationRange(this._editorDecorationIds[i]); - if (decRange - && !isBlocked(decRange.startLineNumber, decRange.endLineNumber) - // if not same length user has modified it, skip and auto-expand - && decRange.endLineNumber - decRange.startLineNumber - === hiddenRange.endLineNumber - hiddenRange.startLineNumber) { - hiddenRanges.push({ + if (decRange) { + if (isCollapsed && (isBlocked(decRange.startLineNumber, decRange.endLineNumber) || decRange.endLineNumber - decRange.startLineNumber !== foldRange.endLineNumber - foldRange.startLineNumber)) { + isCollapsed = false; // uncollapse is the range is blocked or there has been lines removed or added + } + foldedRanges.push({ startLineNumber: decRange.startLineNumber, endLineNumber: decRange.endLineNumber, - type: hiddenRange.type, - isCollapsed: true, - isManualSelection: hiddenRange.isManualSelection + type: foldRange.type, + isCollapsed, + isUserDefined, + isRecovered }); } } } - return hiddenRanges; + return foldedRanges; } /** * Collapse state memento, for persistence only */ public getMemento(): CollapseMemento | undefined { - const hiddenRegions = this._currentHiddenRegions(); + const foldedOrManualRanges = this._currentFoldedOrManualRanges(); const result: ILineMemento[] = []; - for (let i = 0, limit = hiddenRegions.length; i < limit; i++) { - const range = hiddenRegions[i]; + for (let i = 0, limit = foldedOrManualRanges.length; i < limit; i++) { + const range = foldedOrManualRanges[i]; const checksum = this._getLinesChecksum(range.startLineNumber + 1, range.endLineNumber); result.push({ startLineNumber: range.startLineNumber, endLineNumber: range.endLineNumber, + isCollapsed: range.isCollapsed, + isUserDefined: range.isRecovered, checksum: checksum }); } @@ -188,7 +206,7 @@ export class FoldingModel { if (!Array.isArray(state)) { return; } - const hiddenRanges: FoldRange[] = []; + const rangesToRestore: FoldRange[] = []; const maxLineNumber = this._textModel.getLineCount(); for (const range of state) { if (range.startLineNumber >= range.endLineNumber || range.startLineNumber < 1 || range.endLineNumber > maxLineNumber) { @@ -196,17 +214,19 @@ export class FoldingModel { } const checksum = this._getLinesChecksum(range.startLineNumber + 1, range.endLineNumber); if (!range.checksum || checksum === range.checksum) { - hiddenRanges.push({ + const isUserDefined = range.isUserDefined === true; + rangesToRestore.push({ startLineNumber: range.startLineNumber, endLineNumber: range.endLineNumber, type: undefined, - isCollapsed: true, - isManualSelection: true // converts to false when provider sends a match + isCollapsed: range.isCollapsed ?? true, + isUserDefined, + isRecovered: !isUserDefined }); } } - const newRanges = FoldingRegions.sanitizeAndMerge(this._regions, hiddenRanges, maxLineNumber); + const newRanges = FoldingRegions.sanitizeAndMerge(this._regions, rangesToRestore, maxLineNumber); this.updatePost(FoldingRegions.fromFoldRanges(newRanges)); } diff --git a/src/vs/editor/contrib/folding/browser/foldingRanges.ts b/src/vs/editor/contrib/folding/browser/foldingRanges.ts index b45c3222349..2dfb1e50003 100644 --- a/src/vs/editor/contrib/folding/browser/foldingRanges.ts +++ b/src/vs/editor/contrib/folding/browser/foldingRanges.ts @@ -13,7 +13,8 @@ export interface FoldRange { endLineNumber: number; type: string | undefined; isCollapsed: boolean; - isManualSelection: boolean; + isUserDefined: boolean; + isRecovered: boolean; } export const MAX_FOLDING_REGIONS = 0xFFFF; @@ -21,11 +22,38 @@ export const MAX_LINE_NUMBER = 0xFFFFFF; const MASK_INDENT = 0xFF000000; +class BitField { + private readonly _states: Uint32Array; + constructor(size: number) { + const numWords = Math.ceil(size / 32); + this._states = new Uint32Array(numWords); + } + + public get(index: number): boolean { + const arrayIndex = (index / 32) | 0; + const bit = index % 32; + return (this._states[arrayIndex] & (1 << bit)) !== 0; + } + + public set(index: number, newState: boolean) { + const arrayIndex = (index / 32) | 0; + const bit = index % 32; + const value = this._states[arrayIndex]; + if (newState) { + this._states[arrayIndex] = value | (1 << bit); + } else { + this._states[arrayIndex] = value & ~(1 << bit); + } + } +} + export class FoldingRegions { private readonly _startIndexes: Uint32Array; private readonly _endIndexes: Uint32Array; - private readonly _collapseStates: Uint32Array; - private readonly _manualStates: Uint32Array; + private readonly _collapseStates: BitField; + private readonly _userDefinedStates: BitField; + private readonly _recoveredStates: BitField; + private _parentsComputed: boolean; private readonly _types: Array<string | undefined> | undefined; @@ -35,9 +63,9 @@ export class FoldingRegions { } this._startIndexes = startIndexes; this._endIndexes = endIndexes; - const numWords = Math.ceil(startIndexes.length / 32); - this._collapseStates = new Uint32Array(numWords); - this._manualStates = new Uint32Array(numWords); + this._collapseStates = new BitField(startIndexes.length); + this._userDefinedStates = new BitField(startIndexes.length); + this._recoveredStates = new BitField(startIndexes.length); this._types = types; this._parentsComputed = false; } @@ -88,37 +116,27 @@ export class FoldingRegions { } public isCollapsed(index: number): boolean { - const arrayIndex = (index / 32) | 0; - const bit = index % 32; - return (this._collapseStates[arrayIndex] & (1 << bit)) !== 0; + return this._collapseStates.get(index); } public setCollapsed(index: number, newState: boolean) { - const arrayIndex = (index / 32) | 0; - const bit = index % 32; - const value = this._collapseStates[arrayIndex]; - if (newState) { - this._collapseStates[arrayIndex] = value | (1 << bit); - } else { - this._collapseStates[arrayIndex] = value & ~(1 << bit); - } + this._collapseStates.set(index, newState); } - public isManualSelection(index: number): boolean { - const arrayIndex = (index / 32) | 0; - const bit = index % 32; - return (this._manualStates[arrayIndex] & (1 << bit)) !== 0; + public isUserDefined(index: number): boolean { + return this._userDefinedStates.get(index); } - public setManualSelection(index: number, newState: boolean) { - const arrayIndex = (index / 32) | 0; - const bit = index % 32; - const value = this._manualStates[arrayIndex]; - if (newState) { - this._manualStates[arrayIndex] = value | (1 << bit); - } else { - this._manualStates[arrayIndex] = value & ~(1 << bit); - } + public setUserDefined(index: number, newState: boolean) { + return this._userDefinedStates.set(index, newState); + } + + public isRecovered(index: number): boolean { + return this._recoveredStates.get(index); + } + + public setRecovered(index: number, newState: boolean) { + return this._recoveredStates.set(index, newState); } public setCollapsedAllOfType(type: string, newState: boolean) { @@ -188,7 +206,7 @@ export class FoldingRegions { public toString() { const res: string[] = []; for (let i = 0; i < this.length; i++) { - res[i] = `[${this.isManualSelection(i) ? '*' : ' '}${this.isCollapsed(i) ? '+' : '-'}] ${this.getStartLineNumber(i)}/${this.getEndLineNumber(i)}`; + res[i] = `[${this.isUserDefined(i) ? '*' : ' '}${this.isRecovered(i) ? 'r' : ' '}${this.isCollapsed(i) ? '+' : '-'}] ${this.getStartLineNumber(i)}/${this.getEndLineNumber(i)}`; } return res.join(', '); } @@ -199,7 +217,8 @@ export class FoldingRegions { endLineNumber: this._endIndexes[index] & MAX_LINE_NUMBER, type: this._types ? this._types[index] : undefined, isCollapsed: this.isCollapsed(index), - isManualSelection: this.isManualSelection(index) + isUserDefined: this.isUserDefined(index), + isRecovered: this.isRecovered(index) }; } @@ -226,8 +245,11 @@ export class FoldingRegions { if (ranges[i].isCollapsed) { regions.setCollapsed(i, true); } - if (ranges[i].isManualSelection) { - regions.setManualSelection(i, true); + if (ranges[i].isUserDefined) { + regions.setUserDefined(i, true); + } + if (ranges[i].isRecovered) { + regions.setRecovered(i, true); } } return regions; @@ -237,11 +259,10 @@ export class FoldingRegions { * Two inputs, each a FoldingRegions or a FoldRange[], are merged. * Each input must be pre-sorted on startLineNumber. * The first list is assumed to always include all regions currently defined by range providers. - * The second list only contains hidden ranges. + * The second list only contains the previously collapsed and all manual ranges. + * If the line position matches, the range of the new range is taken, and the range is no longer manual * When an entry in one list overlaps an entry in the other, the second list's entry "wins" and - * overlapping entries in the first list are discarded. With one exception: when there is just - * one such second list entry and it is not manual it is discarded, on the assumption that - * user editing has resulted in the range no longer existing. + * overlapping entries in the first list are discarded. * Invalid entries are discarded. An entry is invalid if: * the start and end line numbers aren't a valid range of line numbers, * it is out of sequence or has the same start line as a preceding entry, @@ -252,18 +273,6 @@ export class FoldingRegions { rangesB: FoldingRegions | FoldRange[], maxLineNumber: number | undefined): FoldRange[] { maxLineNumber = maxLineNumber ?? Number.MAX_VALUE; - let result = this._trySanitizeAndMerge(1, rangesA, rangesB, maxLineNumber); - if (!result) { // try again, converting hidden ranges to manually selected - result = this._trySanitizeAndMerge(2, rangesA, rangesB, maxLineNumber); - } - return result!; - } - - private static _trySanitizeAndMerge( - passNumber: number, // it can take two passes to get this done - rangesA: FoldingRegions | FoldRange[], - rangesB: FoldingRegions | FoldRange[], - maxLineNumber: number): FoldRange[] | null { const getIndexedFunction = (r: FoldingRegions | FoldRange[], limit: number) => { return Array.isArray(r) @@ -281,41 +290,34 @@ export class FoldingRegions { let topStackedRange: FoldRange | undefined; let prevLineNumber = 0; const resultRanges: FoldRange[] = []; - let numberAutoExpand = 0; while (nextA || nextB) { let useRange: FoldRange | undefined = undefined; if (nextB && (!nextA || nextA.startLineNumber >= nextB.startLineNumber)) { - // nextB is next - if (nextA - && nextA.startLineNumber === nextB.startLineNumber - && nextA.endLineNumber === nextB.endLineNumber) { - // same range in both lists, merge the details - useRange = nextB; - useRange.isCollapsed = useRange.isCollapsed || nextA.isCollapsed; - // next line removes manual flag when range provider has matching range - useRange.isManualSelection = nextA.isManualSelection && nextB.isManualSelection; - if (!useRange.type) { - useRange.type = nextA.type; + if (nextA && nextA.startLineNumber === nextB.startLineNumber) { + if (nextB.isUserDefined) { + // a user defined range (possibly unfolded) + useRange = nextB; + } else { + // a previously folded range or a (possibly unfolded) recovered range + useRange = nextA; + useRange.isCollapsed = nextB.isCollapsed && nextA.endLineNumber === nextB.endLineNumber; + useRange.isUserDefined = false; + useRange.isRecovered = false; } nextA = getA(++indexA); // not necessary, just for speed - } else if (nextB.isCollapsed && !nextB.isManualSelection && passNumber === 1) { - if (++numberAutoExpand > 1) { - // do second pass keeping these, assuming something like an unmatched /* - return null; - } - // skip nextB (auto expand) by not setting useRange, assuming it was edited - } else { // use nextB + } else { useRange = nextB; - if (useRange.isCollapsed) { - // doesn't match nextA, convert to a manual selection if it wasn't already - useRange.isManualSelection = true; + if (nextB.isCollapsed && !nextB.isUserDefined && !nextB.isRecovered) { + // a previously collapsed range + useRange.isRecovered = true; + useRange.isUserDefined = false; } } nextB = getB(++indexB); } else { - // nextA is next. The B set takes precedence and we sometimes need to look + // nextA is next. The user folded B set takes precedence and we sometimes need to look // ahead in it to check for an upcoming conflict. let scanIndex = indexB; let prescanB = nextB; @@ -324,8 +326,8 @@ export class FoldingRegions { useRange = nextA; break; // no conflict, use this nextA } - if (prescanB.endLineNumber > nextA!.endLineNumber - && (!prescanB.isCollapsed || prescanB.isManualSelection || passNumber === 2)) { + if (prescanB.isUserDefined && prescanB.endLineNumber > nextA!.endLineNumber) { + // we found a user folded range, it wins break; // without setting nextResult, so this nextA gets skipped } prescanB = getB(++scanIndex); diff --git a/src/vs/editor/contrib/folding/test/browser/foldingRanges.test.ts b/src/vs/editor/contrib/folding/test/browser/foldingRanges.test.ts index a1979da3c22..671277579ee 100644 --- a/src/vs/editor/contrib/folding/test/browser/foldingRanges.test.ts +++ b/src/vs/editor/contrib/folding/test/browser/foldingRanges.test.ts @@ -14,22 +14,30 @@ const markers: FoldingMarkers = { end: /^\s*#endregion\b/ }; +enum State { + none = 0, + userDefined = 1, + recovered = 2 +} + suite('FoldingRanges', () => { - const foldRange = (from: number, to: number, collapsed: boolean | undefined = undefined, manual: boolean | undefined = undefined, type: string | undefined = undefined) => + const foldRange = (from: number, to: number, collapsed: boolean | undefined = undefined, state: State = State.none, type: string | undefined = undefined) => <FoldRange>{ startLineNumber: from, endLineNumber: to, type: type, isCollapsed: collapsed || false, - isManualSelection: manual || false + isUserDefined: state === State.userDefined, + isRecovered: state === State.recovered, }; const assertEqualRanges = (range1: FoldRange, range2: FoldRange, msg: string) => { assert.strictEqual(range1.startLineNumber, range2.startLineNumber, msg + ' start'); assert.strictEqual(range1.endLineNumber, range2.endLineNumber, msg + ' end'); assert.strictEqual(range1.type, range2.type, msg + ' type'); assert.strictEqual(range1.isCollapsed, range2.isCollapsed, msg + ' collapsed'); - assert.strictEqual(range1.isManualSelection, range2.isManualSelection, msg + ' manual'); + assert.strictEqual(range1.isUserDefined, range2.isUserDefined, msg + ' userDefined'); + assert.strictEqual(range1.isRecovered, range2.isRecovered, msg + ' recovered'); }; test('test max folding regions', () => { @@ -122,110 +130,88 @@ suite('FoldingRanges', () => { test('sanitizeAndMerge1', () => { const regionSet1: FoldRange[] = [ foldRange(0, 100), // invalid, should be removed - foldRange(1, 100, false, false, 'A'), // valid - foldRange(1, 100, false, false, 'Z'), // invalid, duplicate start + foldRange(1, 100, false, State.none, 'A'), // valid + foldRange(1, 100, false, State.none, 'Z'), // invalid, duplicate start foldRange(10, 10, false), // invalid, should be removed - foldRange(20, 80, false, false, 'C1'), // valid inside 'B' - foldRange(22, 80, true, false, 'D1'), // valid inside 'C1' + foldRange(20, 80, false, State.none, 'C1'), // valid inside 'B' + foldRange(22, 80, true, State.none, 'D1'), // valid inside 'C1' foldRange(90, 101), // invalid, should be removed ]; const regionSet2: FoldRange[] = [ - foldRange(2, 100, false, false, 'B'), // valid, inside 'A' foldRange(20, 80, true), // should merge with C1 foldRange(18, 80, true), // invalid, out of order - foldRange(21, 81, true, false, 'Z'), // invalid, overlapping - foldRange(22, 80, false, false, 'D2'), // should merge with D1 + foldRange(21, 81, true, State.none, 'Z'), // invalid, overlapping + foldRange(22, 80, true, State.none, 'D2'), // should merge with D1 ]; - let result = FoldingRegions.sanitizeAndMerge(regionSet1, regionSet2, 100); - assert.strictEqual(result.length, 4, 'result length1'); - assertEqualRanges(result[0], foldRange(1, 100, false, false, 'A'), 'A1'); - assertEqualRanges(result[1], foldRange(2, 100, false, false, 'B'), 'B1'); - assertEqualRanges(result[2], foldRange(20, 80, true, false, 'C1'), 'C1'); - assertEqualRanges(result[3], foldRange(22, 80, true, false, 'D2'), 'D1'); - const regionClass1 = FoldingRegions.fromFoldRanges(regionSet1); - const regionClass2 = FoldingRegions.fromFoldRanges(regionSet2); - // same tests again with inputs as FoldingRegions instead of FoldRange[] - result = FoldingRegions.sanitizeAndMerge(regionClass1, regionClass2, 100); - assert.strictEqual(result.length, 4, 'result length2'); - assertEqualRanges(result[0], foldRange(1, 100, false, false, 'A'), 'A2'); - assertEqualRanges(result[1], foldRange(2, 100, false, false, 'B'), 'B2'); - assertEqualRanges(result[2], foldRange(20, 80, true, false, 'C1'), 'C2'); - assertEqualRanges(result[3], foldRange(22, 80, true, false, 'D2'), 'D2'); + const result = FoldingRegions.sanitizeAndMerge(regionSet1, regionSet2, 100); + assert.strictEqual(result.length, 3, 'result length1'); + assertEqualRanges(result[0], foldRange(1, 100, false, State.none, 'A'), 'A1'); + assertEqualRanges(result[1], foldRange(20, 80, true, State.none, 'C1'), 'C1'); + assertEqualRanges(result[2], foldRange(22, 80, true, State.none, 'D1'), 'D1'); }); test('sanitizeAndMerge2', () => { const regionSet1: FoldRange[] = [ - foldRange(1, 100, false, false, 'a1'), // valid - foldRange(2, 100, false, false, 'a2'), // valid - foldRange(3, 19, false, false, 'a3'), // valid - foldRange(20, 71, false, false, 'a4'), // overlaps b3 - foldRange(21, 29, false, false, 'a5'), // valid - foldRange(81, 91, false, false, 'a6'), // overlaps b4 + foldRange(1, 100, false, State.none, 'a1'), // valid + foldRange(2, 100, false, State.none, 'a2'), // valid + foldRange(3, 19, false, State.none, 'a3'), // valid + foldRange(20, 71, false, State.none, 'a4'), // overlaps b3 + foldRange(21, 29, false, State.none, 'a5'), // valid + foldRange(81, 91, false, State.none, 'a6'), // overlaps b4 ]; const regionSet2: FoldRange[] = [ - foldRange(30, 39, false, false, 'b1'), // valid - foldRange(40, 49, false, false, 'b2'), // valid - foldRange(50, 100, false, false, 'b3'), // overlaps a4 - foldRange(80, 90, false, false, 'b4'), // overlaps a6 - foldRange(92, 100, false, false, 'b5'), // valid + foldRange(30, 39, true, State.none, 'b1'), // valid, will be recovered + foldRange(40, 49, true, State.userDefined, 'b2'), // valid + foldRange(50, 100, true, State.userDefined, 'b3'), // overlaps a4 + foldRange(80, 90, true, State.userDefined, 'b4'), // overlaps a6 + foldRange(92, 100, true, State.userDefined, 'b5'), // valid ]; - let result = FoldingRegions.sanitizeAndMerge(regionSet1, regionSet2, 100); + const result = FoldingRegions.sanitizeAndMerge(regionSet1, regionSet2, 100); assert.strictEqual(result.length, 9, 'result length1'); - assertEqualRanges(result[0], foldRange(1, 100, false, false, 'a1'), 'P1'); - assertEqualRanges(result[1], foldRange(2, 100, false, false, 'a2'), 'P2'); - assertEqualRanges(result[2], foldRange(3, 19, false, false, 'a3'), 'P3'); - assertEqualRanges(result[3], foldRange(21, 29, false, false, 'a5'), 'P4'); - assertEqualRanges(result[4], foldRange(30, 39, false, false, 'b1'), 'P5'); - assertEqualRanges(result[5], foldRange(40, 49, false, false, 'b2'), 'P6'); - assertEqualRanges(result[6], foldRange(50, 100, false, false, 'b3'), 'P7'); - assertEqualRanges(result[7], foldRange(80, 90, false, false, 'b4'), 'P8'); - assertEqualRanges(result[8], foldRange(92, 100, false, false, 'b5'), 'P9'); - // reverse the two inputs - result = FoldingRegions.sanitizeAndMerge(regionSet2, regionSet1, 100); - assert.strictEqual(result.length, 9, 'result length2'); - assertEqualRanges(result[0], foldRange(1, 100, false, false, 'a1'), 'Q1'); - assertEqualRanges(result[1], foldRange(2, 100, false, false, 'a2'), 'Q2'); - assertEqualRanges(result[2], foldRange(3, 19, false, false, 'a3'), 'Q3'); - assertEqualRanges(result[3], foldRange(20, 71, false, false, 'a4'), 'Q4'); - assertEqualRanges(result[4], foldRange(21, 29, false, false, 'a5'), 'Q5'); - assertEqualRanges(result[5], foldRange(30, 39, false, false, 'b1'), 'Q6'); - assertEqualRanges(result[6], foldRange(40, 49, false, false, 'b2'), 'Q7'); - assertEqualRanges(result[7], foldRange(81, 91, false, false, 'a6'), 'Q8'); - assertEqualRanges(result[8], foldRange(92, 100, false, false, 'b5'), 'Q9'); + assertEqualRanges(result[0], foldRange(1, 100, false, State.none, 'a1'), 'P1'); + assertEqualRanges(result[1], foldRange(2, 100, false, State.none, 'a2'), 'P2'); + assertEqualRanges(result[2], foldRange(3, 19, false, State.none, 'a3'), 'P3'); + assertEqualRanges(result[3], foldRange(21, 29, false, State.none, 'a5'), 'P4'); + assertEqualRanges(result[4], foldRange(30, 39, true, State.recovered, 'b1'), 'P5'); + assertEqualRanges(result[5], foldRange(40, 49, true, State.userDefined, 'b2'), 'P6'); + assertEqualRanges(result[6], foldRange(50, 100, true, State.userDefined, 'b3'), 'P7'); + assertEqualRanges(result[7], foldRange(80, 90, true, State.userDefined, 'b4'), 'P8'); + assertEqualRanges(result[8], foldRange(92, 100, true, State.userDefined, 'b5'), 'P9'); }); test('sanitizeAndMerge3', () => { const regionSet1: FoldRange[] = [ - foldRange(1, 100, false, false, 'a1'), // valid - foldRange(10, 29, false, false, 'a2'), // matches manual hidden - foldRange(35, 39, true, true, 'a3'), // valid + foldRange(1, 100, false, State.none, 'a1'), // valid + foldRange(10, 29, false, State.none, 'a2'), // matches manual hidden + foldRange(35, 39, true, State.recovered, 'a3'), // valid ]; const regionSet2: FoldRange[] = [ - foldRange(10, 29, true, true, 'b1'), // matches a - foldRange(20, 28, true, false, 'b2'), // should get dropped - foldRange(30, 39, true, true, 'b3'), // should remain + foldRange(10, 29, true, State.recovered, 'b1'), // matches a + foldRange(20, 28, true, State.none, 'b2'), // should remain + foldRange(30, 39, true, State.recovered, 'b3'), // should remain ]; const result = FoldingRegions.sanitizeAndMerge(regionSet1, regionSet2, 100); - assert.strictEqual(result.length, 4, 'result length3'); - assertEqualRanges(result[0], foldRange(1, 100, false, false, 'a1'), 'R1'); - assertEqualRanges(result[1], foldRange(10, 29, true, false, 'b1'), 'R2'); - assertEqualRanges(result[2], foldRange(30, 39, true, true, 'b3'), 'R3'); - assertEqualRanges(result[3], foldRange(35, 39, true, true, 'a3'), 'R4'); + assert.strictEqual(result.length, 5, 'result length3'); + assertEqualRanges(result[0], foldRange(1, 100, false, State.none, 'a1'), 'R1'); + assertEqualRanges(result[1], foldRange(10, 29, true, State.none, 'a2'), 'R2'); + assertEqualRanges(result[2], foldRange(20, 28, true, State.recovered, 'b2'), 'R3'); + assertEqualRanges(result[3], foldRange(30, 39, true, State.recovered, 'b3'), 'R3'); + assertEqualRanges(result[4], foldRange(35, 39, true, State.recovered, 'a3'), 'R4'); }); test('sanitizeAndMerge4', () => { const regionSet1: FoldRange[] = [ - foldRange(1, 100, false, false, 'a1'), // valid + foldRange(1, 100, false, State.none, 'a1'), // valid ]; const regionSet2: FoldRange[] = [ - foldRange(20, 28, true, false, 'b1'), // hidden - foldRange(30, 38, true, false, 'b2'), // hidden + foldRange(20, 28, true, State.none, 'b1'), // hidden + foldRange(30, 38, true, State.none, 'b2'), // hidden ]; const result = FoldingRegions.sanitizeAndMerge(regionSet1, regionSet2, 100); assert.strictEqual(result.length, 3, 'result length4'); - assertEqualRanges(result[0], foldRange(1, 100, false, false, 'a1'), 'R1'); - assertEqualRanges(result[1], foldRange(20, 28, true, true, 'b1'), 'R2'); - assertEqualRanges(result[2], foldRange(30, 38, true, true, 'b2'), 'R3'); + assertEqualRanges(result[0], foldRange(1, 100, false, State.none, 'a1'), 'R1'); + assertEqualRanges(result[1], foldRange(20, 28, true, State.recovered, 'b1'), 'R2'); + assertEqualRanges(result[2], foldRange(30, 38, true, State.recovered, 'b2'), 'R3'); }); }); diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.ts new file mode 100644 index 00000000000..8de7e41b395 --- /dev/null +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.ts @@ -0,0 +1,357 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { IActiveCodeEditor, ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from 'vs/editor/browser/editorBrowser'; +import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { IEditorContribution } from 'vs/editor/common/editorCommon'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { OutlineModel, OutlineElement } from 'vs/editor/contrib/documentSymbols/browser/outlineModel'; +import { CancellationToken, CancellationTokenSource, } from 'vs/base/common/cancellation'; +import * as dom from 'vs/base/browser/dom'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { createStringBuilder } from 'vs/editor/common/core/stringBuilder'; +import { RenderLineInput, renderViewLine } from 'vs/editor/common/viewLayout/viewLineRenderer'; +import { SymbolKind } from 'vs/editor/common/languages'; +import { LineDecoration } from 'vs/editor/common/viewLayout/lineDecorations'; + +const enum ScrollDirection { + Down = 0, + Up = 1, + None = 2 +} + +class StickyScrollController implements IEditorContribution { + + static readonly ID = 'store.contrib.stickyScrollController'; + private readonly _editor: ICodeEditor; + private readonly stickyScrollWidget: StickyScrollWidget; + private readonly _languageFeaturesService: ILanguageFeaturesService; + + private readonly _store: DisposableStore = new DisposableStore(); + private readonly _sessionStore: DisposableStore = new DisposableStore(); + + private _ranges: [number, number, number][] = []; + private _cts: CancellationTokenSource | undefined; + private _lastScrollPosition: number = -1; + + constructor( + editor: ICodeEditor, + @ILanguageFeaturesService _languageFeaturesService: ILanguageFeaturesService, + ) { + this._editor = editor; + this._languageFeaturesService = _languageFeaturesService; + this.stickyScrollWidget = new StickyScrollWidget(this._editor); + this._store.add(this._editor.onDidChangeConfiguration(e => { + if (e.hasChanged(EditorOption.stickyScroll)) { + this.onConfigurationChange(); + } + })); + this.onConfigurationChange(); + } + + private onConfigurationChange() { + const options = this._editor.getOption(EditorOption.stickyScroll); + if (options.enabled === false) { + this.stickyScrollWidget.emptyRootNode(); + this._editor.removeOverlayWidget(this.stickyScrollWidget); + this._sessionStore.clear(); + return; + } else { + this._editor.addOverlayWidget(this.stickyScrollWidget); + this._sessionStore.add(this._editor.onDidChangeModel(() => this._update(true))); + this._sessionStore.add(this._editor.onDidScrollChange(() => this._update(false))); + this._sessionStore.add(this._editor.onDidChangeModelContent(() => this._update(true))); + this._sessionStore.add(this._languageFeaturesService.documentSymbolProvider.onDidChange(() => this._update(true))); + this._update(true); + } + } + + private async _update(updateOutline: boolean = false): Promise<void> { + if (updateOutline) { + this._cts?.dispose(true); + this._cts = new CancellationTokenSource(); + await this._updateOutlineModel(this._cts.token); + } + this._renderStickyScroll(); + } + + private _findLineRanges(outlineElement: OutlineElement, depth: number) { + if (outlineElement?.children.size) { + let didRecursion: boolean = false; + for (const outline of outlineElement?.children.values()) { + const kind: SymbolKind = outline.symbol.kind; + if (kind === SymbolKind.Class || kind === SymbolKind.Constructor || kind === SymbolKind.Function || kind === SymbolKind.Interface || kind === SymbolKind.Method) { + didRecursion = true; + this._findLineRanges(outline, depth + 1); + } + } + if (!didRecursion) { + this._addOutlineRanges(outlineElement, depth); + } + } else { + this._addOutlineRanges(outlineElement, depth); + } + } + + private _addOutlineRanges(outlineElement: OutlineElement, depth: number) { + let currentStartLine: number | undefined = 0; + let currentEndLine: number | undefined = 0; + + while (outlineElement) { + const kind: SymbolKind = outlineElement.symbol.kind; + if (kind === SymbolKind.Class || kind === SymbolKind.Constructor || kind === SymbolKind.Function || kind === SymbolKind.Interface || kind === SymbolKind.Method) { + currentStartLine = outlineElement?.symbol.range.startLineNumber as number; + currentEndLine = outlineElement?.symbol.range.endLineNumber as number; + this._ranges.push([currentStartLine, currentEndLine, depth]); + depth--; + } + if (outlineElement.parent instanceof OutlineElement) { + outlineElement = outlineElement.parent; + } else { + break; + } + } + } + + private async _updateOutlineModel(token: CancellationToken) { + if (this._editor.hasModel()) { + const model = this._editor.getModel(); + const outlineModel = await OutlineModel.create(this._languageFeaturesService.documentSymbolProvider, model, token); + if (token.isCancellationRequested) { + return; + } + this._ranges = []; + for (const outline of outlineModel.children.values()) { + if (outline instanceof OutlineElement) { + const kind: SymbolKind = outline.symbol.kind; + if (kind === SymbolKind.Class || kind === SymbolKind.Constructor || kind === SymbolKind.Function || kind === SymbolKind.Interface || kind === SymbolKind.Method) { + this._findLineRanges(outline, 1); + } else { + this._findLineRanges(outline, 0); + } + } + this._ranges = this._ranges.sort(function (a, b) { + if (a[0] !== b[0]) { + return a[0] - b[0]; + } else if (a[1] !== b[1]) { + return b[1] - a[1]; + } else { + return a[2] - b[2]; + } + }); + let previous: number[] = []; + for (const [index, arr] of this._ranges.entries()) { + const [start, end, _depth] = arr; + if (previous[0] === start && previous[1] === end) { + this._ranges.splice(index, 1); + } else { + previous = arr; + } + } + } + } + } + private _renderStickyScroll() { + + if (!(this._editor.hasModel())) { + return; + } + + const lineHeight: number = this._editor.getOption(EditorOption.lineHeight); + const model = this._editor.getModel(); + + const scrollTop = this._editor.getScrollTop(); + let scrollDirection: ScrollDirection; + if (this._lastScrollPosition < scrollTop) { + scrollDirection = ScrollDirection.Down; + } else { + scrollDirection = ScrollDirection.Up; + } + this._lastScrollPosition = scrollTop; + + const scrollToBottomOfWidget = this._editor.getScrollTop() + this.stickyScrollWidget.codeLineCount * lineHeight; + this.stickyScrollWidget.emptyRootNode(); + const beginningLinesConsidered: Set<number> = new Set<number>(); + let topOfElementAtDepth: number; + let bottomOfElementAtDepth: number; + let bottomOfBeginningLine: number; + let topOfEndLine: number; + let bottomOfEndLine: number; + + for (const [index, arr] of this._ranges.entries()) { + const [start, end, depth] = arr; + topOfElementAtDepth = this._editor.getScrollTop() + (depth - 1) * lineHeight; + bottomOfElementAtDepth = this._editor.getScrollTop() + depth * lineHeight; + bottomOfBeginningLine = start * lineHeight; + topOfEndLine = (end - 1) * lineHeight; + bottomOfEndLine = end * lineHeight; + + if (!beginningLinesConsidered.has(start)) { + if (topOfElementAtDepth >= topOfEndLine - 1 && topOfElementAtDepth < bottomOfEndLine - 2) { + beginningLinesConsidered.add(start); + this.stickyScrollWidget.pushCodeLine(new StickyScrollCodeLine(model.getLineContent(start), start, this._editor, -1, (depth - 1) * lineHeight + bottomOfEndLine - bottomOfElementAtDepth)); + break; + } + else if (scrollDirection === ScrollDirection.Down && bottomOfElementAtDepth > bottomOfBeginningLine - 1 && bottomOfElementAtDepth < bottomOfEndLine - 1) { + beginningLinesConsidered.add(start); + this.stickyScrollWidget.pushCodeLine(new StickyScrollCodeLine(model.getLineContent(start), start, this._editor, 0)); + + } else if (scrollDirection === ScrollDirection.Up && scrollToBottomOfWidget > bottomOfBeginningLine - 1 && scrollToBottomOfWidget < bottomOfEndLine || + scrollDirection === ScrollDirection.Up && bottomOfElementAtDepth > bottomOfBeginningLine && bottomOfElementAtDepth < topOfEndLine - 1) { + beginningLinesConsidered.add(start); + this.stickyScrollWidget.pushCodeLine(new StickyScrollCodeLine(model.getLineContent(start), start, this._editor, 0)); + } + } else { + this._ranges.splice(index, 1); + } + } + + this.stickyScrollWidget.updateRootNode(); + } + + dispose(): void { + this._store.dispose(); + this._sessionStore.dispose(); + } +} + +const _ttPolicy = window.trustedTypes?.createPolicy('stickyScrollViewLayer', { createHTML: value => value }); + +class StickyScrollCodeLine { + constructor(private readonly _line: string, private readonly _lineNumber: number, private readonly _editor: IActiveCodeEditor, + private readonly _zIndex: number, private readonly _position?: number) { } + + + getDomNode() { + + const root: HTMLElement = document.createElement('div'); + const modifiedLine = this._line.replace(/\s/g, '\xa0'); + const lineRenderingData = this._editor._getViewModel().getViewLineRenderingData(this._editor.getVisibleRangesPlusViewportAboveBelow()[0], this._lineNumber); + let actualInlineDecorations: LineDecoration[]; + try { + actualInlineDecorations = LineDecoration.filter(lineRenderingData.inlineDecorations, this._lineNumber, lineRenderingData.minColumn, lineRenderingData.maxColumn); + } catch (err) { + console.log(err); + actualInlineDecorations = []; + } + const renderLineInput: RenderLineInput = new RenderLineInput(true, true, modifiedLine, lineRenderingData.continuesWithWrappedLine, + lineRenderingData.isBasicASCII, lineRenderingData.containsRTL, 0, lineRenderingData.tokens, actualInlineDecorations, lineRenderingData.tabSize, + lineRenderingData.startVisibleColumn, 1, 1, 1, 100, 'none', true, true, null); + + const sb = createStringBuilder(2000); + renderViewLine(renderLineInput, sb); + + let newLine; + if (_ttPolicy) { + newLine = _ttPolicy.createHTML(sb.build() as string); + } else { + newLine = sb.build(); + } + + const lineHTMLNode = document.createElement('div'); + lineHTMLNode.style.paddingLeft = this._editor.getLayoutInfo().contentLeft - this._editor.getLayoutInfo().lineNumbersLeft - this._editor.getLayoutInfo().lineNumbersWidth + 'px'; + lineHTMLNode.style.float = 'left'; + lineHTMLNode.style.width = this._editor.getLayoutInfo().width - this._editor.getLayoutInfo().contentLeft + 'px'; + lineHTMLNode.style.backgroundColor = `var(--vscode-editorStickyScroll-background)`; + lineHTMLNode.innerHTML = newLine as string; + + const lineNumberHTMLNode = document.createElement('div'); + lineNumberHTMLNode.style.width = this._editor.getLayoutInfo().contentLeft.toString() + 'px'; + lineNumberHTMLNode.style.backgroundColor = `var(--vscode-editorStickyScroll-background)`; + lineNumberHTMLNode.style.color = 'var(--vscode-editorLineNumber-foreground)'; + + const innerLineNumberHTML = document.createElement('div'); + innerLineNumberHTML.innerText = this._lineNumber.toString(); + innerLineNumberHTML.style.paddingLeft = this._editor.getLayoutInfo().lineNumbersLeft.toString() + 'px'; + innerLineNumberHTML.style.width = this._editor.getLayoutInfo().lineNumbersWidth.toString() + 'px'; + innerLineNumberHTML.style.backgroundColor = `var(--vscode-editorStickyScroll-background)`; + innerLineNumberHTML.style.textAlign = 'right'; + innerLineNumberHTML.style.float = 'left'; + lineNumberHTMLNode.appendChild(innerLineNumberHTML); + + lineHTMLNode.onclick = e => { + e.stopPropagation(); + e.preventDefault(); + this._editor.revealLine(this._lineNumber); + }; + lineHTMLNode.onmouseover = e => { + innerLineNumberHTML.style.background = `var(--vscode-editorStickyScrollHover-background)`; + lineHTMLNode.style.backgroundColor = `var(--vscode-editorStickyScrollHover-background)`; + innerLineNumberHTML.style.cursor = `pointer`; + lineHTMLNode.style.cursor = `pointer`; + }; + lineHTMLNode.onmouseleave = e => { + innerLineNumberHTML.style.background = `var(--vscode-editorStickyScroll-background)`; + lineHTMLNode.style.backgroundColor = `var(--vscode-editorStickyScroll-background)`; + }; + + this._editor.applyFontInfo(lineHTMLNode); + this._editor.applyFontInfo(innerLineNumberHTML); + + root.appendChild(lineNumberHTMLNode); + root.appendChild(lineHTMLNode); + + root.style.zIndex = this._zIndex.toString(); + root.style.backgroundColor = `var(--vscode-editorStickyScroll-background)`; + + // Special case for last line of sticky scroll + if (this._position) { + root.style.position = 'absolute'; + root.style.top = this._position + 'px'; + root.style.width = '100%'; + } + return root; + } +} + +class StickyScrollWidget implements IOverlayWidget { + + private readonly arrayOfCodeLines: StickyScrollCodeLine[] = []; + private readonly rootDomNode: HTMLElement = document.createElement('div'); + + constructor(public readonly _editor: ICodeEditor) { + this.rootDomNode = document.createElement('div'); + this.rootDomNode.style.width = '100%'; + } + + get codeLineCount() { + return this.arrayOfCodeLines.length; + } + + pushCodeLine(codeLine: StickyScrollCodeLine) { + this.arrayOfCodeLines.push(codeLine); + } + + updateRootNode() { + for (const line of this.arrayOfCodeLines) { + this.rootDomNode.appendChild(line.getDomNode()); + } + } + + emptyRootNode() { + this.arrayOfCodeLines.length = 0; + dom.clearNode(this.rootDomNode); + } + + getId(): string { + return 'editor.contrib.stickyScrollWidget'; + } + + getDomNode(): HTMLElement { + this.rootDomNode.style.zIndex = '2'; + this.rootDomNode.style.backgroundColor = `var(--vscode-editorStickyScroll-background)`; + return this.rootDomNode; + } + + getPosition(): IOverlayWidgetPosition | null { + return { + preference: null + }; + } +} + +registerEditorContribution(StickyScrollController.ID, StickyScrollController); + diff --git a/src/vs/editor/editor.all.ts b/src/vs/editor/editor.all.ts index a33146a005f..c32b55e0c07 100644 --- a/src/vs/editor/editor.all.ts +++ b/src/vs/editor/editor.all.ts @@ -41,6 +41,7 @@ import 'vs/editor/contrib/links/browser/links'; import 'vs/editor/contrib/multicursor/browser/multicursor'; import 'vs/editor/contrib/parameterHints/browser/parameterHints'; import 'vs/editor/contrib/rename/browser/rename'; +import 'vs/editor/contrib/stickyScroll/browser/stickyScroll'; import 'vs/editor/contrib/smartSelect/browser/smartSelect'; import 'vs/editor/contrib/snippet/browser/snippetController2'; import 'vs/editor/contrib/suggest/browser/suggestController'; diff --git a/src/vs/editor/standalone/browser/standaloneLanguages.ts b/src/vs/editor/standalone/browser/standaloneLanguages.ts index decc60790f5..56f84726c98 100644 --- a/src/vs/editor/standalone/browser/standaloneLanguages.ts +++ b/src/vs/editor/standalone/browser/standaloneLanguages.ts @@ -526,12 +526,13 @@ export function registerCodeActionProvider(languageSelector: LanguageSelector, p const languageFeaturesService = StandaloneServices.get(ILanguageFeaturesService); return languageFeaturesService.codeActionProvider.register(languageSelector, { providedCodeActionKinds: metadata?.providedCodeActionKinds, + documentation: metadata?.documentation, provideCodeActions: (model: model.ITextModel, range: Range, context: languages.CodeActionContext, token: CancellationToken): languages.ProviderResult<languages.CodeActionList> => { const markerService = StandaloneServices.get(IMarkerService); const markers = markerService.read({ resource: model.uri }).filter(m => { return Range.areIntersectingOrTouching(m, range); }); - return provider.provideCodeActions(model, range, { markers, only: context.only }, token); + return provider.provideCodeActions(model, range, { markers, only: context.only, trigger: context.trigger }, token); }, resolveCodeAction: provider.resolveCodeAction }); @@ -664,6 +665,11 @@ export interface CodeActionContext { * Requested kind of actions to return. */ readonly only?: string; + + /** + * The reason why code actions were requested. + */ + readonly trigger: languages.CodeActionTriggerType; } /** @@ -697,6 +703,8 @@ export interface CodeActionProviderMetadata { * such as `["quickfix.removeLine", "source.fixAll" ...]`. */ readonly providedCodeActionKinds?: readonly string[]; + + readonly documentation?: ReadonlyArray<{ readonly kind: string; readonly command: languages.Command }>; } /** @@ -753,6 +761,7 @@ export function createMonacoLanguagesAPI(): typeof monaco.languages { SignatureHelpTriggerKind: standaloneEnums.SignatureHelpTriggerKind, InlayHintKind: standaloneEnums.InlayHintKind, InlineCompletionTriggerKind: standaloneEnums.InlineCompletionTriggerKind, + CodeActionTriggerType: standaloneEnums.CodeActionTriggerType, // classes FoldingRangeKind: languages.FoldingRangeKind, diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index f608381f9e4..db39c17f9c6 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -1739,6 +1739,15 @@ declare namespace monaco.editor { } /** + * Text snapshot that works like an iterator. + * Will try to return chunks of roughly ~64KB size. + * Will return null when finished. + */ + export interface ITextSnapshot { + read(): string | null; + } + + /** * A model. */ export interface ITextModel { @@ -1769,7 +1778,7 @@ declare namespace monaco.editor { /** * Replace the entire text buffer value contained in this model. */ - setValue(newValue: string): void; + setValue(newValue: string | ITextSnapshot): void; /** * Get the text stored in this model. * @param eol The end of line character preference. Defaults to `EndOfLinePreference.TextDefined`. @@ -1778,6 +1787,12 @@ declare namespace monaco.editor { */ getValue(eol?: EndOfLinePreference, preserveBOM?: boolean): string; /** + * Get the text stored in this model. + * @param preserverBOM Preserve a BOM character if it was detected when the model was constructed. + * @return The text snapshot (it is safe to consume it asynchronously). + */ + createSnapshot(preserveBOM?: boolean): ITextSnapshot; + /** * Get the length of the text stored in this model. */ getValueLength(eol?: EndOfLinePreference, preserveBOM?: boolean): number; @@ -2932,6 +2947,10 @@ declare namespace monaco.editor { */ scrollbar?: IEditorScrollbarOptions; /** + * Control the behavior of the sticky scroll + */ + stickyScroll?: IEditorStickyScrollOptions; + /** * Control the behavior and rendering of the minimap. */ minimap?: IEditorMinimapOptions; @@ -3420,11 +3439,11 @@ declare namespace monaco.editor { */ bracketPairColorization?: IBracketPairColorizationOptions; /** - * Enables dropping into the editor from an external source. + * Controls dropping into the editor from an external source. * - * This shows a preview of the drop location and triggers an `onDropIntoEditor` event. + * When enabled, this shows a preview of the drop location and triggers an `onDropIntoEditor` event. */ - enableDropIntoEditor?: boolean; + dropIntoEditor?: IDropIntoEditorOptions; } export interface IDiffEditorBaseOptions { @@ -3790,6 +3809,16 @@ declare namespace monaco.editor { } /** + * Configuration options for editor sticky scroll + */ + export interface IEditorStickyScrollOptions { + /** + * Enable the sticky scroll + */ + enabled?: boolean; + } + + /** * Configuration options for editor inlayHints */ export interface IEditorInlayHintsOptions { @@ -4301,6 +4330,17 @@ declare namespace monaco.editor { readonly wrappingColumn: number; } + /** + * Configuration options for editor drop into behavior + */ + export interface IDropIntoEditorOptions { + /** + * Enable the dropping into editor. + * Defaults to true. + */ + enabled?: boolean; + } + export enum EditorOption { acceptSuggestionOnCommitCharacter = 0, acceptSuggestionOnEnter = 1, @@ -4334,7 +4374,7 @@ declare namespace monaco.editor { disableMonospaceOptimizations = 29, domReadOnly = 30, dragAndDrop = 31, - enableDropIntoEditor = 32, + dropIntoEditor = 32, emptySelectionClipboard = 33, extraEditorClassName = 34, fastScrollSensitivity = 35, @@ -4408,34 +4448,35 @@ declare namespace monaco.editor { smartSelect = 103, smoothScrolling = 104, stickyTabStops = 105, - stopRenderingLineAfter = 106, - suggest = 107, - suggestFontSize = 108, - suggestLineHeight = 109, - suggestOnTriggerCharacters = 110, - suggestSelection = 111, - tabCompletion = 112, - tabIndex = 113, - unicodeHighlighting = 114, - unusualLineTerminators = 115, - useShadowDOM = 116, - useTabStops = 117, - wordSeparators = 118, - wordWrap = 119, - wordWrapBreakAfterCharacters = 120, - wordWrapBreakBeforeCharacters = 121, - wordWrapColumn = 122, - wordWrapOverride1 = 123, - wordWrapOverride2 = 124, - wrappingIndent = 125, - wrappingStrategy = 126, - showDeprecated = 127, - inlayHints = 128, - editorClassName = 129, - pixelRatio = 130, - tabFocusMode = 131, - layoutInfo = 132, - wrappingInfo = 133 + stickyScroll = 106, + stopRenderingLineAfter = 107, + suggest = 108, + suggestFontSize = 109, + suggestLineHeight = 110, + suggestOnTriggerCharacters = 111, + suggestSelection = 112, + tabCompletion = 113, + tabIndex = 114, + unicodeHighlighting = 115, + unusualLineTerminators = 116, + useShadowDOM = 117, + useTabStops = 118, + wordSeparators = 119, + wordWrap = 120, + wordWrapBreakAfterCharacters = 121, + wordWrapBreakBeforeCharacters = 122, + wordWrapColumn = 123, + wordWrapOverride1 = 124, + wordWrapOverride2 = 125, + wrappingIndent = 126, + wrappingStrategy = 127, + showDeprecated = 128, + inlayHints = 129, + editorClassName = 130, + pixelRatio = 131, + tabFocusMode = 132, + layoutInfo = 133, + wrappingInfo = 134 } export const EditorOptions: { @@ -4473,7 +4514,7 @@ declare namespace monaco.editor { domReadOnly: IEditorOption<EditorOption.domReadOnly, boolean>; dragAndDrop: IEditorOption<EditorOption.dragAndDrop, boolean>; emptySelectionClipboard: IEditorOption<EditorOption.emptySelectionClipboard, boolean>; - enableDropIntoEditor: IEditorOption<EditorOption.enableDropIntoEditor, boolean>; + dropIntoEditor: IEditorOption<EditorOption.dropIntoEditor, Readonly<Required<IDropIntoEditorOptions>>>; extraEditorClassName: IEditorOption<EditorOption.extraEditorClassName, string>; fastScrollSensitivity: IEditorOption<EditorOption.fastScrollSensitivity, number>; find: IEditorOption<EditorOption.find, Readonly<Required<IEditorFindOptions>>>; @@ -4546,6 +4587,7 @@ declare namespace monaco.editor { snippetSuggestions: IEditorOption<EditorOption.snippetSuggestions, 'none' | 'top' | 'bottom' | 'inline'>; smartSelect: IEditorOption<EditorOption.smartSelect, Readonly<Required<ISmartSelectOptions>>>; smoothScrolling: IEditorOption<EditorOption.smoothScrolling, boolean>; + stickyScroll: IEditorOption<EditorOption.stickyScroll, Readonly<Required<IEditorStickyScrollOptions>>>; stopRenderingLineAfter: IEditorOption<EditorOption.stopRenderingLineAfter, number>; suggest: IEditorOption<EditorOption.suggest, Readonly<Required<ISuggestOptions>>>; inlineSuggest: IEditorOption<EditorOption.inlineSuggest, Readonly<Required<IInlineSuggestOptions>>>; @@ -5833,6 +5875,10 @@ declare namespace monaco.languages { * Requested kind of actions to return. */ readonly only?: string; + /** + * The reason why code actions were requested. + */ + readonly trigger: CodeActionTriggerType; } /** @@ -5863,6 +5909,10 @@ declare namespace monaco.languages { * such as `["quickfix.removeLine", "source.fixAll" ...]`. */ readonly providedCodeActionKinds?: readonly string[]; + readonly documentation?: ReadonlyArray<{ + readonly kind: string; + readonly command: Command; + }>; } /** @@ -6435,6 +6485,11 @@ declare namespace monaco.languages { disabled?: string; } + export enum CodeActionTriggerType { + Invoke = 1, + Auto = 2 + } + export interface CodeActionList extends IDisposable { readonly actions: ReadonlyArray<CodeAction>; } @@ -6771,11 +6826,11 @@ declare namespace monaco.languages { provideDocumentSymbols(model: editor.ITextModel, token: CancellationToken): ProviderResult<DocumentSymbol[]>; } - export type TextEdit = { + export interface TextEdit { range: IRange; text: string; eol?: editor.EndOfLineSequence; - }; + } /** * Interface used to format a model diff --git a/src/vs/platform/label/common/label.ts b/src/vs/platform/label/common/label.ts index b9ee570c43e..e5b6b8a1045 100644 --- a/src/vs/platform/label/common/label.ts +++ b/src/vs/platform/label/common/label.ts @@ -51,7 +51,7 @@ export interface ResourceLabelFormatter { } export interface ResourceLabelFormatting { - label: string | ((resource: URI) => string); // myLabel:/${path} + label: string; // myLabel:/${path} separator: '/' | '\\' | ''; tildify?: boolean; normalizeDriveLetter?: boolean; diff --git a/src/vs/platform/markers/common/markerService.ts b/src/vs/platform/markers/common/markerService.ts index e94d1f2bd74..6f6dd4e8810 100644 --- a/src/vs/platform/markers/common/markerService.ts +++ b/src/vs/platform/markers/common/markerService.ts @@ -103,7 +103,7 @@ class MarkerStats implements MarkerStatistics { const result: MarkerStatistics = { errors: 0, warnings: 0, infos: 0, unknowns: 0 }; // TODO this is a hack - if (resource.scheme === Schemas.inMemory || resource.scheme === Schemas.walkThrough || resource.scheme === Schemas.walkThroughSnippet) { + if (resource.scheme === Schemas.inMemory || resource.scheme === Schemas.walkThrough || resource.scheme === Schemas.walkThroughSnippet || resource.scheme === Schemas.vscodeSourceControl) { return result; } diff --git a/src/vs/platform/telemetry/browser/1dsAppender.ts b/src/vs/platform/telemetry/browser/1dsAppender.ts index 5075fdfc7df..1481d2ffecd 100644 --- a/src/vs/platform/telemetry/browser/1dsAppender.ts +++ b/src/vs/platform/telemetry/browser/1dsAppender.ts @@ -4,18 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import type { AppInsightsCore } from '@microsoft/1ds-core-js'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { AbstractOneDataSystemAppender } from 'vs/platform/telemetry/common/1dsAppender'; export class OneDataSystemWebAppender extends AbstractOneDataSystemAppender { constructor( - configurationService: IConfigurationService | undefined, + isInternalTelemetry: boolean, eventPrefix: string, defaultData: { [key: string]: any } | null, iKeyOrClientFactory: string | (() => AppInsightsCore), // allow factory function for testing ) { - super(configurationService, eventPrefix, defaultData, iKeyOrClientFactory); + super(isInternalTelemetry, eventPrefix, defaultData, iKeyOrClientFactory); // If we cannot fetch the endpoint it means it is down and we should not send any telemetry. // This is most likely due to ad blockers diff --git a/src/vs/platform/telemetry/common/1dsAppender.ts b/src/vs/platform/telemetry/common/1dsAppender.ts index 5dd4975c9e1..11c49cba163 100644 --- a/src/vs/platform/telemetry/common/1dsAppender.ts +++ b/src/vs/platform/telemetry/common/1dsAppender.ts @@ -7,7 +7,6 @@ import type { AppInsightsCore, IExtendedConfiguration } from '@microsoft/1ds-cor import type { IChannelConfiguration, IXHROverride, PostChannel } from '@microsoft/1ds-post-js'; import { onUnexpectedError } from 'vs/base/common/errors'; import { mixin } from 'vs/base/common/objects'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITelemetryAppender, validateTelemetryData } from 'vs/platform/telemetry/common/telemetryUtils'; const endpointUrl = 'https://mobile.events.data.microsoft.com/OneCollector/1.0'; @@ -63,7 +62,7 @@ export abstract class AbstractOneDataSystemAppender implements ITelemetryAppende protected readonly endPointUrl = endpointUrl; constructor( - private readonly _configurationService: IConfigurationService | undefined, + private readonly _isInternalTelemetry: boolean, private _eventPrefix: string, private _defaultData: { [key: string]: any } | null, iKeyOrClientFactory: string | (() => AppInsightsCore), // allow factory function for testing @@ -92,8 +91,7 @@ export abstract class AbstractOneDataSystemAppender implements ITelemetryAppende } if (!this._asyncAiCore) { - const isInternal = this._configurationService?.getValue<boolean>('telemetry.internalTesting'); - this._asyncAiCore = getClient(this._aiCoreOrKey, isInternal, this._xhrOverride); + this._asyncAiCore = getClient(this._aiCoreOrKey, this._isInternalTelemetry, this._xhrOverride); } this._asyncAiCore.then( diff --git a/src/vs/platform/telemetry/common/commonProperties.ts b/src/vs/platform/telemetry/common/commonProperties.ts index 06bd318a5eb..1b1df77de55 100644 --- a/src/vs/platform/telemetry/common/commonProperties.ts +++ b/src/vs/platform/telemetry/common/commonProperties.ts @@ -25,7 +25,7 @@ export async function resolveCommonProperties( commit: string | undefined, version: string | undefined, machineId: string | undefined, - msftInternalDomains: string[] | undefined, + isInternalTelemetry: boolean, installSourcePath: string, product?: string ): Promise<{ [name: string]: string | boolean | undefined }> { @@ -50,10 +50,9 @@ export async function resolveCommonProperties( // __GDPR__COMMON__ "common.product" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } result['common.product'] = product || 'desktop'; - const msftInternal = verifyMicrosoftInternalDomain(msftInternalDomains || []); - if (msftInternal) { + if (isInternalTelemetry) { // __GDPR__COMMON__ "common.msftInternal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } - result['common.msftInternal'] = msftInternal; + result['common.msftInternal'] = isInternalTelemetry; } // dynamic properties which value differs on each call diff --git a/src/vs/platform/telemetry/common/telemetryUtils.ts b/src/vs/platform/telemetry/common/telemetryUtils.ts index e8c5e417f2e..ded0380e412 100644 --- a/src/vs/platform/telemetry/common/telemetryUtils.ts +++ b/src/vs/platform/telemetry/common/telemetryUtils.ts @@ -11,6 +11,7 @@ import { URI } from 'vs/base/common/uri'; import { ConfigurationTarget, ConfigurationTargetToString, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IProductService } from 'vs/platform/product/common/productService'; +import { verifyMicrosoftInternalDomain } from 'vs/platform/telemetry/common/commonProperties'; import { ClassifiedEvent, GDPRClassification, StrictPropertyCheck } from 'vs/platform/telemetry/common/gdprTypings'; import { ICustomEndpointTelemetryService, ITelemetryData, ITelemetryEndpoint, ITelemetryInfo, ITelemetryService, TelemetryConfiguration, TelemetryLevel, TELEMETRY_OLD_SETTING_ID, TELEMETRY_SETTING_ID } from 'vs/platform/telemetry/common/telemetry'; @@ -252,6 +253,18 @@ function flatKeys(result: string[], prefix: string, value: { [key: string]: any } } +/** + * Whether or not this is an internal user + * @param productService The product service + * @param configService The config servivce + * @returns true if internal, false otherwise + */ +export function isInternalTelemetry(productService: IProductService, configService: IConfigurationService) { + const msftInternalDomains = productService.msftInternalDomains || []; + const internalTesting = configService.getValue<boolean>('telemetry.internalTesting'); + return verifyMicrosoftInternalDomain(msftInternalDomains) || internalTesting; +} + interface IPathEnvironment { appRoot: string; extensionsPath: string; diff --git a/src/vs/platform/telemetry/node/1dsAppender.ts b/src/vs/platform/telemetry/node/1dsAppender.ts index c7b6442cf7a..b8825f6e8a9 100644 --- a/src/vs/platform/telemetry/node/1dsAppender.ts +++ b/src/vs/platform/telemetry/node/1dsAppender.ts @@ -6,14 +6,13 @@ import type { AppInsightsCore } from '@microsoft/1ds-core-js'; import type { IPayloadData, IXHROverride } from '@microsoft/1ds-post-js'; import * as https from 'https'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { AbstractOneDataSystemAppender } from 'vs/platform/telemetry/common/1dsAppender'; export class OneDataSystemAppender extends AbstractOneDataSystemAppender { constructor( - configurationService: IConfigurationService | undefined, + isInternalTelemetry: boolean, eventPrefix: string, defaultData: { [key: string]: any } | null, iKeyOrClientFactory: string | (() => AppInsightsCore), // allow factory function for testing @@ -48,6 +47,6 @@ export class OneDataSystemAppender extends AbstractOneDataSystemAppender { } }; - super(configurationService, eventPrefix, defaultData, iKeyOrClientFactory, customHttpXHROverride); + super(isInternalTelemetry, eventPrefix, defaultData, iKeyOrClientFactory, customHttpXHROverride); } } diff --git a/src/vs/platform/telemetry/test/browser/1dsAppender.test.ts b/src/vs/platform/telemetry/test/browser/1dsAppender.test.ts index 0c0378845ab..c59461414d1 100644 --- a/src/vs/platform/telemetry/test/browser/1dsAppender.test.ts +++ b/src/vs/platform/telemetry/test/browser/1dsAppender.test.ts @@ -33,7 +33,7 @@ suite('AIAdapter', () => { setup(() => { appInsightsMock = new AppInsightsCoreMock(); - adapter = new OneDataSystemWebAppender(undefined, prefix, undefined!, () => appInsightsMock); + adapter = new OneDataSystemWebAppender(false, prefix, undefined!, () => appInsightsMock); }); teardown(() => { @@ -48,7 +48,7 @@ suite('AIAdapter', () => { }); test('addional data', () => { - adapter = new OneDataSystemWebAppender(undefined, prefix, { first: '1st', second: 2, third: true }, () => appInsightsMock); + adapter = new OneDataSystemWebAppender(false, prefix, { first: '1st', second: 2, third: true }, () => appInsightsMock); adapter.log('testEvent'); assert.strictEqual(appInsightsMock.events.length, 1); diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index 4c3c72a1f30..3f56c931175 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -169,8 +169,14 @@ export interface IPtyHostAttachTarget { environmentVariableCollections: ISerializableEnvironmentVariableCollections | undefined; reconnectionOwner?: string; task?: { label: string; id: string; lastTask: string; group?: string }; + waitOnExit?: WaitOnExitValue; + hideFromUser?: boolean; + isFeatureTerminal?: boolean; + type?: TerminalType; } +export type TerminalType = 'Task' | 'Local' | undefined; + export enum TitleEventSource { /** From the API or the rename command that overrides any other type */ Api, @@ -446,7 +452,7 @@ export interface IShellLaunchConfig { reconnectionOwner?: string; /** Whether to wait for a key press before closing the terminal. */ - waitOnExit?: boolean | string | ((exitCode: number) => string); + waitOnExit?: WaitOnExitValue; /** * A string including ANSI escape sequences that will be written to the terminal emulator @@ -469,7 +475,9 @@ export interface IShellLaunchConfig { /** * This is a terminal that attaches to an already running terminal. */ - attachPersistentProcess?: { id: number; findRevivedId?: boolean; pid: number; title: string; titleSource: TitleEventSource; cwd: string; icon?: TerminalIcon; color?: string; hasChildProcesses?: boolean; fixedDimensions?: IFixedTerminalDimensions; environmentVariableCollections?: ISerializableEnvironmentVariableCollections; reconnectionOwner?: string; task?: { label: string; id: string; lastTask: string; group?: string } }; + attachPersistentProcess?: { + id: number; findRevivedId?: boolean; pid: number; title: string; titleSource: TitleEventSource; cwd: string; icon?: TerminalIcon; color?: string; hasChildProcesses?: boolean; fixedDimensions?: IFixedTerminalDimensions; environmentVariableCollections?: ISerializableEnvironmentVariableCollections; reconnectionOwner?: string; task?: { label: string; id: string; lastTask: string; group?: string }; type?: TerminalType; waitOnExit?: WaitOnExitValue; hideFromUser?: boolean; isFeatureTerminal?: boolean; + }; /** * Whether the terminal process environment should be exactly as provided in @@ -544,9 +552,11 @@ export interface IShellLaunchConfig { /** * The task associated with this terminal */ - task?: { lastTask: string; group?: string; label: string; id: string }; + task?: { label: string; id: string; lastTask: string; group?: string }; } +export type WaitOnExitValue = boolean | string | ((exitCode: number) => string); + export interface ICreateContributedTerminalProfileOptions { icon?: URI | string | { light: URI; dark: URI }; color?: string; diff --git a/src/vs/platform/terminal/common/terminalProcess.ts b/src/vs/platform/terminal/common/terminalProcess.ts index 4329c9e0635..dc360427a6b 100644 --- a/src/vs/platform/terminal/common/terminalProcess.ts +++ b/src/vs/platform/terminal/common/terminalProcess.ts @@ -5,7 +5,7 @@ import { UriComponents } from 'vs/base/common/uri'; import { ISerializableEnvironmentVariableCollection, ISerializableEnvironmentVariableCollections } from 'vs/platform/terminal/common/environmentVariable'; -import { IFixedTerminalDimensions, IRawTerminalTabLayoutInfo, ITerminalEnvironment, ITerminalTabLayoutInfoById, TerminalIcon, TitleEventSource } from 'vs/platform/terminal/common/terminal'; +import { IFixedTerminalDimensions, IRawTerminalTabLayoutInfo, ITerminalEnvironment, ITerminalTabLayoutInfoById, TerminalIcon, TerminalType, TitleEventSource, WaitOnExitValue } from 'vs/platform/terminal/common/terminal'; export interface ISingleTerminalConfiguration<T> { userValue: T | undefined; @@ -62,6 +62,10 @@ export interface IProcessDetails { environmentVariableCollections: ISerializableEnvironmentVariableCollections | undefined; reconnectionOwner?: string; task?: { label: string; id: string; lastTask: string; group?: string }; + waitOnExit?: WaitOnExitValue; + hideFromUser?: boolean; + isFeatureTerminal?: boolean; + type?: TerminalType; } export type ITerminalTabLayoutInfoDto = IRawTerminalTabLayoutInfo<IProcessDetails>; diff --git a/src/vs/platform/terminal/node/ptyService.ts b/src/vs/platform/terminal/node/ptyService.ts index 2b7ad270732..5e1feff3086 100644 --- a/src/vs/platform/terminal/node/ptyService.ts +++ b/src/vs/platform/terminal/node/ptyService.ts @@ -411,7 +411,11 @@ export class PtyService extends Disposable implements IPtyService { fixedDimensions: persistentProcess.fixedDimensions, environmentVariableCollections: persistentProcess.processLaunchOptions.options.environmentVariableCollections, reconnectionOwner: persistentProcess.shellLaunchConfig.reconnectionOwner, - task: persistentProcess.shellLaunchConfig.task + task: persistentProcess.shellLaunchConfig.task, + waitOnExit: persistentProcess.shellLaunchConfig.waitOnExit, + hideFromUser: persistentProcess.shellLaunchConfig.hideFromUser, + isFeatureTerminal: persistentProcess.shellLaunchConfig.isFeatureTerminal, + type: persistentProcess.shellLaunchConfig.type }; } diff --git a/src/vs/platform/theme/common/colorRegistry.ts b/src/vs/platform/theme/common/colorRegistry.ts index 3305267657d..129959b9e23 100644 --- a/src/vs/platform/theme/common/colorRegistry.ts +++ b/src/vs/platform/theme/common/colorRegistry.ts @@ -313,13 +313,17 @@ export const editorBackground = registerColor('editor.background', { light: '#ff export const editorForeground = registerColor('editor.foreground', { light: '#333333', dark: '#BBBBBB', hcDark: Color.white, hcLight: foreground }, nls.localize('editorForeground', "Editor default foreground color.")); /** + * Sticky scroll + */ +export const editorStickyScrollBackground = registerColor('editorStickyScroll.background', { light: darken(editorBackground, 0.2), dark: lighten(editorBackground, 0.2), hcDark: lighten(editorBackground, 0.2), hcLight: darken(editorBackground, 0.2) }, nls.localize('editorStickyScrollBackground', "Sticky scroll background color for the editor")); +export const editorStickyScrollHoverBackground = registerColor('editorStickyScrollHover.background', { light: darken(editorBackground, 0.4), dark: lighten(editorBackground, 0.4), hcDark: lighten(editorBackground, 0.4), hcLight: darken(editorBackground, 0.4) }, nls.localize('editorStickyScrollHoverBackground', "Sticky scroll on hover background color for the editor")); + +/** * Editor widgets */ export const editorWidgetBackground = registerColor('editorWidget.background', { dark: '#252526', light: '#F3F3F3', hcDark: '#0C141F', hcLight: Color.white }, nls.localize('editorWidgetBackground', 'Background color of editor widgets, such as find/replace.')); export const editorWidgetForeground = registerColor('editorWidget.foreground', { dark: foreground, light: foreground, hcDark: foreground, hcLight: foreground }, nls.localize('editorWidgetForeground', 'Foreground color of editor widgets, such as find/replace.')); - export const editorWidgetBorder = registerColor('editorWidget.border', { dark: '#454545', light: '#C8C8C8', hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('editorWidgetBorder', 'Border color of editor widgets. The color is only used if the widget chooses to have a border and if the color is not overridden by a widget.')); - export const editorWidgetResizeBorder = registerColor('editorWidget.resizeBorder', { light: null, dark: null, hcDark: null, hcLight: null }, nls.localize('editorWidgetResizeBorder', "Border color of the resize bar of editor widgets. The color is only used if the widget chooses to have a resize border and if the color is not overridden by a widget.")); /** diff --git a/src/vs/platform/userDataProfile/common/userDataProfile.ts b/src/vs/platform/userDataProfile/common/userDataProfile.ts index 3d0c1ba1b4c..1d7ad908f95 100644 --- a/src/vs/platform/userDataProfile/common/userDataProfile.ts +++ b/src/vs/platform/userDataProfile/common/userDataProfile.ts @@ -155,7 +155,7 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf readonly _serviceBrand: undefined; - private enabled: boolean = false; + protected enabled: boolean = false; protected readonly defaultProfileShouldIncludeExtensionsResourceAlways: boolean = false; readonly profilesHome: URI; diff --git a/src/vs/platform/userDataProfile/electron-main/userDataProfile.ts b/src/vs/platform/userDataProfile/electron-main/userDataProfile.ts index 0ac191f058a..db97f74bd82 100644 --- a/src/vs/platform/userDataProfile/electron-main/userDataProfile.ts +++ b/src/vs/platform/userDataProfile/electron-main/userDataProfile.ts @@ -17,6 +17,7 @@ import { IStringDictionary } from 'vs/base/common/collections'; export const IUserDataProfilesMainService = refineServiceDecorator<IUserDataProfilesService, IUserDataProfilesMainService>(IUserDataProfilesService); export interface IUserDataProfilesMainService extends IUserDataProfilesService { + isEnabled(): boolean; unsetWorkspace(workspaceIdentifier: WorkspaceIdentifier): Promise<void>; readonly onWillCreateProfile: Event<WillCreateProfileEvent>; readonly onWillRemoveProfile: Event<WillRemoveProfileEvent>; @@ -34,6 +35,10 @@ export class UserDataProfilesMainService extends UserDataProfilesService impleme super(stateMainService, uriIdentityService, environmentService, fileService, logService); } + isEnabled(): boolean { + return this.enabled; + } + protected override saveStoredProfiles(storedProfiles: StoredUserDataProfile[]): void { this.stateMainService.setItem(UserDataProfilesMainService.PROFILES_KEY, storedProfiles); } diff --git a/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts index 2ee6816721e..8e5d39ff29e 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts @@ -205,8 +205,10 @@ export class WorkspacesManagementMainService extends Disposable implements IWork // Delete from disk this.doDeleteUntitledWorkspaceSync(workspace); - // unset workspace from profiles - this.userDataProfilesMainService.unsetWorkspace(workspace); + if (this.userDataProfilesMainService.isEnabled()) { + // unset workspace from profiles + this.userDataProfilesMainService.unsetWorkspace(workspace); + } // Event this._onDidDeleteUntitledWorkspace.fire(workspace); diff --git a/src/vs/server/node/server.cli.ts b/src/vs/server/node/server.cli.ts index 4c5ec7e1e2a..837b0b500d4 100644 --- a/src/vs/server/node/server.cli.ts +++ b/src/vs/server/node/server.cli.ts @@ -44,7 +44,6 @@ const isSupportedForCmd = (optionId: keyof RemoteParsedArgs) => { case 'enable-smoke-test-driver': case 'extensions-download-dir': case 'builtin-extensions-dir': - case 'locate-shell-integration-path': case 'telemetry': return false; default: @@ -74,6 +73,7 @@ const isSupportedForPipe = (optionId: keyof RemoteParsedArgs) => { case 'category': case 'verbose': case 'remote': + case 'locate-shell-integration-path': return true; default: return false; @@ -135,6 +135,20 @@ export function main(desc: ProductDescription, args: string[]): void { console.log(buildVersionMessage(desc.version, desc.commit)); return; } + if (parsedArgs['locate-shell-integration-path']) { + let file: string; + switch (parsedArgs['locate-shell-integration-path']) { + // Usage: `[[ "$TERM_PROGRAM" == "vscode" ]] && . "$(code --locate-shell-integration-path bash)"` + case 'bash': file = 'shellIntegration-bash.sh'; break; + // Usage: `if ($env:TERM_PROGRAM -eq "vscode") { . "$(code --locate-shell-integration-path pwsh)" }` + case 'pwsh': file = 'shellIntegration.ps1'; break; + // Usage: `[[ "$TERM_PROGRAM" == "vscode" ]] && . "$(code --locate-shell-integration-path zsh)"` + case 'zsh': file = 'shellIntegration-rc.zsh'; break; + default: throw new Error('Error using --locate-shell-integration-path: Invalid shell type'); + } + console.log(resolve(__dirname, '../..', 'workbench', 'contrib', 'terminal', 'browser', 'media', file)); + return; + } if (cliPipe) { if (parsedArgs['openExternal']) { openInBrowser(parsedArgs['_'], verbose); @@ -218,7 +232,6 @@ export function main(desc: ProductDescription, args: string[]): void { return; } - const newCommandline: string[] = []; for (const key in parsedArgs) { const val = parsedArgs[key as keyof typeof parsedArgs]; diff --git a/src/vs/server/node/serverServices.ts b/src/vs/server/node/serverServices.ts index ab5f5cb7d57..bc2d71f8f03 100644 --- a/src/vs/server/node/serverServices.ts +++ b/src/vs/server/node/serverServices.ts @@ -49,7 +49,7 @@ import { RequestService } from 'vs/platform/request/node/requestService'; import { resolveCommonProperties } from 'vs/platform/telemetry/common/commonProperties'; import { ITelemetryService, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; import { ITelemetryServiceConfig } from 'vs/platform/telemetry/common/telemetryService'; -import { getPiiPathsFromEnvironment, ITelemetryAppender, NullAppender, supportsTelemetry } from 'vs/platform/telemetry/common/telemetryUtils'; +import { getPiiPathsFromEnvironment, isInternalTelemetry, ITelemetryAppender, NullAppender, supportsTelemetry } from 'vs/platform/telemetry/common/telemetryUtils'; import ErrorTelemetry from 'vs/platform/telemetry/node/errorTelemetry'; import { IPtyService, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; import { PtyHostService } from 'vs/platform/terminal/node/ptyHostService'; @@ -131,15 +131,16 @@ export async function setupServerServices(connectionToken: ServerConnectionToken let oneDsAppender: ITelemetryAppender = NullAppender; const machineId = await getMachineId(); + const isInternal = isInternalTelemetry(productService, configurationService); if (supportsTelemetry(productService, environmentService)) { if (productService.aiConfig && productService.aiConfig.ariaKey) { - oneDsAppender = new OneDataSystemAppender(configurationService, eventPrefix, null, productService.aiConfig.ariaKey); + oneDsAppender = new OneDataSystemAppender(isInternal, eventPrefix, null, productService.aiConfig.ariaKey); disposables.add(toDisposable(() => oneDsAppender?.flush())); // Ensure the AI appender is disposed so that it flushes remaining data } const config: ITelemetryServiceConfig = { appenders: [oneDsAppender], - commonProperties: resolveCommonProperties(fileService, release(), hostname(), process.arch, productService.commit, productService.version + '-remote', machineId, productService.msftInternalDomains, environmentService.installSourcePath, 'remoteAgent'), + commonProperties: resolveCommonProperties(fileService, release(), hostname(), process.arch, productService.commit, productService.version + '-remote', machineId, isInternal, environmentService.installSourcePath, 'remoteAgent'), piiPaths: getPiiPathsFromEnvironment(environmentService) }; const initialTelemetryLevelArg = environmentService.args['telemetry-level']; diff --git a/src/vs/workbench/browser/actions/layoutActions.ts b/src/vs/workbench/browser/actions/layoutActions.ts index 150c192a1b8..519e716b8c7 100644 --- a/src/vs/workbench/browser/actions/layoutActions.ts +++ b/src/vs/workbench/browser/actions/layoutActions.ts @@ -81,7 +81,7 @@ export class ToggleActivityBarVisibilityAction extends Action2 { id: ToggleActivityBarVisibilityAction.ID, title: { value: localize('toggleActivityBar', "Toggle Activity Bar Visibility"), - mnemonicTitle: localize({ key: 'miShowActivityBar', comment: ['&& denotes a mnemonic'] }, "Show &&Activity Bar"), + mnemonicTitle: localize({ key: 'miActivityBar', comment: ['&& denotes a mnemonic'] }, "&&Activity Bar"), original: 'Toggle Activity Bar Visibility' }, category: CATEGORIES.View, @@ -406,7 +406,7 @@ MenuRegistry.appendMenuItems([ group: '2_workbench_layout', command: { id: ToggleSidebarVisibilityAction.ID, - title: localize({ key: 'miShowSidebar', comment: ['&& denotes a mnemonic'] }, "Show &&Primary Side Bar"), + title: localize({ key: 'miShowSidebar', comment: ['&& denotes a mnemonic'] }, "&&Primary Side Bar"), toggled: SideBarVisibleContext }, order: 1 @@ -417,7 +417,7 @@ MenuRegistry.appendMenuItems([ group: '0_workbench_layout', command: { id: ToggleSidebarVisibilityAction.ID, - title: localize('miShowSidebarNoMnnemonic', "Show Primary Side Bar"), + title: localize('miSidebarNoMnnemonic', "Primary Side Bar"), toggled: SideBarVisibleContext }, order: 0 @@ -464,7 +464,7 @@ export class ToggleStatusbarVisibilityAction extends Action2 { id: ToggleStatusbarVisibilityAction.ID, title: { value: localize('toggleStatusbar', "Toggle Status Bar Visibility"), - mnemonicTitle: localize({ key: 'miShowStatusbar', comment: ['&& denotes a mnemonic'] }, "Show S&&tatus Bar"), + mnemonicTitle: localize({ key: 'miStatusbar', comment: ['&& denotes a mnemonic'] }, "S&&tatus Bar"), original: 'Toggle Status Bar Visibility' }, category: CATEGORIES.View, @@ -573,7 +573,7 @@ if (isWindows || isLinux || isWeb) { id: 'workbench.action.toggleMenuBar', title: { value: localize('toggleMenuBar', "Toggle Menu Bar"), - mnemonicTitle: localize({ key: 'miShowMenuBar', comment: ['&& denotes a mnemonic'] }, "Show Menu &&Bar"), + mnemonicTitle: localize({ key: 'miMenuBar', comment: ['&& denotes a mnemonic'] }, "Menu &&Bar"), original: 'Toggle Menu Bar' }, category: CATEGORIES.View, @@ -596,7 +596,7 @@ if (isWindows || isLinux || isWeb) { MenuRegistry.appendMenuItem(MenuId.TitleBarContext, { command: { id: 'workbench.action.toggleMenuBar', - title: localize('miShowMenuBarNoMnemonic', "Show Menu Bar"), + title: localize('miMenuBarNoMnemonic', "Menu Bar"), toggled: ContextKeyExpr.and(IsMacNativeContext.toNegated(), ContextKeyExpr.notEquals('config.window.menuBarVisibility', 'hidden'), ContextKeyExpr.notEquals('config.window.menuBarVisibility', 'toggle'), ContextKeyExpr.notEquals('config.window.menuBarVisibility', 'compact')) }, order: 0 diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index d5f339775a3..9151c945a6c 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -578,7 +578,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi input2: { resource: filesToMerge[1].resource }, base: { resource: filesToMerge[2].resource }, result: { resource: filesToMerge[3].resource }, - options: { pinned: true } + options: { pinned: true, override: 'mergeEditor.Input' } // TODO@bpasero remove the override once the resolver is ready }]; } diff --git a/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions.ts b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions.ts index 2b0b723d0e3..cf521c32e16 100644 --- a/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions.ts +++ b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions.ts @@ -76,7 +76,7 @@ MenuRegistry.appendMenuItems([ group: '0_workbench_layout', command: { id: ToggleAuxiliaryBarAction.ID, - title: localize('miShowAuxiliaryBarNoMnemonic', "Show Secondary Side Bar"), + title: localize('miAuxiliaryBarNoMnemonic', "Secondary Side Bar"), toggled: AuxiliaryBarVisibleContext }, order: 2 @@ -116,7 +116,7 @@ MenuRegistry.appendMenuItems([ group: '2_workbench_layout', command: { id: ToggleAuxiliaryBarAction.ID, - title: localize({ key: 'miShowAuxiliaryBar', comment: ['&& denotes a mnemonic'] }, "Show Secondary Si&&de Bar"), + title: localize({ key: 'miAuxiliaryBar', comment: ['&& denotes a mnemonic'] }, "Secondary Si&&de Bar"), toggled: AuxiliaryBarVisibleContext }, order: 2 diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts index 51879dbf0ba..0ef796ae3fa 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts @@ -504,7 +504,7 @@ registerAction2(class ToggleBreadcrumb extends Action2 { id: 'breadcrumbs.toggle', title: { value: localize('cmd.toggle', "Toggle Breadcrumbs"), - mnemonicTitle: localize('miShowBreadcrumbs', "Show &&Breadcrumbs"), + mnemonicTitle: localize('miBreadcrumbs', "&&Breadcrumbs"), original: 'Toggle Breadcrumbs', }, category: CATEGORIES.View, diff --git a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts index e9686439360..947f4475f3b 100644 --- a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts +++ b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts @@ -32,7 +32,7 @@ interface IDropOperation { } function isDropIntoEditorEnabledGlobally(configurationService: IConfigurationService) { - return configurationService.getValue<boolean>('workbench.editor.dropIntoEditor.enabled'); + return configurationService.getValue<boolean>('editor.dropIntoEditor.enabled'); } function isDragIntoEditorEvent(e: DragEvent): boolean { diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index facf063df26..44e9ed15b06 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -1154,9 +1154,9 @@ export class TabsTitleControl extends TitleControl { tabContainer.classList.toggle(`sticky-${option}`, isTabSticky && options.pinnedTabSizing === option); } - // Sticky compact/shrink tabs need a position to remain at their location + // If not wrapping tabs, sticky compact/shrink tabs need a position to remain at their location // when scrolling to stay in view (requirement for position: sticky) - if (isTabSticky && options.pinnedTabSizing !== 'normal') { + if (!options.wrapTabs && isTabSticky && options.pinnedTabSizing !== 'normal') { let stickyTabWidth = 0; switch (options.pinnedTabSizing) { case 'compact': diff --git a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts index 2aa1285f9a1..2dcf135be7c 100644 --- a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts @@ -240,7 +240,7 @@ export class TextDiffEditor extends AbstractTextEditor<IDiffEditorViewState> imp return { ...super.getConfigurationOverrides(), readOnly, - enableDropIntoEditor: !readOnly, + dropIntoEditor: { enabled: !readOnly }, originalEditable: this.input instanceof DiffEditorInput && !this.input.original.hasCapability(EditorInputCapabilities.Readonly), lineDecorationsWidth: '2ch' }; @@ -251,7 +251,7 @@ export class TextDiffEditor extends AbstractTextEditor<IDiffEditorViewState> imp this.diffEditorControl?.updateOptions({ readOnly: input.hasCapability(EditorInputCapabilities.Readonly), originalEditable: !input.original.hasCapability(EditorInputCapabilities.Readonly), - enableDropIntoEditor: !input.hasCapability(EditorInputCapabilities.Readonly) + dropIntoEditor: { enabled: !input.hasCapability(EditorInputCapabilities.Readonly) } }); } else { super.updateReadonly(input); diff --git a/src/vs/workbench/browser/parts/editor/textEditor.ts b/src/vs/workbench/browser/parts/editor/textEditor.ts index c803409ef74..b43dd3baa8d 100644 --- a/src/vs/workbench/browser/parts/editor/textEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textEditor.ts @@ -138,7 +138,7 @@ export abstract class AbstractTextEditor<T extends IEditorViewState> extends Abs this.updateEditorControlOptions({ readOnly, - enableDropIntoEditor: !readOnly + dropIntoEditor: { enabled: !readOnly }, }); } @@ -150,7 +150,7 @@ export abstract class AbstractTextEditor<T extends IEditorViewState> extends Abs lineNumbersMinChars: 3, fixedOverflowWidgets: true, readOnly, - enableDropIntoEditor: !readOnly, + dropIntoEditor: { enabled: !readOnly }, renderValidationDecorations: 'on' // render problems even in readonly editors (https://github.com/microsoft/vscode/issues/89057) }; } diff --git a/src/vs/workbench/browser/parts/panel/panelActions.ts b/src/vs/workbench/browser/parts/panel/panelActions.ts index e64439239c5..b23e8bd6fa3 100644 --- a/src/vs/workbench/browser/parts/panel/panelActions.ts +++ b/src/vs/workbench/browser/parts/panel/panelActions.ts @@ -422,7 +422,7 @@ MenuRegistry.appendMenuItems([ group: '2_workbench_layout', command: { id: TogglePanelAction.ID, - title: localize({ key: 'miShowPanel', comment: ['&& denotes a mnemonic'] }, "Show &&Panel"), + title: localize({ key: 'miPanel', comment: ['&& denotes a mnemonic'] }, "&&Panel"), toggled: PanelVisibleContext }, order: 5 @@ -433,7 +433,7 @@ MenuRegistry.appendMenuItems([ group: '0_workbench_layout', command: { id: TogglePanelAction.ID, - title: localize('miShowPanelNoMnemonic', "Show Panel"), + title: localize('miPanelNoMnemonic', "Panel"), toggled: PanelVisibleContext }, order: 4 diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 7c4cef14f90..31f4d9b6e27 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -524,12 +524,12 @@ class ToogleConfigAction extends Action2 { registerAction2(class ToogleCommandCenter extends ToogleConfigAction { constructor() { - super('window.commandCenter', localize('toggle.commandCenter', 'Show Command Center'), 1); + super('window.commandCenter', localize('toggle.commandCenter', 'Command Center'), 1); } }); registerAction2(class ToogleLayoutControl extends ToogleConfigAction { constructor() { - super('workbench.layoutControl.enabled', localize('toggle.layout', 'Show Layout Controls'), 2); + super('workbench.layoutControl.enabled', localize('toggle.layout', 'Layout Controls'), 2); } }); diff --git a/src/vs/workbench/browser/web.api.ts b/src/vs/workbench/browser/web.api.ts index 637b1af74cd..258b6880e1a 100644 --- a/src/vs/workbench/browser/web.api.ts +++ b/src/vs/workbench/browser/web.api.ts @@ -98,6 +98,18 @@ export interface IWorkbench { ): Promise<R>; }; + workspace: { + /** + * Forwards a port. If the current embedder implements a tunnelFactory then that will be used to make the tunnel. + * By default, openTunnel only support localhost; however, a tunnelFactory can be used to support other ips. + * + * @throws When run in an environment without a remote. + * + * @param tunnelOptions The `localPort` is a suggestion only. If that port is not available another will be chosen. + */ + openTunnel(tunnelOptions: ITunnelOptions): Thenable<ITunnel>; + }; + /** * Triggers shutdown of the workbench programmatically. After this method is * called, the workbench is not usable anymore and the page needs to reload @@ -110,16 +122,6 @@ export interface IWorkbench { * has been persisted. */ shutdown: () => Promise<void>; - - /** - * Forwards a port. If the current embedder implements a tunnelFactory then that will be used to make the tunnel. - * By default, openTunnel only support localhost; however, a tunnelFactory can be used to support other ips. - * - * @throws When run in an environment without a remote. - * - * @param tunnelOptions The `localPort` is a suggestion only. If that port is not available another will be chosen. - */ - openTunnel(tunnelOptions: ITunnelOptions): Thenable<ITunnel>; } export interface IWorkbenchConstructionOptions { diff --git a/src/vs/workbench/browser/web.factory.ts b/src/vs/workbench/browser/web.factory.ts index 993aa0ce071..eef12507e51 100644 --- a/src/vs/workbench/browser/web.factory.ts +++ b/src/vs/workbench/browser/web.factory.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IWorkbench, IWorkbenchConstructionOptions, Menu } from 'vs/workbench/browser/web.api'; +import { ITunnel, ITunnelOptions, IWorkbench, IWorkbenchConstructionOptions, Menu } from 'vs/workbench/browser/web.api'; import { BrowserMain } from 'vs/workbench/browser/web.main'; import { URI } from 'vs/base/common/uri'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; @@ -152,3 +152,14 @@ export namespace window { return workbench.window.withProgress(options, task); } } + +export namespace workspace { + + /** + * {@linkcode IWorkbench.workspace IWorkbench.workspace.openTunnel} + */ + export async function openTunnel(tunnelOptions: ITunnelOptions): Promise<ITunnel> { + const workbench = await workbenchPromise.p; + return workbench.workspace.openTunnel(tunnelOptions); + } +} diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index d0791572ccf..a307a64ec7e 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -137,7 +137,7 @@ export class BrowserMain extends Disposable { const openerService = accessor.get(IOpenerService); const productService = accessor.get(IProductService); const telemetryService = accessor.get(ITelemetryService); - const progessService = accessor.get(IProgressService); + const progressService = accessor.get(IProgressService); const environmentService = accessor.get(IBrowserWorkbenchEnvironmentService); const instantiationService = accessor.get(IInstantiationService); const remoteExplorerService = accessor.get(IRemoteExplorerService); @@ -169,28 +169,30 @@ export class BrowserMain extends Disposable { } }, window: { - withProgress: (options, task) => progessService.withProgress(options, task) + withProgress: (options, task) => progressService.withProgress(options, task) }, - shutdown: () => lifecycleService.shutdown(), - openTunnel: async (tunnelOptions) => { - const tunnel = await remoteExplorerService.forward({ - remote: tunnelOptions.remoteAddress, - local: tunnelOptions.localAddressPort, - name: tunnelOptions.label, - source: { - source: TunnelSource.Extension, - description: labelService.getHostLabel(Schemas.vscodeRemote, this.configuration.remoteAuthority) - }, - elevateIfNeeded: false - }); - if (!tunnel) { - throw new Error('cannot open tunnel'); - } + workspace: { + openTunnel: async (tunnelOptions) => { + const tunnel = await remoteExplorerService.forward({ + remote: tunnelOptions.remoteAddress, + local: tunnelOptions.localAddressPort, + name: tunnelOptions.label, + source: { + source: TunnelSource.Extension, + description: labelService.getHostLabel(Schemas.vscodeRemote, this.configuration.remoteAuthority) + }, + elevateIfNeeded: false + }); + if (!tunnel) { + throw new Error('cannot open tunnel'); + } - return new class extends DisposableTunnel implements ITunnel { - override localAddress!: string; - }({ port: tunnel.tunnelRemotePort, host: tunnel.tunnelRemoteHost }, tunnel.localAddress, () => tunnel.dispose()); - } + return new class extends DisposableTunnel implements ITunnel { + override localAddress!: string; + }({ port: tunnel.tunnelRemotePort, host: tunnel.tunnelRemoteHost }, tunnel.localAddress, () => tunnel.dispose()); + } + }, + shutdown: () => lifecycleService.shutdown() }; }); } diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index b042c202d98..c8728506774 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -466,11 +466,6 @@ const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Con 'default': 'both', 'description': localize('layoutControlType', "Controls whether the layout control in the custom title bar is displayed as a single menu button or with multiple UI toggles."), }, - 'workbench.editor.dropIntoEditor.enabled': { - 'type': 'boolean', - 'default': true, - 'markdownDescription': localize('dropIntoEditor', "Controls whether you can drag and drop a file into a text editor by holding down `shift` (instead of opening the file in an editor)."), - }, 'workbench.experimental.layoutControl.enabled': { 'type': 'boolean', 'tags': ['experimental'], diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index a886ffaee13..6f6ab509248 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -484,8 +484,6 @@ export interface IResourceDiffEditorInput extends IBaseUntypedEditorInput { readonly modified: IResourceEditorInput | ITextResourceEditorInput | IUntitledTextResourceEditorInput; } -export type IResourceMergeEditorInputSide = (IResourceEditorInput | ITextResourceEditorInput) & { detail?: string }; - /** * A resource merge editor input compares multiple editors * highlighting the differences for merging. @@ -498,12 +496,12 @@ export interface IResourceMergeEditorInput extends IBaseUntypedEditorInput { /** * The one changed version of the file. */ - readonly input1: IResourceMergeEditorInputSide; + readonly input1: IResourceEditorInput | ITextResourceEditorInput; /** * The second changed version of the file. */ - readonly input2: IResourceMergeEditorInputSide; + readonly input2: IResourceEditorInput | ITextResourceEditorInput; /** * The base common ancestor of the file to merge. diff --git a/src/vs/workbench/contrib/bracketPairColorizer2Telemetry/browser/bracketPairColorizer2Telemetry.contribution.ts b/src/vs/workbench/contrib/bracketPairColorizer2Telemetry/browser/bracketPairColorizer2Telemetry.contribution.ts new file mode 100644 index 00000000000..c1051e9d4cf --- /dev/null +++ b/src/vs/workbench/contrib/bracketPairColorizer2Telemetry/browser/bracketPairColorizer2Telemetry.contribution.ts @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { onUnexpectedError } from 'vs/base/common/errors'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; +import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; +import { EnablementState } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; + +class BracketPairColorizer2TelemetryContribution { + constructor( + @IConfigurationService private readonly configurationService: IConfigurationService, + @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, + @ITelemetryService private readonly telemetryService: ITelemetryService + ) { + this.init().catch(onUnexpectedError); + } + + private async init(): Promise<void> { + const bracketPairColorizerId = 'coenraads.bracket-pair-colorizer-2'; + + await this.extensionsWorkbenchService.queryLocal(); + const extension = this.extensionsWorkbenchService.installed.find(e => e.identifier.id === bracketPairColorizerId); + if ( + !extension || + ((extension.enablementState !== EnablementState.EnabledGlobally) && + (extension.enablementState !== EnablementState.EnabledWorkspace)) + ) { + return; + } + + const nativeBracketPairColorizationEnabledKey = 'editor.bracketPairColorization.enabled'; + const nativeColorizationEnabled = !!this.configurationService.getValue(nativeBracketPairColorizationEnabledKey); + + type BracketPairColorizer2InstalledClassification = { + owner: 'hediet'; + nativeColorizationEnabled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether or not built-in bracket pair colorization is being used' }; + comment: 'We use this to understand how many users have the bracket pair colorizer extension installed (and how many of them have native bracket pair colorization enabled), as the extension does not do anything if native bracket pair colorization is enabled.'; + }; + type BracketPairColorizer2Event = { + nativeColorizationEnabled: boolean; + }; + this.telemetryService.publicLog2<BracketPairColorizer2Event, BracketPairColorizer2InstalledClassification>('bracketPairColorizer2Usage', { + nativeColorizationEnabled + }); + } +} + +Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(BracketPairColorizer2TelemetryContribution, LifecyclePhase.Restored); + diff --git a/src/vs/workbench/contrib/codeEditor/browser/toggleMinimap.ts b/src/vs/workbench/contrib/codeEditor/browser/toggleMinimap.ts index 54fc34de741..f0ea2a89e33 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/toggleMinimap.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/toggleMinimap.ts @@ -20,7 +20,7 @@ export class ToggleMinimapAction extends Action2 { title: { value: localize('toggleMinimap', "Toggle Minimap"), original: 'Toggle Minimap', - mnemonicTitle: localize({ key: 'miShowMinimap', comment: ['&& denotes a mnemonic'] }, "Show &&Minimap") + mnemonicTitle: localize({ key: 'miMinimap', comment: ['&& denotes a mnemonic'] }, "&&Minimap") }, category: CATEGORIES.View, f1: true, diff --git a/src/vs/workbench/contrib/comments/browser/commentNode.ts b/src/vs/workbench/contrib/comments/browser/commentNode.ts index 450c76ae8ad..674ea47d406 100644 --- a/src/vs/workbench/contrib/comments/browser/commentNode.ts +++ b/src/vs/workbench/contrib/comments/browser/commentNode.ts @@ -528,7 +528,11 @@ export class CommentNode<T extends IRange | ICellRange> extends Disposable { this.updateCommentBody(newComment.body); } - if (newComment.mode !== undefined && newComment.mode !== this.comment.mode) { + const isChangingMode: boolean = newComment.mode !== undefined && newComment.mode !== this.comment.mode; + + this.comment = newComment; + + if (isChangingMode) { if (newComment.mode === languages.CommentMode.Editing) { this.switchToEditMode(); } else { @@ -536,8 +540,6 @@ export class CommentNode<T extends IRange | ICellRange> extends Disposable { } } - this.comment = newComment; - if (newComment.label) { this._isPendingLabel.innerText = newComment.label; } else { diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts index 99c4aae1ca9..c057977818c 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts @@ -456,6 +456,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget override show(rangeOrPos: IRange | IPosition, heightInLines: number): void { this._isExpanded = true; super.show(rangeOrPos, heightInLines); + this._commentThread.collapsibleState = languages.CommentThreadCollapsibleState.Expanded; this._refresh(this._commentThreadWidget.getDimensions()); } diff --git a/src/vs/workbench/contrib/debug/node/telemetryApp.ts b/src/vs/workbench/contrib/debug/node/telemetryApp.ts index 601ff4f9b1e..6296aeb862e 100644 --- a/src/vs/workbench/contrib/debug/node/telemetryApp.ts +++ b/src/vs/workbench/contrib/debug/node/telemetryApp.ts @@ -7,7 +7,7 @@ import { Server } from 'vs/base/parts/ipc/node/ipc.cp'; import { TelemetryAppenderChannel } from 'vs/platform/telemetry/common/telemetryIpc'; import { OneDataSystemAppender } from 'vs/platform/telemetry/node/1dsAppender'; -const appender = new OneDataSystemAppender(undefined, process.argv[2], JSON.parse(process.argv[3]), process.argv[4]); +const appender = new OneDataSystemAppender(false, process.argv[2], JSON.parse(process.argv[3]), process.argv[4]); process.once('exit', () => appender.flush()); const channel = new TelemetryAppenderChannel([appender]); diff --git a/src/vs/workbench/contrib/debug/test/node/streamDebugAdapter.test.ts b/src/vs/workbench/contrib/debug/test/node/streamDebugAdapter.test.ts index 06ea6440b6c..c0ffe9dd115 100644 --- a/src/vs/workbench/contrib/debug/test/node/streamDebugAdapter.test.ts +++ b/src/vs/workbench/contrib/debug/test/node/streamDebugAdapter.test.ts @@ -49,7 +49,13 @@ suite('Debug - StreamDebugAdapter', () => { const pipeName = crypto.randomBytes(10).toString('hex'); const pipePath = platform.isWindows ? join('\\\\.\\pipe\\', pipeName) : join(tmpdir(), pipeName); - const server = net.createServer(serverConnection).listen(pipePath); + const server = await new Promise<net.Server>((resolve, reject) => { + const server = net.createServer(serverConnection); + server.once('listening', () => resolve(server)); + server.once('error', reject); + server.listen(pipePath); + }); + const debugAdapter = new NamedPipeDebugAdapter({ type: 'pipeServer', path: pipePath diff --git a/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts b/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts index 9be6a5124f7..0602d52a480 100644 --- a/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts +++ b/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts @@ -10,7 +10,7 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle import { Action2, IAction2Options, registerAction2 } from 'vs/platform/actions/common/actions'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { localize } from 'vs/nls'; -import { IEditSessionsWorkbenchService, Change, ChangeType, Folder, EditSession, FileType, EDIT_SESSION_SYNC_CATEGORY, EditSessionSchemaVersion, IEditSessionsLogService } from 'vs/workbench/contrib/editSessions/common/editSessions'; +import { IEditSessionsWorkbenchService, Change, ChangeType, Folder, EditSession, FileType, EDIT_SESSION_SYNC_CATEGORY, EDIT_SESSIONS_CONTAINER_ID, EditSessionSchemaVersion, IEditSessionsLogService, EDIT_SESSIONS_VIEW_ICON, EDIT_SESSIONS_TITLE, EDIT_SESSIONS_SCHEME, EDIT_SESSIONS_SHOW_VIEW, EDIT_SESSIONS_SIGNED_IN } from 'vs/workbench/contrib/editSessions/common/editSessions'; import { ISCMRepository, ISCMService } from 'vs/workbench/contrib/scm/common/scm'; import { IFileService } from 'vs/platform/files/common/files'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; @@ -32,13 +32,20 @@ import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuratio import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { ContextKeyExpr, ContextKeyExpression, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, ContextKeyExpression, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { getVirtualWorkspaceLocation } from 'vs/platform/workspace/common/virtualWorkspace'; import { Schemas } from 'vs/base/common/network'; import { IsWebContext } from 'vs/platform/contextkey/common/contextkeys'; import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { EditSessionsLogService } from 'vs/workbench/contrib/editSessions/common/editSessionsLogService'; +import { IViewContainersRegistry, Extensions as ViewExtensions, ViewContainerLocation, IViewsService } from 'vs/workbench/common/views'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { EditSessionsDataViews } from 'vs/workbench/contrib/editSessions/browser/editSessionsViews'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { EditSessionsContentProvider } from 'vs/workbench/contrib/editSessions/browser/editSessionsContentProvider'; registerSingleton(IEditSessionsLogService, EditSessionsLogService); registerSingleton(IEditSessionsWorkbenchService, EditSessionsWorkbenchService); @@ -63,6 +70,8 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo private registered = false; private continueEditSessionOptions: ContinueEditSessionItem[] = []; + private readonly shouldShowViewsContext: IContextKey<boolean>; + constructor( @IEditSessionsWorkbenchService private readonly editSessionsWorkbenchService: IEditSessionsWorkbenchService, @IFileService private readonly fileService: IFileService, @@ -74,9 +83,11 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo @IDialogService private readonly dialogService: IDialogService, @IEditSessionsLogService private readonly logService: IEditSessionsLogService, @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IInstantiationService private readonly instantiationService: IInstantiationService, @IProductService private readonly productService: IProductService, @IConfigurationService private configurationService: IConfigurationService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, + @ITextModelService textModelResolverService: ITextModelService, @IQuickInputService private readonly quickInputService: IQuickInputService, @ICommandService private commandService: ICommandService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @@ -101,6 +112,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo }); this.registerActions(); + this.registerViews(); continueEditSessionExtPoint.setHandler(extensions => { const continueEditSessionOptions: ContinueEditSessionItem[] = []; @@ -128,6 +140,26 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo } this.continueEditSessionOptions = continueEditSessionOptions; }); + + this.shouldShowViewsContext = EDIT_SESSIONS_SHOW_VIEW.bindTo(this.contextKeyService); + + textModelResolverService.registerTextModelContentProvider(EDIT_SESSIONS_SCHEME, instantiationService.createInstance(EditSessionsContentProvider)); + } + + private registerViews() { + const container = Registry.as<IViewContainersRegistry>(ViewExtensions.ViewContainersRegistry).registerViewContainer( + { + id: EDIT_SESSIONS_CONTAINER_ID, + title: EDIT_SESSIONS_TITLE, + ctorDescriptor: new SyncDescriptor( + ViewPaneContainer, + [EDIT_SESSIONS_CONTAINER_ID, { mergeViewWithContainerWhenSingleView: true }] + ), + icon: EDIT_SESSIONS_VIEW_ICON, + hideIfEmpty: true + }, ViewContainerLocation.Sidebar + ); + this._register(this.instantiationService.createInstance(EditSessionsDataViews, container)); } private registerActions() { @@ -143,9 +175,32 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo this.registerContinueInLocalFolderAction(); + this.registerShowEditSessionViewAction(); + this.registered = true; } + private registerShowEditSessionViewAction() { + const that = this; + this._register(registerAction2(class ShowEditSessionView extends Action2 { + constructor() { + super({ + id: 'workbench.editSessions.actions.showEditSessions', + title: { value: localize('show edit session', "Show Edit Sessions"), original: 'Show Edit Sessions' }, + category: EDIT_SESSION_SYNC_CATEGORY, + f1: true, + precondition: EDIT_SESSIONS_SIGNED_IN + }); + } + + async run(accessor: ServicesAccessor) { + that.shouldShowViewsContext.set(true); + const viewsService = accessor.get(IViewsService); + await viewsService.openViewContainer(EDIT_SESSIONS_CONTAINER_ID); + } + })); + } + private registerContinueEditSessionAction() { const that = this; this._register(registerAction2(class ContinueEditSessionAction extends Action2 { @@ -195,7 +250,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo }); } - async run(accessor: ServicesAccessor): Promise<void> { + async run(accessor: ServicesAccessor, editSessionId?: string): Promise<void> { await that.progressService.withProgress({ location: ProgressLocation.Notification, title: localize('resuming edit session', 'Resuming edit session...') @@ -206,7 +261,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo }; that.telemetryService.publicLog2<ResumeEvent, ResumeClassification>('editSessions.resume'); - await that.resumeEditSession(); + await that.resumeEditSession(editSessionId); }); } })); @@ -536,7 +591,7 @@ Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfigurat 'workbench.experimental.editSessions.enabled': { 'type': 'boolean', 'tags': ['experimental', 'usesOnlineServices'], - 'default': false, + 'default': true, 'markdownDescription': localize('editSessionsEnabled', "Controls whether to display cloud-enabled actions to store and resume uncommitted changes when switching between web, desktop, or devices."), }, } diff --git a/src/vs/workbench/contrib/editSessions/browser/editSessionsContentProvider.ts b/src/vs/workbench/contrib/editSessions/browser/editSessionsContentProvider.ts new file mode 100644 index 00000000000..30d56a6c398 --- /dev/null +++ b/src/vs/workbench/contrib/editSessions/browser/editSessionsContentProvider.ts @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from 'vs/base/common/uri'; +import { ITextModel } from 'vs/editor/common/model'; +import { IModelService } from 'vs/editor/common/services/model'; +import { ITextModelContentProvider } from 'vs/editor/common/services/resolverService'; +import { EDIT_SESSIONS_SCHEME, IEditSessionsWorkbenchService } from 'vs/workbench/contrib/editSessions/common/editSessions'; + +export class EditSessionsContentProvider implements ITextModelContentProvider { + + constructor( + @IEditSessionsWorkbenchService private editSessionsWorkbenchService: IEditSessionsWorkbenchService, + @IModelService private readonly modelService: IModelService, + ) { } + + async provideTextContent(uri: URI): Promise<ITextModel | null> { + let model: ITextModel | null = null; + if (uri.scheme === EDIT_SESSIONS_SCHEME) { + const match = /(?<ref>[^/]+)\/(?<folderName>[^/]+)\/(?<filePath>.*)/.exec(uri.path.substring(1)); + if (match?.groups) { + const { ref, folderName, filePath } = match.groups; + const data = await this.editSessionsWorkbenchService.read(ref); + const content = data?.editSession.folders.find((f) => f.name === folderName)?.workingChanges.find((change) => change.relativeFilePath === filePath)?.contents; + if (content) { + model = this.modelService.createModel(content, null, uri); + } + } + } + return model; + } +} diff --git a/src/vs/workbench/contrib/editSessions/browser/editSessionsViews.ts b/src/vs/workbench/contrib/editSessions/browser/editSessionsViews.ts new file mode 100644 index 00000000000..36b0c497939 --- /dev/null +++ b/src/vs/workbench/contrib/editSessions/browser/editSessionsViews.ts @@ -0,0 +1,188 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { localize } from 'vs/nls'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { TreeView, TreeViewPane } from 'vs/workbench/browser/parts/views/treeView'; +import { Extensions, ITreeItem, ITreeViewDataProvider, ITreeViewDescriptor, IViewsRegistry, TreeItemCollapsibleState, TreeViewItemHandleArg, ViewContainer } from 'vs/workbench/common/views'; +import { EDIT_SESSIONS_SCHEME, EDIT_SESSIONS_SHOW_VIEW, EDIT_SESSIONS_SIGNED_IN, EDIT_SESSIONS_TITLE, IEditSessionsWorkbenchService } from 'vs/workbench/contrib/editSessions/common/editSessions'; +import { URI } from 'vs/base/common/uri'; +import { fromNow } from 'vs/base/common/date'; +import { Codicon } from 'vs/base/common/codicons'; +import { API_OPEN_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; +import { registerAction2, Action2, MenuId } from 'vs/platform/actions/common/actions'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; + +export class EditSessionsDataViews extends Disposable { + constructor( + container: ViewContainer, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(); + this.registerViews(container); + } + + private registerViews(container: ViewContainer): void { + const viewId = 'workbench.views.editSessions.data'; + const name = localize('edit sessions data', 'All Sessions'); + const treeView = this.instantiationService.createInstance(TreeView, viewId, name); + treeView.showCollapseAllAction = true; + treeView.showRefreshAction = true; + const disposable = treeView.onDidChangeVisibility(visible => { + if (visible && !treeView.dataProvider) { + disposable.dispose(); + treeView.dataProvider = this.instantiationService.createInstance(EditSessionDataViewDataProvider); + } + }); + Registry.as<IViewsRegistry>(Extensions.ViewsRegistry).registerViews([<ITreeViewDescriptor>{ + id: viewId, + name, + ctorDescriptor: new SyncDescriptor(TreeViewPane), + canToggleVisibility: true, + canMoveView: false, + treeView, + collapsed: false, + when: ContextKeyExpr.and(EDIT_SESSIONS_SIGNED_IN, EDIT_SESSIONS_SHOW_VIEW), + order: 100, + hideByDefault: true, + }], container); + + registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.editSessions.actions.resume', + title: localize('workbench.editSessions.actions.resume', "Resume Edit Session"), + icon: Codicon.repoPull, + menu: { + id: MenuId.ViewItemContext, + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', viewId), ContextKeyExpr.regex('viewItem', /edit-session/i)), + group: 'inline' + } + }); + } + + async run(accessor: ServicesAccessor, handle: TreeViewItemHandleArg): Promise<void> { + const editSessionId = URI.parse(handle.$treeItemHandle).path.substring(1); + const commandService = accessor.get(ICommandService); + await commandService.executeCommand('workbench.experimental.editSessions.actions.resumeLatest', editSessionId); + await treeView.refresh(); + } + }); + + registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.editSessions.actions.delete', + title: localize('workbench.editSessions.actions.delete', "Delete Edit Session"), + icon: Codicon.trash, + menu: { + id: MenuId.ViewItemContext, + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', viewId), ContextKeyExpr.regex('viewItem', /edit-session/i)), + group: 'inline' + } + }); + } + + async run(accessor: ServicesAccessor, handle: TreeViewItemHandleArg): Promise<void> { + const editSessionId = URI.parse(handle.$treeItemHandle).path.substring(1); + const dialogService = accessor.get(IDialogService); + const editSessionWorkbenchService = accessor.get(IEditSessionsWorkbenchService); + const result = await dialogService.confirm({ + message: localize('confirm delete', 'Are you sure you want to permanently delete the edit session with ref {0}? You cannot undo this action.', editSessionId), + type: 'warning', + title: EDIT_SESSIONS_TITLE + }); + if (result.confirmed) { + await editSessionWorkbenchService.delete(editSessionId); + await treeView.refresh(); + } + } + }); + } +} + +class EditSessionDataViewDataProvider implements ITreeViewDataProvider { + constructor( + @IEditSessionsWorkbenchService private readonly editSessionsWorkbenchService: IEditSessionsWorkbenchService + ) { } + + async getChildren(element?: ITreeItem): Promise<ITreeItem[]> { + if (!element) { + return this.getAllEditSessions(); + } + + const [ref, folderName, filePath] = URI.parse(element.handle).path.substring(1).split('/'); + + if (ref && !folderName) { + return this.getEditSession(ref); + } else if (ref && folderName && !filePath) { + return this.getEditSessionFolderContents(ref, folderName); + } + + return []; + } + + private async getAllEditSessions(): Promise<ITreeItem[]> { + const allEditSessions = await this.editSessionsWorkbenchService.list(); + return allEditSessions.map((session) => { + const resource = URI.from({ scheme: EDIT_SESSIONS_SCHEME, authority: 'remote-session-content', path: `/${session.ref}` }); + return { + handle: resource.toString(), + collapsibleState: TreeItemCollapsibleState.Collapsed, + label: { label: session.ref }, + description: fromNow(session.created, true), + themeIcon: Codicon.repo, + contextValue: `edit-session` + }; + }); + } + + private async getEditSession(ref: string): Promise<ITreeItem[]> { + const data = await this.editSessionsWorkbenchService.read(ref); + + if (!data) { + return []; + } + + return data.editSession.folders.map((folder) => { + const resource = URI.from({ scheme: EDIT_SESSIONS_SCHEME, authority: 'remote-session-content', path: `/${data.ref}/${folder.name}` }); + return { + handle: resource.toString(), + collapsibleState: TreeItemCollapsibleState.Collapsed, + label: { label: folder.name }, + themeIcon: Codicon.folder + }; + }); + } + + private async getEditSessionFolderContents(ref: string, folderName: string): Promise<ITreeItem[]> { + const data = await this.editSessionsWorkbenchService.read(ref); + + if (!data) { + return []; + } + + return (data.editSession.folders.find((folder) => folder.name === folderName)?.workingChanges ?? []).map((change) => { + const resource = URI.from({ scheme: EDIT_SESSIONS_SCHEME, authority: 'remote-session-content', path: `/${data.ref}/${folderName}/${change.relativeFilePath}` }); + return { + handle: resource.toString(), + resourceUri: resource, + collapsibleState: TreeItemCollapsibleState.None, + label: { label: change.relativeFilePath }, + themeIcon: Codicon.file, + command: { + id: API_OPEN_EDITOR_COMMAND_ID, + title: localize('open file', 'Open File'), + arguments: [resource, undefined, undefined] + } + }; + }); + } +} diff --git a/src/vs/workbench/contrib/editSessions/browser/editSessionsWorkbenchService.ts b/src/vs/workbench/contrib/editSessions/browser/editSessionsWorkbenchService.ts index b63b550ab35..d8dbac59900 100644 --- a/src/vs/workbench/contrib/editSessions/browser/editSessionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/editSessions/browser/editSessionsWorkbenchService.ts @@ -14,7 +14,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { IRequestService } from 'vs/platform/request/common/request'; import { IStorageService, IStorageValueChangeEvent, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { createSyncHeaders, IAuthenticationProvider } from 'vs/platform/userDataSync/common/userDataSync'; +import { createSyncHeaders, IAuthenticationProvider, IResourceRefHandle } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncStoreClient } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; import { AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -122,6 +122,21 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes } } + async list(): Promise<IResourceRefHandle[]> { + await this.initialize(); + if (!this.initialized) { + throw new Error(`Unable to list edit sessions.`); + } + + try { + return this.storeClient?.getAllRefs('editSessions') ?? []; + } catch (ex) { + this.logService.error(ex); + } + + return []; + } + private async initialize() { if (this.initialized) { return; diff --git a/src/vs/workbench/contrib/editSessions/common/editSessions.ts b/src/vs/workbench/contrib/editSessions/common/editSessions.ts index 789976a50e5..da791afa9df 100644 --- a/src/vs/workbench/contrib/editSessions/common/editSessions.ts +++ b/src/vs/workbench/contrib/editSessions/common/editSessions.ts @@ -3,11 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Codicon } from 'vs/base/common/codicons'; import { localize } from 'vs/nls'; import { ILocalizedString } from 'vs/platform/action/common/action'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; +import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; +import { IResourceRefHandle } from 'vs/platform/userDataSync/common/userDataSync'; export const EDIT_SESSION_SYNC_CATEGORY: ILocalizedString = { original: 'Edit Sessions', @@ -21,6 +24,7 @@ export interface IEditSessionsWorkbenchService { read(ref: string | undefined): Promise<{ ref: string; editSession: EditSession } | undefined>; write(editSession: EditSession): Promise<string>; delete(ref: string): Promise<void>; + list(): Promise<IResourceRefHandle[]>; } export const IEditSessionsLogService = createDecorator<IEditSessionsLogService>('IEditSessionsLogService'); @@ -65,3 +69,12 @@ export interface EditSession { export const EDIT_SESSIONS_SIGNED_IN_KEY = 'editSessionsSignedIn'; export const EDIT_SESSIONS_SIGNED_IN = new RawContextKey<boolean>(EDIT_SESSIONS_SIGNED_IN_KEY, false); + +export const EDIT_SESSIONS_CONTAINER_ID = 'workbench.view.editSessions'; +export const EDIT_SESSIONS_TITLE = localize('edit sessions', 'Edit Sessions'); + +export const EDIT_SESSIONS_VIEW_ICON = registerIcon('edit-sessions-view-icon', Codicon.cloudDownload, localize('editSessionViewIcon', 'View icon of the edit sessions view.')); + +export const EDIT_SESSIONS_SHOW_VIEW = new RawContextKey<boolean>('editSessionsShowView', false); + +export const EDIT_SESSIONS_SCHEME = 'vscode-edit-sessions'; diff --git a/src/vs/workbench/contrib/editSessions/test/browser/editSessions.test.ts b/src/vs/workbench/contrib/editSessions/test/browser/editSessions.test.ts index 07917f285e5..138436bc52f 100644 --- a/src/vs/workbench/contrib/editSessions/test/browser/editSessions.test.ts +++ b/src/vs/workbench/contrib/editSessions/test/browser/editSessions.test.ts @@ -28,6 +28,12 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { TestEnvironmentService } from 'vs/workbench/test/browser/workbenchTestServices'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { Event } from 'vs/base/common/event'; +import { IViewDescriptorService } from 'vs/workbench/common/views'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; const folderName = 'test-folder'; const folderUri = URI.file(`/${folderName}`); @@ -76,6 +82,17 @@ suite('Edit session sync', () => { // Stub repositories instantiationService.stub(ISCMService, '_repositories', new Map()); + instantiationService.stub(IContextKeyService, new MockContextKeyService()); + instantiationService.stub(IThemeService, new class extends mock<IThemeService>() { + override onDidColorThemeChange = Event.None; + override onDidFileIconThemeChange = Event.None; + }); + instantiationService.stub(IViewDescriptorService, { + onDidChangeLocation: Event.None + }); + instantiationService.stub(ITextModelService, new class extends mock<ITextModelService>() { + override registerTextModelContentProvider = () => ({ dispose: () => { } }); + }); editSessionsContribution = instantiationService.createInstance(EditSessionsContribution); }); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index b85bfe7485f..1282bccb531 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -60,7 +60,7 @@ interface InstalledExtensionsEvent { } interface ExtensionsLoadClassification extends GDPRClassification<InstalledExtensionsEvent> { owner: 'digitarald'; - comment: 'Helps to understand which extensions are the most actively used.', + comment: 'Helps to understand which extensions are the most actively used.'; readonly extensionIds: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'The list of extension ids that are installed.' }; readonly count: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'The number of extensions that are installed.' }; } diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts index 6040e37a790..030f0b5ca87 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts @@ -216,6 +216,7 @@ suite('ExtensionRecommendationsService Test', () => { onDidInstallExtensions: didInstallEvent.event, onUninstallExtension: uninstallEvent.event, onDidUninstallExtension: didUninstallEvent.event, + onDidChangeProfileExtensions: Event.None, async getInstalled() { return []; }, async canInstall() { return true; }, async getExtensionsControlManifest() { return { malicious: [], deprecated: {} }; }, @@ -447,7 +448,7 @@ suite('ExtensionRecommendationsService Test', () => { }); }); - test.skip('ExtensionRecommendationsService: Able to retrieve collection of all ignored recommendations', async () => { + test('ExtensionRecommendationsService: Able to retrieve collection of all ignored recommendations', async () => { const storageService = instantiationService.get(IStorageService); const workspaceIgnoredRecommendations = ['ms-dotnettools.csharp']; // ignore a stored recommendation and a workspace recommendation. @@ -462,9 +463,7 @@ suite('ExtensionRecommendationsService Test', () => { await testObject.activationPromise; const recommendations = testObject.getAllRecommendationsWithReason(); - assert.ok(recommendations['ms-python.python'], 'ms-python.python extension shall exist'); - assert.ok(!recommendations['mockpublisher2.mockextension2'], 'mockpublisher2.mockextension2 extension shall not exist'); - assert.ok(!recommendations['ms-dotnettools.csharp'], 'ms-dotnettools.csharp extension shall not exist'); + assert.deepStrictEqual(Object.keys(recommendations), ['ms-python.python', 'mockpublisher1.mockextension1']); }); test('ExtensionRecommendationsService: Able to dynamically ignore/unignore global recommendations', async () => { diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index e90d052ae29..6e15cf4d9c8 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -989,7 +989,7 @@ registerAction2(class extends Action2 { menu: { id: MenuId.ViewTitle, group: 'navigation', - when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), ViewHasSomeCollapsibleRootItemContext), + when: ContextKeyExpr.equals('view', VIEW_ID), order: 40 } }); @@ -1001,26 +1001,3 @@ registerAction2(class extends Action2 { explorerView.collapseAll(); } }); - -registerAction2(class extends Action2 { - constructor() { - super({ - id: 'workbench.files.action.expandExplorerFolders', - title: { value: nls.localize('expandExplorerFolders', "Expand Folders in Explorer"), original: 'Expand Folders in Explorer' }, - f1: true, - icon: Codicon.expandAll, - menu: { - id: MenuId.ViewTitle, - group: 'navigation', - when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), ViewHasSomeCollapsibleRootItemContext.toNegated()), - order: 40 - } - }); - } - - run(accessor: ServicesAccessor) { - const viewsService = accessor.get(IViewsService); - const explorerView = viewsService.getViewWithId(VIEW_ID) as ExplorerView; - explorerView.expandAll(); - } -}); diff --git a/src/vs/workbench/contrib/markers/browser/markersTable.ts b/src/vs/workbench/contrib/markers/browser/markersTable.ts index 4d39773daed..3aaaa1c9df6 100644 --- a/src/vs/workbench/contrib/markers/browser/markersTable.ts +++ b/src/vs/workbench/contrib/markers/browser/markersTable.ts @@ -411,7 +411,7 @@ export class MarkersTable extends Disposable implements IProblemsWidget { const items: MarkerTableItem[] = []; for (const resourceMarker of this.resourceMarkers) { for (const marker of resourceMarker.markers) { - if (marker.resource.scheme === network.Schemas.walkThrough || marker.resource.scheme === network.Schemas.walkThroughSnippet) { + if (marker.resource.scheme === network.Schemas.walkThrough || marker.resource.scheme === network.Schemas.walkThroughSnippet || marker.resource.scheme === network.Schemas.vscodeSourceControl) { continue; } diff --git a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts index 778ae2c5497..103d6512066 100644 --- a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts +++ b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts @@ -19,7 +19,7 @@ import { IDisposable, dispose, Disposable, toDisposable, DisposableStore } from import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { QuickFixAction, QuickFixActionViewItem } from 'vs/workbench/contrib/markers/browser/markersViewActions'; import { ILabelService } from 'vs/platform/label/common/label'; -import { basename, isEqual } from 'vs/base/common/resources'; +import { dirname, basename, isEqual } from 'vs/base/common/resources'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { ITreeFilter, TreeVisibility, TreeFilterResult, ITreeRenderer, ITreeNode } from 'vs/base/browser/ui/tree/tree'; import { FilterOptions } from 'vs/workbench/contrib/markers/browser/markersFilterOptions'; @@ -185,7 +185,7 @@ export class ResourceMarkersRenderer implements ITreeRenderer<ResourceMarkers, R if (this.fileService.hasProvider(resourceMarkers.resource) || resourceMarkers.resource.scheme === network.Schemas.untitled) { templateData.resourceLabel.setFile(resourceMarkers.resource, { matches: uriMatches }); } else { - templateData.resourceLabel.setResource({ name: resourceMarkers.name, description: this.labelService.getUriLabel(resourceMarkers.resource, { relative: true }), resource: resourceMarkers.resource }, { matches: uriMatches }); + templateData.resourceLabel.setResource({ name: resourceMarkers.name, description: this.labelService.getUriLabel(dirname(resourceMarkers.resource), { relative: true }), resource: resourceMarkers.resource }, { matches: uriMatches }); } this.updateCount(node, templateData); @@ -456,7 +456,7 @@ export class Filter implements ITreeFilter<MarkerElement, FilterData> { } private filterResourceMarkers(resourceMarkers: ResourceMarkers): TreeFilterResult<FilterData> { - if (resourceMarkers.resource.scheme === network.Schemas.walkThrough || resourceMarkers.resource.scheme === network.Schemas.walkThroughSnippet) { + if (resourceMarkers.resource.scheme === network.Schemas.walkThrough || resourceMarkers.resource.scheme === network.Schemas.walkThroughSnippet || resourceMarkers.resource.scheme === network.Schemas.vscodeSourceControl) { return false; } diff --git a/src/vs/workbench/contrib/mergeEditor/browser/commands/commands.ts b/src/vs/workbench/contrib/mergeEditor/browser/commands/commands.ts index 3c028ec8ebd..2f1dfdaef94 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/commands/commands.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/commands/commands.ts @@ -7,17 +7,37 @@ import { Codicon } from 'vs/base/common/codicons'; import { URI, UriComponents } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { ILocalizedString } from 'vs/platform/action/common/action'; -import { Action2, MenuId } from 'vs/platform/actions/common/actions'; +import { Action2, IAction2Options, MenuId } from 'vs/platform/actions/common/actions'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { EditorResolution } from 'vs/platform/editor/common/editor'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { API_OPEN_DIFF_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; -import { IResourceMergeEditorInput } from 'vs/workbench/common/editor'; import { MergeEditorInput, MergeEditorInputData } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; import { MergeEditor } from 'vs/workbench/contrib/mergeEditor/browser/view/mergeEditor'; +import { MergeEditorViewModel } from 'vs/workbench/contrib/mergeEditor/browser/view/viewModel'; import { ctxIsMergeEditor, ctxMergeEditorLayout } from 'vs/workbench/contrib/mergeEditor/common/mergeEditor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +abstract class MergeEditorAction extends Action2 { + constructor(desc: Readonly<IAction2Options>) { + super(desc); + } + + run(accessor: ServicesAccessor): void { + const { activeEditorPane } = accessor.get(IEditorService); + if (activeEditorPane instanceof MergeEditor) { + const vm = activeEditorPane.viewModel.get(); + if (!vm) { + return; + } + this.runWithViewModel(vm, accessor); + } + } + + abstract runWithViewModel(viewModel: MergeEditorViewModel, accessor: ServicesAccessor): void; +} + export class OpenMergeEditor extends Action2 { constructor() { super({ @@ -28,14 +48,15 @@ export class OpenMergeEditor extends Action2 { run(accessor: ServicesAccessor, ...args: unknown[]): void { const validatedArgs = IRelaxedOpenArgs.validate(args[0]); - const input: IResourceMergeEditorInput = { - base: { resource: validatedArgs.base }, - input1: { resource: validatedArgs.input1.uri, label: validatedArgs.input1.title, description: validatedArgs.input1.description, detail: validatedArgs.input1.detail }, - input2: { resource: validatedArgs.input2.uri, label: validatedArgs.input2.title, description: validatedArgs.input2.description, detail: validatedArgs.input2.detail }, - result: { resource: validatedArgs.output }, - options: { preserveFocus: true } - }; - accessor.get(IEditorService).openEditor(input); + const instaService = accessor.get(IInstantiationService); + const input = instaService.createInstance( + MergeEditorInput, + validatedArgs.base, + validatedArgs.input1, + validatedArgs.input2, + validatedArgs.output, + ); + accessor.get(IEditorService).openEditor(input, { preserveFocus: true, override: EditorResolution.DISABLED }); } } @@ -167,7 +188,7 @@ const mergeEditorCategory: ILocalizedString = { original: 'Merge Editor', }; -export class OpenResultResource extends Action2 { +export class OpenResultResource extends MergeEditorAction { constructor() { super({ id: 'merge.openResult', @@ -187,15 +208,13 @@ export class OpenResultResource extends Action2 { }); } - async run(accessor: ServicesAccessor): Promise<void> { + override runWithViewModel(viewModel: MergeEditorViewModel, accessor: ServicesAccessor): void { const editorService = accessor.get(IEditorService); - if (editorService.activeEditor instanceof MergeEditorInput) { - editorService.openEditor({ resource: editorService.activeEditor.result }); - } + editorService.openEditor({ resource: viewModel.model.result.uri }); } } -export class GoToNextConflict extends Action2 { +export class GoToNextConflict extends MergeEditorAction { constructor() { super({ id: 'merge.goToNextConflict', @@ -218,15 +237,12 @@ export class GoToNextConflict extends Action2 { }); } - run(accessor: ServicesAccessor): void { - const { activeEditorPane } = accessor.get(IEditorService); - if (activeEditorPane instanceof MergeEditor) { - activeEditorPane.viewModel.get()?.goToNextModifiedBaseRange(true); - } + override runWithViewModel(viewModel: MergeEditorViewModel): void { + viewModel.goToNextModifiedBaseRange(true); } } -export class GoToPreviousConflict extends Action2 { +export class GoToPreviousConflict extends MergeEditorAction { constructor() { super({ id: 'merge.goToPreviousConflict', @@ -252,15 +268,12 @@ export class GoToPreviousConflict extends Action2 { }); } - run(accessor: ServicesAccessor): void { - const { activeEditorPane } = accessor.get(IEditorService); - if (activeEditorPane instanceof MergeEditor) { - activeEditorPane.viewModel.get()?.goToPreviousModifiedBaseRange(true); - } + override runWithViewModel(viewModel: MergeEditorViewModel): void { + viewModel.goToPreviousModifiedBaseRange(true); } } -export class ToggleActiveConflictInput1 extends Action2 { +export class ToggleActiveConflictInput1 extends MergeEditorAction { constructor() { super({ id: 'merge.toggleActiveConflictInput1', @@ -277,19 +290,12 @@ export class ToggleActiveConflictInput1 extends Action2 { }); } - run(accessor: ServicesAccessor): void { - const { activeEditorPane } = accessor.get(IEditorService); - if (activeEditorPane instanceof MergeEditor) { - const vm = activeEditorPane.viewModel.get(); - if (!vm) { - return; - } - vm.toggleActiveConflict(1); - } + override runWithViewModel(viewModel: MergeEditorViewModel): void { + viewModel.toggleActiveConflict(1); } } -export class ToggleActiveConflictInput2 extends Action2 { +export class ToggleActiveConflictInput2 extends MergeEditorAction { constructor() { super({ id: 'merge.toggleActiveConflictInput2', @@ -306,19 +312,12 @@ export class ToggleActiveConflictInput2 extends Action2 { }); } - run(accessor: ServicesAccessor): void { - const { activeEditorPane } = accessor.get(IEditorService); - if (activeEditorPane instanceof MergeEditor) { - const vm = activeEditorPane.viewModel.get(); - if (!vm) { - return; - } - vm.toggleActiveConflict(2); - } + override runWithViewModel(viewModel: MergeEditorViewModel): void { + viewModel.toggleActiveConflict(2); } } -export class CompareInput1WithBaseCommand extends Action2 { +export class CompareInput1WithBaseCommand extends MergeEditorAction { constructor() { super({ id: 'mergeEditor.compareInput1WithBase', @@ -336,14 +335,14 @@ export class CompareInput1WithBaseCommand extends Action2 { menu: { id: MenuId.MergeInput1Toolbar } }); } - run(accessor: ServicesAccessor, ...args: unknown[]): void { - const editorService = accessor.get(IEditorService); + + override runWithViewModel(viewModel: MergeEditorViewModel, accessor: ServicesAccessor): void { const commandService = accessor.get(ICommandService); - mergeEditorCompare(editorService, commandService, 1); + mergeEditorCompare(viewModel, commandService, 1); } } -export class CompareInput2WithBaseCommand extends Action2 { +export class CompareInput2WithBaseCommand extends MergeEditorAction { constructor() { super({ id: 'mergeEditor.compareInput2WithBase', @@ -361,32 +360,25 @@ export class CompareInput2WithBaseCommand extends Action2 { menu: { id: MenuId.MergeInput2Toolbar } }); } - run(accessor: ServicesAccessor, ...args: unknown[]): void { - const editorService = accessor.get(IEditorService); + + override runWithViewModel(viewModel: MergeEditorViewModel, accessor: ServicesAccessor): void { const commandService = accessor.get(ICommandService); - mergeEditorCompare(editorService, commandService, 2); + mergeEditorCompare(viewModel, commandService, 2); } } -function mergeEditorCompare(editorService: IEditorService, commandService: ICommandService, inputNumber: 1 | 2) { - const { activeEditorPane } = editorService; - if (activeEditorPane instanceof MergeEditor) { - if (!activeEditorPane.model) { - return; - } - - const base = activeEditorPane.model.base.uri; - const input = inputNumber === 1 ? activeEditorPane.model.input1.uri : activeEditorPane.model.input2.uri; - - openDiffEditor(commandService, base, input); - } +function mergeEditorCompare(viewModel: MergeEditorViewModel, commandService: ICommandService, inputNumber: 1 | 2) { + const model = viewModel.model; + const base = model.base.uri; + const input = inputNumber === 1 ? model.input1.uri : model.input2.uri; + openDiffEditor(commandService, base, input); } function openDiffEditor(commandService: ICommandService, left: URI, right: URI, label?: string) { commandService.executeCommand(API_OPEN_DIFF_EDITOR_COMMAND_ID, left, right, label); } -export class OpenBaseFile extends Action2 { +export class OpenBaseFile extends MergeEditorAction { constructor() { super({ id: 'merge.openBaseEditor', @@ -400,15 +392,58 @@ export class OpenBaseFile extends Action2 { }); } - run(accessor: ServicesAccessor): void { + override runWithViewModel(viewModel: MergeEditorViewModel, accessor: ServicesAccessor): void { const openerService = accessor.get(IOpenerService); - const { activeEditorPane } = accessor.get(IEditorService); - if (activeEditorPane instanceof MergeEditor) { - const vm = activeEditorPane.viewModel.get(); - if (!vm) { - return; - } - openerService.open(vm.model.base.uri); - } + openerService.open(viewModel.model.base.uri); + } +} + +export class AcceptAllInput1 extends MergeEditorAction { + constructor() { + super({ + id: 'merge.acceptAllInput1', + category: mergeEditorCategory, + title: { + value: localize( + 'merge.acceptAllInput1', + 'Accept All Changes from Left' + ), + original: 'Accept All Changes from Left', + }, + f1: true, + precondition: ctxIsMergeEditor, + menu: [ + { id: MenuId.MergeInput1Toolbar, } + ] + }); + } + + override runWithViewModel(viewModel: MergeEditorViewModel): void { + viewModel.acceptAll(1); + } +} + +export class AcceptAllInput2 extends MergeEditorAction { + constructor() { + super({ + id: 'merge.acceptAllInput2', + category: mergeEditorCategory, + title: { + value: localize( + 'merge.acceptAllInput2', + 'Accept All Changes from Right' + ), + original: 'Accept All Changes from Right', + }, + f1: true, + precondition: ctxIsMergeEditor, + menu: [ + { id: MenuId.MergeInput2Toolbar, } + ] + }); + } + + override runWithViewModel(viewModel: MergeEditorViewModel): void { + viewModel.acceptAll(2); } } diff --git a/src/vs/workbench/contrib/mergeEditor/browser/commands/devCommands.ts b/src/vs/workbench/contrib/mergeEditor/browser/commands/devCommands.ts index 0f2b38ea5b4..17df90e7d26 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/commands/devCommands.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/commands/devCommands.ts @@ -10,15 +10,16 @@ import { localize } from 'vs/nls'; import { Action2 } from 'vs/platform/actions/common/actions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; -import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { MergeEditor } from 'vs/workbench/contrib/mergeEditor/browser/view/mergeEditor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IWorkbenchFileService } from 'vs/workbench/services/files/common/files'; import { URI } from 'vs/base/common/uri'; -import { IResourceMergeEditorInput } from 'vs/workbench/common/editor'; +import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; import { ctxIsMergeEditor } from 'vs/workbench/contrib/mergeEditor/common/mergeEditor'; +import { EditorResolution } from 'vs/platform/editor/common/editor'; interface MergeEditorContents { languageId: string; @@ -98,6 +99,8 @@ export class MergeEditorOpenContents extends Action2 { async run(accessor: ServicesAccessor): Promise<void> { const service = accessor.get(IWorkbenchFileService); + const instaService = accessor.get(IInstantiationService); + const editorService = accessor.get(IEditorService); const inputService = accessor.get(IQuickInputService); const clipboardService = accessor.get(IClipboardService); const textModelService = accessor.get(ITextModelService); @@ -151,12 +154,13 @@ export class MergeEditorOpenContents extends Action2 { setLanguageId(resultUri, content.languageId), ]); - const input: IResourceMergeEditorInput = { - base: { resource: baseUri }, - input1: { resource: input1Uri, label: 'Input 1', description: 'Input 1', detail: '(from JSON)' }, - input2: { resource: input2Uri, label: 'Input 2', description: 'Input 2', detail: '(from JSON)' }, - result: { resource: resultUri }, - }; - accessor.get(IEditorService).openEditor(input); + const input = instaService.createInstance( + MergeEditorInput, + baseUri, + { uri: input1Uri, title: 'Input 1', description: 'Input 1', detail: '(from JSON)' }, + { uri: input2Uri, title: 'Input 2', description: 'Input 2', detail: '(from JSON)' }, + resultUri, + ); + editorService.openEditor(input, { override: EditorResolution.DISABLED }); } } diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts index c06a6e0447c..8120b688a8e 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts @@ -10,7 +10,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { EditorPaneDescriptor, IEditorPaneRegistry } from 'vs/workbench/browser/editor'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { EditorExtensions, IEditorFactoryRegistry } from 'vs/workbench/common/editor'; -import { CompareInput1WithBaseCommand, CompareInput2WithBaseCommand, GoToNextConflict, GoToPreviousConflict, OpenBaseFile, OpenMergeEditor, OpenResultResource, SetColumnLayout, SetMixedLayout, ToggleActiveConflictInput1, ToggleActiveConflictInput2 } from 'vs/workbench/contrib/mergeEditor/browser/commands/commands'; +import { AcceptAllInput1, AcceptAllInput2, CompareInput1WithBaseCommand, CompareInput2WithBaseCommand, GoToNextConflict, GoToPreviousConflict, OpenBaseFile, OpenMergeEditor, OpenResultResource, SetColumnLayout, SetMixedLayout, ToggleActiveConflictInput1, ToggleActiveConflictInput2 } from 'vs/workbench/contrib/mergeEditor/browser/commands/commands'; import { MergeEditorCopyContentsToJSON, MergeEditorOpenContents } from 'vs/workbench/contrib/mergeEditor/browser/commands/devCommands'; import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; import { MergeEditor, MergeEditorResolverContribution, MergeEditorOpenHandlerContribution } from 'vs/workbench/contrib/mergeEditor/browser/view/mergeEditor'; @@ -51,6 +51,9 @@ registerAction2(ToggleActiveConflictInput2); registerAction2(CompareInput1WithBaseCommand); registerAction2(CompareInput2WithBaseCommand); +registerAction2(AcceptAllInput1); +registerAction2(AcceptAllInput2); + Registry .as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench) diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts index 50470d8df66..9a4286f26b6 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts @@ -140,8 +140,8 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput implements override toUntyped(): IResourceMergeEditorInput { return { - input1: { resource: this.input1.uri, label: this.input1.title, description: this.input1.description, detail: this.input1.detail }, - input2: { resource: this.input2.uri, label: this.input2.title, description: this.input2.description, detail: this.input2.detail }, + input1: { resource: this.input1.uri, label: this.input1.title, description: this.input1.description }, + input2: { resource: this.input2.uri, label: this.input2.title, description: this.input2.description }, base: { resource: this.base }, result: { resource: this.result }, options: { diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts index 46aee59e160..b17a1e8eebf 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts @@ -47,7 +47,7 @@ import { MergeEditorViewModel } from 'vs/workbench/contrib/mergeEditor/browser/v import { ctxMergeBaseUri, ctxIsMergeEditor, ctxMergeEditorLayout, ctxMergeResultUri, MergeEditorLayoutTypes } from 'vs/workbench/contrib/mergeEditor/common/mergeEditor'; import { settingsSashBorder } from 'vs/workbench/contrib/preferences/common/settingsEditorColorRegistry'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IEditorResolverService, MergeEditorInputFactoryFunction, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; +import { EditorInputFactoryFunction, IEditorResolverService, MergeEditorInputFactoryFunction, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import './colors'; import { InputCodeEditorView } from './editors/inputCodeEditorView'; @@ -559,6 +559,28 @@ export class MergeEditorResolverContribution extends Disposable { ) { super(); + const editorInputFactory: EditorInputFactoryFunction = (editor) => { + return { + editor: instantiationService.createInstance( + MergeEditorInput, + editor.resource, + { + uri: editor.resource, + title: '', + description: '', + detail: '' + }, + { + uri: editor.resource, + title: '', + description: '', + detail: '' + }, + editor.resource + ) + }; + }; + const mergeEditorInputFactory: MergeEditorInputFactoryFunction = (mergeEditor: IResourceMergeEditorInput): EditorInputWithOptions => { return { editor: instantiationService.createInstance( @@ -566,15 +588,15 @@ export class MergeEditorResolverContribution extends Disposable { mergeEditor.base.resource, { uri: mergeEditor.input1.resource, - title: mergeEditor.input1.label ?? basename(mergeEditor.input1.resource), - description: mergeEditor.input1.description ?? '', - detail: mergeEditor.input1.detail + title: basename(mergeEditor.input1.resource), + description: '', + detail: '' }, { uri: mergeEditor.input2.resource, - title: mergeEditor.input2.label ?? basename(mergeEditor.input2.resource), - description: mergeEditor.input2.description ?? '', - detail: mergeEditor.input2.detail + title: basename(mergeEditor.input2.resource), + description: '', + detail: '' }, mergeEditor.result.resource ) @@ -584,13 +606,14 @@ export class MergeEditorResolverContribution extends Disposable { this._register(editorResolverService.registerEditor( `*`, { - id: DEFAULT_EDITOR_ASSOCIATION.id, - label: DEFAULT_EDITOR_ASSOCIATION.displayName, + id: MergeEditorInput.ID, + label: localize('editor.mergeEditor.label', "Merge Editor"), detail: DEFAULT_EDITOR_ASSOCIATION.providerDisplayName, - priority: RegisteredEditorPriority.builtin + priority: RegisteredEditorPriority.option }, {}, { + createEditorInput: editorInputFactory, createMergeEditorInput: mergeEditorInputFactory } )); diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts index de910cceaee..37643b3fb6b 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts @@ -143,4 +143,17 @@ export class MergeEditorViewModel { ); }); } + + public acceptAll(inputNumber: 1 | 2): void { + transaction(tx => { + /** @description Toggle Active Conflict */ + for (const range of this.model.modifiedBaseRanges.get()) { + this.setState( + range, + this.model.getState(range).get().withInputValue(inputNumber, true), + tx + ); + } + }); + } } diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/markdownCell.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/markdownCell.ts index 870d4b2d719..813a6ef4354 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/markdownCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/markdownCell.ts @@ -329,7 +329,7 @@ export class StatefulMarkdownCell extends Disposable { width: width, height: editorHeight }, - enableDropIntoEditor: true, + dropIntoEditor: { enabled: true }, // overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode() }, { contributions: this.notebookEditor.creationOptions.cellEditorContributions diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts index 949c8dd25b5..fff16d43316 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -275,7 +275,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende width: 0, height: 0 }, - enableDropIntoEditor: true, + dropIntoEditor: { enabled: true }, }, { contributions: this.notebookEditor.creationOptions.cellEditorContributions }); diff --git a/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts b/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts index 91045b0fb95..bd8178fcec7 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts +++ b/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts @@ -281,7 +281,7 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr } // Show for remote windows on the desktop, but not when in code server web - if (this.remoteAuthority && !isWeb) { + if (this.remoteAuthority && (!isWeb || this.environmentService.options?.webSocketFactory)) { const hostLabel = this.labelService.getHostLabel(Schemas.vscodeRemote, this.remoteAuthority) || this.remoteAuthority; switch (this.connectionState) { case 'initializing': diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index f3d20816324..001c769f36a 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -1067,7 +1067,6 @@ export class TunnelPanelDescriptor implements IViewDescriptor { readonly ctorDescriptor: SyncDescriptor<TunnelPanel>; readonly canToggleVisibility = true; readonly hideByDefault = false; - readonly workspace = true; // group is not actually used for views that are not extension contributed. Use order instead. readonly group = 'details@0'; // -500 comes from the remote explorer viewOrderDelegate diff --git a/src/vs/workbench/contrib/scm/browser/activity.ts b/src/vs/workbench/contrib/scm/browser/activity.ts index 57e7afbf13a..75929726a57 100644 --- a/src/vs/workbench/contrib/scm/browser/activity.ts +++ b/src/vs/workbench/contrib/scm/browser/activity.ts @@ -19,10 +19,6 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' import { stripIcons } from 'vs/base/common/iconLabels'; import { Schemas } from 'vs/base/common/network'; import { Iterable } from 'vs/base/common/iterator'; -import { ILabelService } from 'vs/platform/label/common/label'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { URI } from 'vs/base/common/uri'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; function getCount(repository: ISCMRepository): number { if (typeof repository.provider.count === 'number') { @@ -276,46 +272,3 @@ export class SCMActiveResourceContextKeyController implements IWorkbenchContribu this.repositoryDisposables.clear(); } } - -class SCMInputTextDocumentLabelFormatter { - - readonly separator = '/'; - - readonly label = (resource: URI) => { - const match = /git\/(?<repositoryId>scm[\d+])\/input/i.exec(resource.path); - - if (match?.groups === undefined) { - return resource.toString(); - } - - const { repositoryId } = match.groups; - const repository = this.scmService.getRepository(repositoryId); - - if (repository === undefined || repository.provider.rootUri === undefined) { - return resource.toString(); - } - - const folder = this.workspaceContextService.getWorkspaceFolder(repository.provider.rootUri); - const repositoryName = folder?.uri.toString() === repository.provider.rootUri.toString() ? folder.name : basename(repository.provider.rootUri); - - return `${repositoryName} (${repository.provider.label})`; - }; - - constructor( - @ISCMService private readonly scmService: ISCMService, - @IWorkspaceContextService private workspaceContextService: IWorkspaceContextService, - ) { } -} - -export class SCMInputTextDocumentContribution implements IWorkbenchContribution { - constructor( - @IInstantiationService instantiationService: IInstantiationService, - @ILabelService labelService: ILabelService - ) { - labelService.registerFormatter({ - scheme: Schemas.vscode, - authority: 'scm', - formatting: instantiationService.createInstance(SCMInputTextDocumentLabelFormatter) - }); - } -} diff --git a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts index 6c0ceafb8ac..956e867a21f 100644 --- a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts +++ b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts @@ -10,7 +10,7 @@ import { DirtyDiffWorkbenchController } from './dirtydiffDecorator'; import { VIEWLET_ID, ISCMService, VIEW_PANE_ID, ISCMProvider, ISCMViewService, REPOSITORIES_VIEW_PANE_ID } from 'vs/workbench/contrib/scm/common/scm'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; -import { SCMActiveResourceContextKeyController, SCMInputTextDocumentContribution, SCMStatusController } from './activity'; +import { SCMActiveResourceContextKeyController, SCMStatusController } from './activity'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; @@ -115,9 +115,6 @@ Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench) Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench) .registerWorkbenchContribution(SCMStatusController, LifecyclePhase.Restored); -Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench) - .registerWorkbenchContribution(SCMInputTextDocumentContribution, LifecyclePhase.Restored); - Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).registerConfiguration({ id: 'scm', order: 5, diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index e809b5cb465..32a0d626d66 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -1788,9 +1788,8 @@ class SCMInputWidget { } const uri = URI.from({ - scheme: Schemas.vscode, - authority: 'scm', - path: `/${input.repository.provider.contextValue}/${input.repository.provider.id}/input`, + scheme: Schemas.vscodeSourceControl, + path: `${input.repository.provider.contextValue}/${input.repository.provider.id}/input`, query }); @@ -1964,7 +1963,7 @@ class SCMInputWidget { scrollbar: { alwaysConsumeMouseWheel: false }, overflowWidgetsDomNode, renderWhitespace: 'none', - enableDropIntoEditor: true, + dropIntoEditor: { enabled: true }, accessibilitySupport }; diff --git a/src/vs/workbench/contrib/search/browser/searchActions.ts b/src/vs/workbench/contrib/search/browser/searchActions.ts index dc0023470ab..4b503feac04 100644 --- a/src/vs/workbench/contrib/search/browser/searchActions.ts +++ b/src/vs/workbench/contrib/search/browser/searchActions.ts @@ -456,7 +456,8 @@ class ReplaceActionRunner { async performReplace(element: FolderMatch | FileMatch | Match): Promise<any> { // since multiple elements can be selected, we need to check the type of the FolderMatch/FileMatch/Match before we perform the replace. - const elementsToReplace = getElementsToOperateOnInfo(this.viewer, element, this.configurationService.getValue<ISearchConfigurationProperties>('search')); + const opInfo = getElementsToOperateOnInfo(this.viewer, element, this.configurationService.getValue<ISearchConfigurationProperties>('search')); + const elementsToReplace = opInfo.elements; await Promise.all(elementsToReplace.map(async (elem) => { const parent = elem.parent(); @@ -585,11 +586,12 @@ export class RemoveAction extends AbstractSearchAndReplaceAction { } override run(): Promise<any> { - const elementsToRemove = getElementsToOperateOnInfo(this.viewer, this.element, this.configurationService.getValue<ISearchConfigurationProperties>('search')); + const opInfo = getElementsToOperateOnInfo(this.viewer, this.element, this.configurationService.getValue<ISearchConfigurationProperties>('search')); + const elementsToRemove = opInfo.elements; const currentBottomFocusElement = elementsToRemove[elementsToRemove.length - 1]; - const nextFocusElement = !currentBottomFocusElement || currentBottomFocusElement instanceof SearchResult || arrayContainsElementOrParent(currentBottomFocusElement, elementsToRemove) ? + const nextFocusElement = opInfo.mustReselect && (!currentBottomFocusElement || currentBottomFocusElement instanceof SearchResult || arrayContainsElementOrParent(currentBottomFocusElement, elementsToRemove)) ? this.getElementToFocusAfterRemoved(this.viewer, <any>currentBottomFocusElement) : null; @@ -823,13 +825,15 @@ export const focusSearchListCommand: ICommandHandler = accessor => { }); }; -function getElementsToOperateOnInfo(viewer: WorkbenchObjectTree<RenderableMatch, void>, currElement: RenderableMatch, sortConfig: ISearchConfigurationProperties): RenderableMatch[] { +function getElementsToOperateOnInfo(viewer: WorkbenchObjectTree<RenderableMatch, void>, currElement: RenderableMatch, sortConfig: ISearchConfigurationProperties): { elements: RenderableMatch[]; mustReselect: boolean } { let elements: RenderableMatch[] = viewer.getSelection().filter((x): x is RenderableMatch => x !== null).sort((a, b) => searchComparer(a, b, sortConfig.sortOrder)); + const mustReselect = elements.includes(currElement); // this indicates whether we need to re-focus/re-select on a remove. + // if selection doesn't include multiple elements, just return current focus element. if (!(elements.length > 1 && elements.includes(currElement))) { elements = [currElement]; } - return elements; + return { elements, mustReselect }; } diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index dbe5a62e3be..9df0e983136 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -340,7 +340,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer processContext.set(process && !isVirtual); } this._onDidRegisterSupportedExecutions.fire(); - if (this._jsonTasksSupported && !this._tasksReconnected) { + if (this._configurationService.getValue(TaskSettingId.Reconnection) === true && this._jsonTasksSupported && !this._tasksReconnected) { this._reconnectTasks(); } } diff --git a/src/vs/workbench/contrib/tasks/browser/task.contribution.ts b/src/vs/workbench/contrib/tasks/browser/task.contribution.ts index 658d86f53b2..9e62a8604dc 100644 --- a/src/vs/workbench/contrib/tasks/browser/task.contribution.ts +++ b/src/vs/workbench/contrib/tasks/browser/task.contribution.ts @@ -513,6 +513,12 @@ configurationRegistry.registerConfiguration({ description: nls.localize('task.showDecorations', "Shows decorations at points of interest in the terminal buffer such as the first problem found via a watch task. Note that this will only take effect for future tasks."), default: true }, + [TaskSettingId.Reconnection]: { + type: 'boolean', + description: nls.localize('task.experimental.reconnection', "On window reload, reconnect to running watch/background tasks. Note that this is experimental, so you could encounter issues."), + default: false, + tags: ['experimental'] + }, [TaskSettingId.SaveBeforeRun]: { markdownDescription: nls.localize( 'task.saveBeforeRun', diff --git a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts index 22c53792466..ff02f515527 100644 --- a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts @@ -30,14 +30,14 @@ import { URI } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService } from 'vs/platform/log/common/log'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IShellLaunchConfig, TerminalLocation, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; +import { IShellLaunchConfig, TerminalLocation, TerminalSettingId, WaitOnExitValue } from 'vs/platform/terminal/common/terminal'; import { formatMessageForTerminal } from 'vs/platform/terminal/common/terminalStrings'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { IViewDescriptorService, IViewsService, ViewContainerLocation } from 'vs/workbench/common/views'; import { TaskTerminalStatus } from 'vs/workbench/contrib/tasks/browser/taskTerminalStatus'; import { ProblemCollectorEventKind, ProblemHandlingStrategy, StartStopProblemCollector, WatchingProblemCollector } from 'vs/workbench/contrib/tasks/common/problemCollectors'; import { GroupKind } from 'vs/workbench/contrib/tasks/common/taskConfiguration'; -import { CommandOptions, CommandString, ContributedTask, CustomTask, DependsOrder, ICommandConfiguration, IExtensionTaskSource, InMemoryTask, IShellConfiguration, IShellQuotingOptions, ITaskEvent, PanelKind, RevealKind, RevealProblemKind, RuntimeType, ShellQuoting, Task, TaskEvent, TaskEventKind, TaskScope, TaskSettingId, TaskSourceKind } from 'vs/workbench/contrib/tasks/common/tasks'; +import { CommandOptions, CommandString, ContributedTask, CustomTask, DependsOrder, ICommandConfiguration, IConfigurationProperties, IExtensionTaskSource, InMemoryTask, IPresentationOptions, IShellConfiguration, IShellQuotingOptions, ITaskEvent, PanelKind, RevealKind, RevealProblemKind, RuntimeType, ShellQuoting, Task, TaskEvent, TaskEventKind, TaskScope, TaskSettingId, TaskSourceKind } from 'vs/workbench/contrib/tasks/common/tasks'; import { ITaskService } from 'vs/workbench/contrib/tasks/common/taskService'; import { IResolvedVariables, IResolveSet, ITaskExecuteResult, ITaskResolver, ITaskSummary, ITaskSystem, ITaskSystemInfo, ITaskSystemInfoResolver, ITaskTerminateResponse, TaskError, TaskErrors, TaskExecuteKind, Triggers } from 'vs/workbench/contrib/tasks/common/taskSystem'; import { ITerminalGroupService, ITerminalInstance, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; @@ -284,6 +284,11 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { } if (this._tasksToReconnect.includes(task._id)) { this._terminalForTask = terminals.find(t => t.shellLaunchConfig.attachPersistentProcess?.task?.id === task._id); + // Restore the waitOnExit value of the terminal because it may have been a function + // that cannot be persisted in the pty host + if ('command' in task && task.command.presentation && this._terminalForTask) { + this._terminalForTask.waitOnExit = getWaitOnExitValue(task.command.presentation, task.configurationProperties); + } this.run(task, resolver, trigger); } return undefined; @@ -541,35 +546,38 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { alreadyResolved = alreadyResolved ?? new Map<string, string>(); const promises: Promise<ITaskSummary>[] = []; if (task.configurationProperties.dependsOn) { - for (const dependency of task.configurationProperties.dependsOn) { - const dependencyTask = await resolver.resolve(dependency.uri, dependency.task!); - if (dependencyTask) { - this._adoptConfigurationForDependencyTask(dependencyTask, task); - const key = dependencyTask.getMapKey(); - let promise = this._activeTasks[key] ? this._getDependencyPromise(this._activeTasks[key]) : undefined; - if (!promise) { - this._fireTaskEvent(TaskEvent.create(TaskEventKind.DependsOnStarted, task)); - encounteredDependencies.add(task.getCommonTaskId()); - promise = this._executeDependencyTask(dependencyTask, resolver, trigger, encounteredDependencies, alreadyResolved); - } - promises.push(promise); - if (task.configurationProperties.dependsOrder === DependsOrder.sequence) { - const promiseResult = await promise; - if (promiseResult.exitCode === 0) { - promise = Promise.resolve(promiseResult); - } else { - promise = Promise.reject(promiseResult); - break; + if (!this._terminalForTask) { + // we already handle dependent tasks when reconnecting, don't create extras + for (const dependency of task.configurationProperties.dependsOn) { + const dependencyTask = await resolver.resolve(dependency.uri, dependency.task!); + if (dependencyTask) { + this._adoptConfigurationForDependencyTask(dependencyTask, task); + const key = dependencyTask.getMapKey(); + let promise = this._activeTasks[key] ? this._getDependencyPromise(this._activeTasks[key]) : undefined; + if (!promise) { + this._fireTaskEvent(TaskEvent.create(TaskEventKind.DependsOnStarted, task)); + encounteredDependencies.add(task.getCommonTaskId()); + promise = this._executeDependencyTask(dependencyTask, resolver, trigger, encounteredDependencies, alreadyResolved); + } + promises.push(promise); + if (task.configurationProperties.dependsOrder === DependsOrder.sequence) { + const promiseResult = await promise; + if (promiseResult.exitCode === 0) { + promise = Promise.resolve(promiseResult); + } else { + promise = Promise.reject(promiseResult); + break; + } } + promises.push(promise); + } else { + this._log(nls.localize('dependencyFailed', + 'Couldn\'t resolve dependent task \'{0}\' in workspace folder \'{1}\'', + Types.isString(dependency.task) ? dependency.task : JSON.stringify(dependency.task, undefined, 0), + dependency.uri.toString() + )); + this._showOutput(); } - promises.push(promise); - } else { - this._log(nls.localize('dependencyFailed', - 'Couldn\'t resolve dependent task \'{0}\' in workspace folder \'{1}\'', - Types.isString(dependency.task) ? dependency.task : JSON.stringify(dependency.task, undefined, 0), - dependency.uri.toString() - )); - this._showOutput(); } } } @@ -951,7 +959,6 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { this._fireTaskEvent(TaskEvent.create(TaskEventKind.ProcessStarted, task, terminal!.processId!)); processStartedSignaled = true; } - this._fireTaskEvent(TaskEvent.create(TaskEventKind.ProcessEnded, task, exitCode ?? undefined)); for (let i = 0; i < eventCounter; i++) { @@ -1065,7 +1072,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { return needsFolderQualification ? task.getQualifiedLabel() : (task.configurationProperties.name || ''); } - private async _createShellLaunchConfig(task: CustomTask | ContributedTask, workspaceFolder: IWorkspaceFolder | undefined, variableResolver: VariableResolver, platform: Platform.Platform, options: CommandOptions, command: CommandString, args: CommandString[], waitOnExit: boolean | string | ((exitCode: number) => string)): Promise<IShellLaunchConfig | undefined> { + private async _createShellLaunchConfig(task: CustomTask | ContributedTask, workspaceFolder: IWorkspaceFolder | undefined, variableResolver: VariableResolver, platform: Platform.Platform, options: CommandOptions, command: CommandString, args: CommandString[], waitOnExit: WaitOnExitValue): Promise<IShellLaunchConfig | undefined> { let shellLaunchConfig: IShellLaunchConfig; const isShellCommand = task.command.runtime === RuntimeType.Shell; const needsFolderQualification = this._contextService.getWorkbenchState() === WorkbenchState.WORKSPACE; @@ -1338,24 +1345,10 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { const options = await this._resolveOptions(resolver, task.command.options); const presentationOptions = task.command.presentation; - let waitOnExit: boolean | string | ((exitCode: number) => string) = false; if (!presentationOptions) { throw new Error('Task presentation options should not be undefined here.'); } - - if ((presentationOptions.close === undefined) || (presentationOptions.close === false)) { - if ((presentationOptions.reveal !== RevealKind.Never) || !task.configurationProperties.isBackground || (presentationOptions.close === false)) { - if (presentationOptions.panel === PanelKind.New) { - waitOnExit = taskShellIntegrationWaitOnExitSequence(nls.localize('closeTerminal', 'Press any key to close the terminal.')); - } else if (presentationOptions.showReuseMessage) { - waitOnExit = taskShellIntegrationWaitOnExitSequence(nls.localize('reuseTerminal', 'Terminal will be reused by tasks, press any key to close it.')); - } else { - waitOnExit = true; - } - } - } else { - waitOnExit = !presentationOptions.close; - } + const waitOnExit = getWaitOnExitValue(presentationOptions, task.configurationProperties); let command: CommandString | undefined; let args: CommandString[] | undefined; @@ -1384,7 +1377,6 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { return [undefined, new TaskError(Severity.Error, nls.localize('TerminalTaskSystem', 'Can\'t execute a shell command on an UNC drive using cmd.exe.'), TaskErrors.UnknownError)]; } } - const prefersSameTerminal = presentationOptions.panel === PanelKind.Dedicated; const allowsSharedTerminal = presentationOptions.panel === PanelKind.Shared; const group = presentationOptions.group; @@ -1787,6 +1779,21 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { } } +function getWaitOnExitValue(presentationOptions: IPresentationOptions, configurationProperties: IConfigurationProperties) { + if ((presentationOptions.close === undefined) || (presentationOptions.close === false)) { + if ((presentationOptions.reveal !== RevealKind.Never) || !configurationProperties.isBackground || (presentationOptions.close === false)) { + if (presentationOptions.panel === PanelKind.New) { + return taskShellIntegrationWaitOnExitSequence(nls.localize('closeTerminal', 'Press any key to close the terminal.')); + } else if (presentationOptions.showReuseMessage) { + return taskShellIntegrationWaitOnExitSequence(nls.localize('reuseTerminal', 'Terminal will be reused by tasks, press any key to close it.')); + } else { + return true; + } + } + } + return !presentationOptions.close; +} + function taskShellIntegrationWaitOnExitSequence(message: string): (exitCode: number) => string { return (exitCode) => { return `${VSCodeSequence(VSCodeOscPt.CommandFinished, exitCode.toString())}${message}`; diff --git a/src/vs/workbench/contrib/tasks/common/tasks.ts b/src/vs/workbench/contrib/tasks/common/tasks.ts index 2033ec632d9..cfa357558d2 100644 --- a/src/vs/workbench/contrib/tasks/common/tasks.ts +++ b/src/vs/workbench/contrib/tasks/common/tasks.ts @@ -1201,7 +1201,8 @@ export const enum TaskSettingId { QuickOpenDetail = 'task.quickOpen.detail', QuickOpenSkip = 'task.quickOpen.skip', QuickOpenShowAll = 'task.quickOpen.showAll', - AllowAutomaticTasks = 'task.allowAutomaticTasks' + AllowAutomaticTasks = 'task.allowAutomaticTasks', + Reconnection = 'task.experimental.reconnection' } export const enum TasksSchemaProperties { diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 45871689f60..ed6ba54d0eb 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -11,7 +11,7 @@ import { FindReplaceState } from 'vs/editor/contrib/find/browser/findState'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IKeyMods } from 'vs/platform/quickinput/common/quickInput'; import { ITerminalCapabilityStore, ITerminalCommand } from 'vs/platform/terminal/common/capabilities/capabilities'; -import { IExtensionTerminalProfile, IProcessPropertyMap, IShellIntegration, IShellLaunchConfig, ITerminalDimensions, ITerminalLaunchError, ITerminalProfile, ITerminalTabLayoutInfoById, ProcessPropertyType, TerminalExitReason, TerminalIcon, TerminalLocation, TerminalShellType, TitleEventSource } from 'vs/platform/terminal/common/terminal'; +import { IExtensionTerminalProfile, IProcessPropertyMap, IShellIntegration, IShellLaunchConfig, ITerminalDimensions, ITerminalLaunchError, ITerminalProfile, ITerminalTabLayoutInfoById, ProcessPropertyType, TerminalExitReason, TerminalIcon, TerminalLocation, TerminalShellType, TerminalType, TitleEventSource, WaitOnExitValue } from 'vs/platform/terminal/common/terminal'; import { IGenericMarkProperties } from 'vs/platform/terminal/common/terminalProcess'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; @@ -257,6 +257,11 @@ interface ITerminalEditorInputObject { readonly icon: TerminalIcon | undefined; readonly color: string | undefined; readonly hasChildProcesses?: boolean; + readonly task?: { label: string; id: string; lastTask: string; group?: string; waitOnExit?: WaitOnExitValue }; + readonly type?: TerminalType; + readonly isFeatureTerminal?: boolean; + readonly hideFromUser?: boolean; + readonly reconnectionOwner?: string; } export interface ISerializedTerminalEditorInput extends ITerminalEditorInputObject { @@ -514,6 +519,12 @@ export interface ITerminalInstance { readonly hasFocus: boolean; /** + * Get or set the behavior of the terminal when it closes. This was indented only to be called + * after reconnecting to a terminal. + */ + waitOnExit: WaitOnExitValue | undefined; + + /** * An event that fires when the terminal instance's title changes. */ onTitleChanged: Event<ITerminalInstance>; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalEditorSerializer.ts b/src/vs/workbench/contrib/terminal/browser/terminalEditorSerializer.ts index 2a8abde35cb..853b4eb98e7 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalEditorSerializer.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalEditorSerializer.ts @@ -43,7 +43,11 @@ export class TerminalInputSerializer implements IEditorSerializer { icon: instance.icon, color: instance.color, resource: instance.resource.toString(), - hasChildProcesses: instance.hasChildProcesses + hasChildProcesses: instance.hasChildProcesses, + task: instance.shellLaunchConfig.task, + type: instance.shellLaunchConfig.type, + isFeatureTerminal: instance.shellLaunchConfig.isFeatureTerminal, + hideFromUser: instance.shellLaunchConfig.hideFromUser, }; } } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 09736ecd0b0..fde509c864f 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -87,6 +87,7 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IGenericMarkProperties } from 'vs/platform/terminal/common/terminalProcess'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { getIconRegistry } from 'vs/platform/theme/common/iconRegistry'; +import { TaskSettingId } from 'vs/workbench/contrib/tasks/common/tasks'; const enum Constants { /** @@ -226,6 +227,11 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { xterm?: XtermTerminal; disableLayout: boolean = false; + get waitOnExit(): ITerminalInstance['waitOnExit'] { return this._shellLaunchConfig.attachPersistentProcess?.waitOnExit || this._shellLaunchConfig.waitOnExit; } + set waitOnExit(value: ITerminalInstance['waitOnExit']) { + this._shellLaunchConfig.waitOnExit = value; + } + get target(): TerminalLocation | undefined { return this._target; } set target(value: TerminalLocation | undefined) { if (this.xterm) { @@ -303,12 +309,13 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { get description(): string | undefined { if (this._description) { return this._description; - } else if (this._shellLaunchConfig.type) { - if (this._shellLaunchConfig.type === 'Task') { + } + const type = this.shellLaunchConfig.attachPersistentProcess?.type || this.shellLaunchConfig.type; + if (type) { + if (type === 'Task') { return nls.localize('terminalTypeTask', "Task"); - } else { - return nls.localize('terminalTypeLocal', "Local"); } + return nls.localize('terminalTypeLocal', "Local"); } return undefined; } @@ -407,6 +414,18 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // the resource is already set when it's been moved from another window this._resource = resource || getTerminalUri(this._workspaceContextService.getWorkspace().id, this.instanceId, this.title); + if (this._shellLaunchConfig.attachPersistentProcess?.hideFromUser) { + this._shellLaunchConfig.hideFromUser = this._shellLaunchConfig.attachPersistentProcess.hideFromUser; + } + + if (this._shellLaunchConfig.attachPersistentProcess?.isFeatureTerminal) { + this._shellLaunchConfig.isFeatureTerminal = this._shellLaunchConfig.attachPersistentProcess.isFeatureTerminal; + } + + if (this._shellLaunchConfig.attachPersistentProcess?.type) { + this._shellLaunchConfig.type = this._shellLaunchConfig.attachPersistentProcess.type; + } + if (this.shellLaunchConfig.cwd) { const cwdUri = typeof this._shellLaunchConfig.cwd === 'string' ? URI.from({ scheme: Schemas.file, @@ -537,7 +556,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._labelComputer?.refreshLabel(); } })); - this._workspaceContextService.onDidChangeWorkspaceFolders(() => this._labelComputer?.refreshLabel()); + this.addDisposable(this._workspaceContextService.onDidChangeWorkspaceFolders(() => this._labelComputer?.refreshLabel())); // Clear out initial data events after 10 seconds, hopefully extension hosts are up and // running at that point. @@ -677,7 +696,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._shutdownPersistentProcessId = shutdownPersistentProcessId; } get persistentProcessId(): number | undefined { return this._processManager.persistentProcessId ?? this._shutdownPersistentProcessId; } - get shouldPersist(): boolean { return (this._processManager.shouldPersist || this._shutdownPersistentProcessId !== undefined) && !this.shellLaunchConfig.isTransient; } + get shouldPersist(): boolean { return (this._processManager.shouldPersist || this._shutdownPersistentProcessId !== undefined) && !this.shellLaunchConfig.isTransient && (!this.reconnectionOwner || this._configurationService.getValue(TaskSettingId.Reconnection) === true); } /** * Create xterm.js instance and attach data listeners. @@ -760,11 +779,11 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._loadTypeAheadAddon(xterm); - this._configurationService.onDidChangeConfiguration(e => { + this.addDisposable(this._configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(TerminalSettingId.LocalEchoEnabled)) { this._loadTypeAheadAddon(xterm); } - }); + })); this._pathService.userHome().then(userHome => { this._userHome = userHome.fsPath; @@ -1764,18 +1783,19 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // Only trigger wait on exit when the exit was *not* triggered by the // user (via the `workbench.action.terminal.kill` command). - if (this._shellLaunchConfig.waitOnExit && this._processManager.processState !== ProcessState.KilledByUser) { + const waitOnExit = this.waitOnExit; + if (waitOnExit && this._processManager.processState !== ProcessState.KilledByUser) { this._xtermReadyPromise.then(xterm => { if (exitMessage) { xterm.raw.write(formatMessageForTerminal(exitMessage)); } - switch (typeof this._shellLaunchConfig.waitOnExit) { + switch (typeof waitOnExit) { case 'string': - xterm.raw.write(formatMessageForTerminal(this._shellLaunchConfig.waitOnExit, { excludeLeadingNewLine: true })); + xterm.raw.write(formatMessageForTerminal(waitOnExit, { excludeLeadingNewLine: true })); break; case 'function': if (this.exitCode !== undefined) { - xterm.raw.write(formatMessageForTerminal(this._shellLaunchConfig.waitOnExit(this.exitCode), { excludeLeadingNewLine: true })); + xterm.raw.write(formatMessageForTerminal(waitOnExit(this.exitCode), { excludeLeadingNewLine: true })); } break; } @@ -2367,12 +2387,12 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } // Recreate the process if the terminal has not yet been interacted with and it's not a - // special terminal (eg. task, extension terminal) + // special terminal (eg. extension terminal) if ( info.requiresAction && this._configHelper.config.environmentChangesRelaunch && !this._processManager.hasWrittenData && - (this.reconnectionOwner || !this._shellLaunchConfig.isFeatureTerminal) && + (!this._shellLaunchConfig.isFeatureTerminal || (this.reconnectionOwner && this._configurationService.getValue(TaskSettingId.Reconnection) === true)) && !this._shellLaunchConfig.customPtyImplementation && !this._shellLaunchConfig.isExtensionOwnedTerminal && !this._shellLaunchConfig.attachPersistentProcess @@ -2732,14 +2752,15 @@ export class TerminalLabelComputer extends Disposable { labelType: TerminalLabelType, reset?: boolean ) { + const type = this._instance.shellLaunchConfig.attachPersistentProcess?.type || this._instance.shellLaunchConfig.type; const templateProperties: ITerminalLabelTemplateProperties = { cwd: this._instance.cwd || this._instance.initialCwd || '', cwdFolder: '', workspaceFolder: this._instance.workspaceFolder ? path.basename(this._instance.workspaceFolder.uri.fsPath) : undefined, - local: this._instance.shellLaunchConfig.type === 'Local' ? this._instance.shellLaunchConfig.type : undefined, + local: type === 'Local' ? type : undefined, process: this._instance.processName, sequence: this._instance.sequence, - task: this._instance.shellLaunchConfig.type === 'Task' ? this._instance.shellLaunchConfig.type : undefined, + task: type === 'Task' ? type : undefined, fixedDimensions: this._instance.fixedCols ? (this._instance.fixedRows ? `\u2194${this._instance.fixedCols} \u2195${this._instance.fixedRows}` : `\u2194${this._instance.fixedCols}`) : (this._instance.fixedRows ? `\u2195${this._instance.fixedRows}` : ''), diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index 901505cb26a..a82ae60c449 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -36,6 +36,7 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { TaskSettingId } from 'vs/workbench/contrib/tasks/common/tasks'; /** The amount of time to consider terminal errors to be related to the launch */ const LAUNCHING_DURATION = 500; @@ -245,8 +246,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce // this is a copy of what the merged environment collection is on the remote side const env = await this._resolveEnvironment(backend, variableResolver, shellLaunchConfig); - - const shouldPersist = (!!shellLaunchConfig.reconnectionOwner || !shellLaunchConfig.isFeatureTerminal) && this._configHelper.config.enablePersistentSessions && !shellLaunchConfig.isTransient; + const shouldPersist = ((this._configurationService.getValue(TaskSettingId.Reconnection) && shellLaunchConfig.reconnectionOwner) || !shellLaunchConfig.isFeatureTerminal) && this._configHelper.config.enablePersistentSessions && !shellLaunchConfig.isTransient; if (shellLaunchConfig.attachPersistentProcess) { const result = await backend.attachToProcess(shellLaunchConfig.attachPersistentProcess.id); if (result) { @@ -318,7 +318,6 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce } this._process = newProcess; - this._setProcessState(ProcessState.Launching); // Add any capabilities inherit to the backend @@ -441,7 +440,6 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce remoteAuthority: undefined, os: OS }); - const activeWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot(Schemas.file); const initialCwd = await terminalEnvironment.getCwd( @@ -462,7 +460,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce windowsEnableConpty: this._configHelper.config.windowsEnableConpty && !isScreenReaderModeEnabled, environmentVariableCollections: this._extEnvironmentVariableCollection ? serializeEnvironmentVariableCollections(this._extEnvironmentVariableCollection.collections) : undefined }; - const shouldPersist = this._configHelper.config.enablePersistentSessions && (!!this.reconnectionOwner || !shellLaunchConfig.isFeatureTerminal); + const shouldPersist = ((this._configurationService.getValue(TaskSettingId.Reconnection) && shellLaunchConfig.reconnectionOwner) || !shellLaunchConfig.isFeatureTerminal) && this._configHelper.config.enablePersistentSessions && !shellLaunchConfig.isTransient; return await backend.createProcess(shellLaunchConfig, initialCwd, cols, rows, this._configHelper.config.unicodeVersion, env, options, shouldPersist); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index f315f11cd30..1d504f3334c 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -175,7 +175,6 @@ export class TerminalService implements ITerminalService { // we update detected profiles when an instance is created so that, // for example, we detect if you've installed a pwsh this.onDidCreateInstance(() => this._terminalProfileService.refreshAvailableProfiles()); - this._forwardInstanceHostEvents(this._terminalGroupService); this._forwardInstanceHostEvents(this._terminalEditorService); this._terminalGroupService.onDidChangeActiveGroup(this._onDidChangeActiveGroup.fire, this._onDidChangeActiveGroup); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts index d262afd51fb..2b1b063de2d 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts @@ -458,12 +458,14 @@ class TerminalTabsRenderer implements IListRenderer<ITerminalInstance, ITerminal disposeElement(instance: ITerminalInstance, index: number, templateData: ITerminalTabEntryTemplate): void { templateData.elementDisposables?.dispose(); templateData.elementDisposables = undefined; + templateData.actionBar.clear(); } disposeTemplate(templateData: ITerminalTabEntryTemplate): void { templateData.elementDisposables?.dispose(); templateData.elementDisposables = undefined; templateData.label.dispose(); + templateData.actionBar.clear(); } fillActionBar(instance: ITerminalInstance, template: ITerminalTabEntryTemplate): void { diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts b/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts index feff72648ca..964be5b8eb5 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts @@ -76,7 +76,7 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { this._register(this._contextMenuService.onDidHideContextMenu(() => this._contextMenuVisible = false)); this._hoverDelayer = this._register(new Delayer(this._configurationService.getValue('workbench.hover.delay'))); - this._configurationService.onDidChangeConfiguration(e => { + this._register(this._configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(TerminalSettingId.ShellIntegrationDecorationIcon) || e.affectsConfiguration(TerminalSettingId.ShellIntegrationDecorationIconSuccess) || e.affectsConfiguration(TerminalSettingId.ShellIntegrationDecorationIconError)) { @@ -92,8 +92,8 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { } this._updateDecorationVisibility(); } - }); - this._themeService.onDidColorThemeChange(() => this._refreshStyles(true)); + })); + this._register(this._themeService.onDidColorThemeChange(() => this._refreshStyles(true))); this._updateDecorationVisibility(); this._register(this._capabilities.onDidAddCapability(c => { if (c === TerminalCapability.CommandDetection) { diff --git a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts index 718807b102e..d42faa01aa9 100644 --- a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts +++ b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts @@ -815,11 +815,39 @@ class MultiRunTestDecoration extends RunTestDecoration implements ITestDecoratio allActions.push(new Action('testing.gutter.debugAll', localize('debug all test', 'Debug All Tests'), undefined, undefined, () => this.defaultDebug())); } + const testItems = this.tests.map(testItem => ({ + currentLabel: testItem.test.item.label, + testItem, + parent: testItem.test.parent, + })); + + const getLabelConflicts = (tests: typeof testItems) => { + const labelCount = new Map<string, number>(); + for (const test of tests) { + labelCount.set(test.currentLabel, (labelCount.get(test.currentLabel) || 0) + 1); + } + + return tests.filter(e => labelCount.get(e.currentLabel)! > 1); + }; + + let conflicts, hasParent = true; + while ((conflicts = getLabelConflicts(testItems)).length && hasParent) { + for (const conflict of conflicts) { + if (conflict.parent) { + const parent = this.testService.collection.getNodeById(conflict.parent); + conflict.currentLabel = parent?.item.label + ' > ' + conflict.currentLabel; + conflict.parent = parent?.parent ? parent.parent : null; + } else { + hasParent = false; + } + } + } + const disposable = new DisposableStore(); - const testSubmenus = this.tests.map(({ test, resultItem }) => { - const actions = this.getTestContextMenuActions(test, resultItem); + const testSubmenus = testItems.map(({ currentLabel, testItem }) => { + const actions = this.getTestContextMenuActions(testItem.test, testItem.resultItem); disposable.add(actions); - return new SubmenuAction(test.item.extId, stripIcons(test.item.label), actions.object); + return new SubmenuAction(testItem.test.item.extId, stripIcons(currentLabel), actions.object); }); return { object: Separator.join(allActions, testSubmenus), dispose: () => disposable.dispose() }; diff --git a/src/vs/workbench/contrib/testing/browser/testingProgressUiService.ts b/src/vs/workbench/contrib/testing/browser/testingProgressUiService.ts index 69b323858b3..e8f1733a68f 100644 --- a/src/vs/workbench/contrib/testing/browser/testingProgressUiService.ts +++ b/src/vs/workbench/contrib/testing/browser/testingProgressUiService.ts @@ -129,6 +129,7 @@ export class TestingProgressUiService extends Disposable implements ITestingProg if (!this.windowProg.value) { this.windowProg.value = this.instantiaionService.createInstance(UnmanagedProgress, { location: ProgressLocation.Window, + type: 'loading' }); this.testViewProg.value = this.instantiaionService.createInstance(UnmanagedProgress, { location: Testing.ViewletId, diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index d201a99a8a6..55f61b3c685 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -57,8 +57,10 @@ import { IUserDataInitializationService } from 'vs/workbench/services/userData/b import { MarkdownString } from 'vs/base/common/htmlContent'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { ctxIsMergeEditor, ctxMergeBaseUri } from 'vs/workbench/contrib/mergeEditor/common/mergeEditor'; +import { EditorResolution } from 'vs/platform/editor/common/editor'; const CONTEXT_CONFLICTS_SOURCES = new RawContextKey<string>('conflictsSources', ''); @@ -728,12 +730,14 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo for (const conflict of conflicts) { const remoteResourceName = localize({ key: 'remoteResourceName', comment: ['remote as in file in cloud'] }, "{0} (Remote)", basename(conflict.remoteResource)); const localResourceName = localize('localResourceName', "{0} (Local)", basename(conflict.remoteResource)); - await this.editorService.openEditor({ - input1: { resource: conflict.remoteResource, label: localize('Theirs', 'Theirs'), description: remoteResourceName }, - input2: { resource: conflict.localResource, label: localize('Yours', 'Yours'), description: localResourceName }, - base: { resource: conflict.baseResource }, - result: { resource: conflict.previewResource } - }); + const input = this.instantiationService.createInstance( + MergeEditorInput, + conflict.baseResource, + { title: localize('Yours', 'Yours'), description: localResourceName, detail: undefined, uri: conflict.localResource }, + { title: localize('Theirs', 'Theirs'), description: remoteResourceName, detail: undefined, uri: conflict.remoteResource }, + conflict.previewResource, + ); + await this.editorService.openEditor(input, { override: EditorResolution.DISABLED }); } } diff --git a/src/vs/workbench/electron-sandbox/desktop.contribution.ts b/src/vs/workbench/electron-sandbox/desktop.contribution.ts index 1dd0c84873b..6559a5f52b7 100644 --- a/src/vs/workbench/electron-sandbox/desktop.contribution.ts +++ b/src/vs/workbench/electron-sandbox/desktop.contribution.ts @@ -242,6 +242,7 @@ import { ModifierKeyEmitter } from 'vs/base/browser/dom'; type: 'boolean', description: localize('experimentalUseSandbox', "Experimental: When enabled, the window will have sandbox mode enabled via Electron API."), default: false, + 'scope': ConfigurationScope.APPLICATION, ignoreSync: true }, } diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index 54779255d79..0d197e84c16 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -137,8 +137,11 @@ export class NativeWindow extends Disposable { // Touchbar this.updateTouchbarMenu(); - // Potential data loss - this.notifyOnAppRootEditors(appRootUri); + // Potential data loss when editing files + // from within the installation app root + if (this.environmentService.isBuilt) { + this.notifyOnAppRootEditors(appRootUri); + } })); // prevent opening a real URL inside the window @@ -912,7 +915,7 @@ export class NativeWindow extends Disposable { input2: { resource: resources[1].resource }, base: { resource: resources[2].resource }, result: { resource: resources[3].resource }, - options: { pinned: true } + options: { pinned: true, override: 'mergeEditor.Input' } // TODO@bpasero remove the override once the resolver is ready }; editors.push(mergeEditor); } else if (diffMode && isResourceEditorInput(resources[0]) && isResourceEditorInput(resources[1])) { diff --git a/src/vs/workbench/services/configuration/browser/configurationService.ts b/src/vs/workbench/services/configuration/browser/configurationService.ts index 3dc4ea6419e..a1245856684 100644 --- a/src/vs/workbench/services/configuration/browser/configurationService.ts +++ b/src/vs/workbench/services/configuration/browser/configurationService.ts @@ -717,7 +717,7 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat if (e.preserveData) { await Promise.all([ this.copyProfileSettings(e.previous.settingsResource, e.profile.settingsResource), - this.fileService.copy(e.previous.tasksResource, e.profile.tasksResource) + this.copyProfileTasks(e.previous.tasksResource, e.profile.tasksResource) ]); } const promises: Promise<ConfigurationModel>[] = []; @@ -751,6 +751,12 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat await this.fileService.writeFile(to, VSBuffer.fromString(toContent)); } + private async copyProfileTasks(from: URI, to: URI): Promise<void> { + if (await this.fileService.exists(from)) { + await this.fileService.copy(from, to); + } + } + private onDefaultConfigurationChanged(configurationModel: ConfigurationModel, properties?: string[]): void { if (this.workspace) { const previousData = this._configuration.toData(); @@ -906,12 +912,12 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat private async onWorkspaceFolderConfigurationChanged(folder: IWorkspaceFolder): Promise<void> { const [folderConfiguration] = await this.loadFolderConfigurations([folder]); const previous = { data: this._configuration.toData(), workspace: this.workspace }; - const folderConfiguraitonChange = this._configuration.compareAndUpdateFolderConfiguration(folder.uri, folderConfiguration); + const folderConfigurationChange = this._configuration.compareAndUpdateFolderConfiguration(folder.uri, folderConfiguration); if (this.getWorkbenchState() === WorkbenchState.FOLDER) { const workspaceConfigurationChange = this._configuration.compareAndUpdateWorkspaceConfiguration(folderConfiguration); - this.triggerConfigurationChange(mergeChanges(folderConfiguraitonChange, workspaceConfigurationChange), previous, ConfigurationTarget.WORKSPACE); + this.triggerConfigurationChange(mergeChanges(folderConfigurationChange, workspaceConfigurationChange), previous, ConfigurationTarget.WORKSPACE); } else { - this.triggerConfigurationChange(folderConfiguraitonChange, previous, ConfigurationTarget.WORKSPACE_FOLDER); + this.triggerConfigurationChange(folderConfigurationChange, previous, ConfigurationTarget.WORKSPACE_FOLDER); } this.updateRestrictedSettings(); } diff --git a/src/vs/workbench/services/editor/browser/editorResolverService.ts b/src/vs/workbench/services/editor/browser/editorResolverService.ts index c00b46869c6..ec9fc2b563b 100644 --- a/src/vs/workbench/services/editor/browser/editorResolverService.ts +++ b/src/vs/workbench/services/editor/browser/editorResolverService.ts @@ -51,8 +51,7 @@ export class EditorResolverService extends Disposable implements IEditorResolver private static readonly conflictingDefaultsStorageID = 'editorOverrideService.conflictingDefaults'; // Data Stores - private _editors: Map<string | glob.IRelativePattern, Map<string, RegisteredEditors>> = new Map<string | glob.IRelativePattern, Map<string, RegisteredEditors>>(); - private _flattenedEditors: Map<string | glob.IRelativePattern, RegisteredEditors> = new Map(); + private _editors: Map<string | glob.IRelativePattern, RegisteredEditors> = new Map<string | glob.IRelativePattern, RegisteredEditors>(); private cache: Set<string> | undefined; constructor( @@ -236,29 +235,18 @@ export class EditorResolverService extends Disposable implements IEditorResolver ): IDisposable { let registeredEditor = this._editors.get(globPattern); if (registeredEditor === undefined) { - registeredEditor = new Map<string, RegisteredEditors>(); + registeredEditor = []; this._editors.set(globPattern, registeredEditor); } - - let editorsWithId = registeredEditor.get(editorInfo.id); - if (editorsWithId === undefined) { - editorsWithId = []; - } - const remove = insert(editorsWithId, { + const remove = insert(registeredEditor, { globPattern, editorInfo, options, editorFactoryObject }); - registeredEditor.set(editorInfo.id, editorsWithId); - this._flattenedEditors = this._flattenEditorsMap(); this._onDidChangeEditorRegistrations.fire(); return toDisposable(() => { remove(); - if (editorsWithId && editorsWithId.length === 0) { - registeredEditor?.delete(editorInfo.id); - } - this._flattenedEditors = this._flattenEditorsMap(); this._onDidChangeEditorRegistrations.fire(); }); } @@ -294,42 +282,10 @@ export class EditorResolverService extends Disposable implements IEditorResolver } /** - * Given the nested nature of the editors map, we should merge factories of the same glob and id to make it flat - */ - private _flattenEditorsMap() { - const editors = new Map<string | glob.IRelativePattern, RegisteredEditors>(); - for (const [glob, value] of this._editors) { - const registeredEditors: RegisteredEditors = []; - for (const editors of value.values()) { - let registeredEditor: RegisteredEditor | undefined = undefined; - // Merge all editors with the same id and glob pattern together - for (const editor of editors) { - if (!registeredEditor) { - registeredEditor = { - editorInfo: editor.editorInfo, - globPattern: editor.globPattern, - options: {}, - editorFactoryObject: {} - }; - } - // Merge options and factories - registeredEditor.options = { ...registeredEditor.options, ...editor.options }; - registeredEditor.editorFactoryObject = { ...registeredEditor.editorFactoryObject, ...editor.editorFactoryObject }; - } - if (registeredEditor) { - registeredEditors.push(registeredEditor); - } - } - editors.set(glob, registeredEditors); - } - return editors; - } - - /** * Returns all editors as an array. Possible to contain duplicates */ private get _registeredEditors(): RegisteredEditors { - return flatten(Array.from(this._flattenedEditors.values())); + return flatten(Array.from(this._editors.values())); } updateUserAssociations(globPattern: string, editorID: string): void { @@ -350,7 +306,7 @@ export class EditorResolverService extends Disposable implements IEditorResolver const userSettings = this.getAssociationsForResource(resource); const matchingEditors: RegisteredEditor[] = []; // Then all glob patterns - for (const [key, editors] of this._flattenedEditors) { + for (const [key, editors] of this._editors) { for (const editor of editors) { const foundInSettings = userSettings.find(setting => setting.viewType === editor.editorInfo.id); if ((foundInSettings && editor.editorInfo.priority !== RegisteredEditorPriority.exclusive) || globMatchesResource(key, resource)) { @@ -490,11 +446,6 @@ export class EditorResolverService extends Disposable implements IEditorResolver } } - // If no factory is above, return flow back to caller letting them know we could not resolve it - if (!selectedEditor.editorFactoryObject.createEditorInput) { - return; - } - // Respect options passed back const inputWithOptions = await selectedEditor.editorFactoryObject.createEditorInput(editor, group); options = inputWithOptions.options ?? options; @@ -788,7 +739,7 @@ export class EditorResolverService extends Disposable implements IEditorResolver const cacheStorage: Set<string> = new Set<string>(); // Store just the relative pattern pieces without any path info - for (const [globPattern, contribPoint] of this._flattenedEditors) { + for (const [globPattern, contribPoint] of this._editors) { const nonOptional = !!contribPoint.find(c => c.editorInfo.priority !== RegisteredEditorPriority.option && c.editorInfo.id !== DEFAULT_EDITOR_ASSOCIATION.id); // Don't keep a cache of the optional ones as those wouldn't be opened on start anyways if (!nonOptional) { diff --git a/src/vs/workbench/services/editor/common/editorResolverService.ts b/src/vs/workbench/services/editor/common/editorResolverService.ts index 9de7b992657..b4e47e8d4f2 100644 --- a/src/vs/workbench/services/editor/common/editorResolverService.ts +++ b/src/vs/workbench/services/editor/common/editorResolverService.ts @@ -19,7 +19,6 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { EditorInputWithOptions, EditorInputWithOptionsAndGroup, IResourceDiffEditorInput, IResourceMergeEditorInput, IUntitledTextResourceEditorInput, IUntypedEditorInput } from 'vs/workbench/common/editor'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { PreferredGroup } from 'vs/workbench/services/editor/common/editorService'; -import { AtLeastOne } from 'vs/base/common/types'; export const IEditorResolverService = createDecorator<IEditorResolverService>('editorResolverService'); @@ -110,15 +109,13 @@ export type DiffEditorInputFactoryFunction = (diffEditorInput: IResourceDiffEdit export type MergeEditorInputFactoryFunction = (mergeEditorInput: IResourceMergeEditorInput, group: IEditorGroup) => EditorInputFactoryResult; -type EditorInputFactories = { - createEditorInput?: EditorInputFactoryFunction; +export type EditorInputFactoryObject = { + createEditorInput: EditorInputFactoryFunction; createUntitledEditorInput?: UntitledEditorInputFactoryFunction; createDiffEditorInput?: DiffEditorInputFactoryFunction; createMergeEditorInput?: MergeEditorInputFactoryFunction; }; -export type EditorInputFactoryObject = AtLeastOne<EditorInputFactories>; - export interface IEditorResolverService { readonly _serviceBrand: undefined; /** @@ -141,8 +138,7 @@ export interface IEditorResolverService { readonly onDidChangeEditorRegistrations: Event<void>; /** - * Registers a specific editor. Editors with the same glob pattern and ID will be grouped together by the resolver. - * This allows for registration of the factories in different locations + * Registers a specific editor. * @param globPattern The glob pattern for this registration * @param editorInfo Information about the registration * @param options Specific options which apply to this registration diff --git a/src/vs/workbench/services/editor/test/browser/editorResolverService.test.ts b/src/vs/workbench/services/editor/test/browser/editorResolverService.test.ts index 2b3a585ed5c..5bfe40ad314 100644 --- a/src/vs/workbench/services/editor/test/browser/editorResolverService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorResolverService.test.ts @@ -382,67 +382,4 @@ suite('EditorResolverService', () => { assert.strictEqual(service.getEditors().length, editors.length); assert.strictEqual(service.getEditors().some(editor => editor.id === 'TEST_EDITOR'), false); }); - - test('Multiple registrations to same glob and id #155859', async () => { - const [part, service, accessor] = await createEditorResolverService(); - const testEditorInfo = { - id: 'TEST_EDITOR', - label: 'Test Editor Label', - detail: 'Test Editor Details', - priority: RegisteredEditorPriority.default - }; - const registeredSingleEditor = service.registerEditor('*.test', - testEditorInfo, - {}, - { - createEditorInput: ({ resource, options }, group) => ({ editor: new TestFileEditorInput(URI.parse(resource.toString()), TEST_EDITOR_INPUT_ID) }) - } - ); - - const registeredDiffEditor = service.registerEditor('*.test', - testEditorInfo, - {}, - { - createDiffEditorInput: ({ modified, original, options }, group) => ({ - editor: accessor.instantiationService.createInstance( - DiffEditorInput, - 'name', - 'description', - new TestFileEditorInput(URI.parse(original.toString()), TEST_EDITOR_INPUT_ID), - new TestFileEditorInput(URI.parse(modified.toString()), TEST_EDITOR_INPUT_ID), - undefined) - }) - } - ); - - // Resolve a diff - let resultingResolution = await service.resolveEditor({ - original: { resource: URI.file('my://resource-basics.test') }, - modified: { resource: URI.file('my://resource-basics.test') } - }, part.activeGroup); - assert.ok(resultingResolution); - assert.notStrictEqual(typeof resultingResolution, 'number'); - if (resultingResolution !== ResolvedStatus.ABORT && resultingResolution !== ResolvedStatus.NONE) { - assert.strictEqual(resultingResolution.editor.typeId, 'workbench.editors.diffEditorInput'); - resultingResolution.editor.dispose(); - } else { - assert.fail(); - } - - // Remove diff registration - registeredDiffEditor.dispose(); - - // Resolve a diff again, expected failure - resultingResolution = await service.resolveEditor({ - original: { resource: URI.file('my://resource-basics.test') }, - modified: { resource: URI.file('my://resource-basics.test') } - }, part.activeGroup); - assert.ok(resultingResolution); - assert.strictEqual(typeof resultingResolution, 'number'); - if (resultingResolution !== ResolvedStatus.NONE) { - assert.fail(); - } - - registeredSingleEditor.dispose(); - }); }); diff --git a/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts b/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts index bc5e140d42a..61275010652 100644 --- a/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts @@ -331,7 +331,7 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten const missingExtensions = extensions.filter(({ id }) => !galleryExtensionsMap.has(id.toLowerCase())); if (missingExtensions.length) { - this.logService.info('Skipping the additional builtin extensions because their compatible versions are not foud.', missingExtensions); + this.logService.info('Skipping the additional builtin extensions because their compatible versions are not found.', missingExtensions); } const webExtensions: IWebExtension[] = []; diff --git a/src/vs/workbench/services/host/browser/browserHostService.ts b/src/vs/workbench/services/host/browser/browserHostService.ts index 25fca6a0136..0aebd31d5b9 100644 --- a/src/vs/workbench/services/host/browser/browserHostService.ts +++ b/src/vs/workbench/services/host/browser/browserHostService.ts @@ -268,7 +268,7 @@ export class BrowserHostService extends Disposable implements IHostService { input2: { resource: editors[1].resource }, base: { resource: editors[2].resource }, result: { resource: editors[3].resource }, - options: { pinned: true } + options: { pinned: true, override: 'mergeEditor.Input' } // TODO@bpasero remove the override once the resolver is ready }); } diff --git a/src/vs/workbench/services/keybinding/browser/keybindingService.ts b/src/vs/workbench/services/keybinding/browser/keybindingService.ts index fe4650f771f..f75556ad8a3 100644 --- a/src/vs/workbench/services/keybinding/browser/keybindingService.ts +++ b/src/vs/workbench/services/keybinding/browser/keybindingService.ts @@ -743,7 +743,9 @@ class UserKeybindings extends Disposable { private async whenCurrentProfieChanged(e: DidChangeUserDataProfileEvent): Promise<void> { if (e.preserveData) { - await this.fileService.copy(e.previous.keybindingsResource, e.profile.keybindingsResource); + if (await this.fileService.exists(e.previous.keybindingsResource)) { + await this.fileService.copy(e.previous.keybindingsResource, e.profile.keybindingsResource); + } } this.watch(); this.reloadConfigurationScheduler.schedule(); diff --git a/src/vs/workbench/services/label/common/labelService.ts b/src/vs/workbench/services/label/common/labelService.ts index b680533dabc..4135a251e94 100644 --- a/src/vs/workbench/services/label/common/labelService.ts +++ b/src/vs/workbench/services/label/common/labelService.ts @@ -402,33 +402,32 @@ export class LabelService extends Disposable implements ILabelService { } private formatUri(resource: URI, formatting: ResourceLabelFormatting, forceNoTildify?: boolean): string { - let label = typeof formatting.label !== 'string' ? formatting.label(resource) : - formatting.label.replace(labelMatchingRegexp, (match, token, qsToken, qsValue) => { - switch (token) { - case 'scheme': return resource.scheme; - case 'authority': return resource.authority; - case 'authoritySuffix': { - const i = resource.authority.indexOf('+'); - return i === -1 ? resource.authority : resource.authority.slice(i + 1); - } - case 'path': - return formatting.stripPathStartingSeparator - ? resource.path.slice(resource.path[0] === formatting.separator ? 1 : 0) - : resource.path; - default: { - if (qsToken === 'query') { - const { query } = resource; - if (query && query[0] === '{' && query[query.length - 1] === '}') { - try { - return JSON.parse(query)[qsValue] || ''; - } catch { } - } + let label = formatting.label.replace(labelMatchingRegexp, (match, token, qsToken, qsValue) => { + switch (token) { + case 'scheme': return resource.scheme; + case 'authority': return resource.authority; + case 'authoritySuffix': { + const i = resource.authority.indexOf('+'); + return i === -1 ? resource.authority : resource.authority.slice(i + 1); + } + case 'path': + return formatting.stripPathStartingSeparator + ? resource.path.slice(resource.path[0] === formatting.separator ? 1 : 0) + : resource.path; + default: { + if (qsToken === 'query') { + const { query } = resource; + if (query && query[0] === '{' && query[query.length - 1] === '}') { + try { + return JSON.parse(query)[qsValue] || ''; + } catch { } } - - return ''; } + + return ''; } - }); + } + }); // convert \c:\something => C:\something if (formatting.normalizeDriveLetter && hasDriveLetterIgnorePlatform(label)) { diff --git a/src/vs/workbench/services/label/test/browser/label.test.ts b/src/vs/workbench/services/label/test/browser/label.test.ts index 338660d225b..745055c930e 100644 --- a/src/vs/workbench/services/label/test/browser/label.test.ts +++ b/src/vs/workbench/services/label/test/browser/label.test.ts @@ -164,18 +164,6 @@ suite('URI Label', () => { assert.strictEqual(labelService.getUriLabel(uri1, { relative: false }), 'LABEL: /END'); }); - test('custom formatting function', function () { - labelService.registerFormatter({ - scheme: 'vscode', - formatting: { - label: (resource) => { return resource.toString(); }, - separator: '/', - } - }); - - const uri1 = URI.parse('vscode://microsoft.com/1/2/3/4/5'); - assert.strictEqual(labelService.getUriLabel(uri1), uri1.toString()); - }); test('label caching', () => { const m = new Memento('cachedResourceLabelFormatters', storageService).getMemento(StorageScope.PROFILE, StorageTarget.MACHINE); diff --git a/src/vs/workbench/services/telemetry/browser/telemetryService.ts b/src/vs/workbench/services/telemetry/browser/telemetryService.ts index 3ad179570b6..f181154fe05 100644 --- a/src/vs/workbench/services/telemetry/browser/telemetryService.ts +++ b/src/vs/workbench/services/telemetry/browser/telemetryService.ts @@ -15,7 +15,7 @@ import { ClassifiedEvent, GDPRClassification, StrictPropertyCheck } from 'vs/pla import { ITelemetryData, ITelemetryInfo, ITelemetryService, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; import { TelemetryLogAppender } from 'vs/platform/telemetry/common/telemetryLogAppender'; import { ITelemetryServiceConfig, TelemetryService as BaseTelemetryService } from 'vs/platform/telemetry/common/telemetryService'; -import { ITelemetryAppender, NullTelemetryService, supportsTelemetry } from 'vs/platform/telemetry/common/telemetryUtils'; +import { isInternalTelemetry, ITelemetryAppender, NullTelemetryService, supportsTelemetry } from 'vs/platform/telemetry/common/telemetryUtils'; import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { resolveWorkbenchCommonProperties } from 'vs/workbench/services/telemetry/browser/workbenchCommonProperties'; @@ -40,12 +40,13 @@ export class TelemetryService extends Disposable implements ITelemetryService { if (supportsTelemetry(productService, environmentService) && productService.aiConfig?.ariaKey) { // If remote server is present send telemetry through that, else use the client side appender const appenders = []; - const telemetryProvider: ITelemetryAppender = remoteAgentService.getConnection() !== null ? { log: remoteAgentService.logTelemetry.bind(remoteAgentService), flush: remoteAgentService.flushTelemetry.bind(remoteAgentService) } : new OneDataSystemWebAppender(configurationService, 'monacoworkbench', null, productService.aiConfig?.ariaKey); + const isInternal = isInternalTelemetry(productService, configurationService); + const telemetryProvider: ITelemetryAppender = remoteAgentService.getConnection() !== null ? { log: remoteAgentService.logTelemetry.bind(remoteAgentService), flush: remoteAgentService.flushTelemetry.bind(remoteAgentService) } : new OneDataSystemWebAppender(isInternal, 'monacoworkbench', null, productService.aiConfig?.ariaKey); appenders.push(telemetryProvider); appenders.push(new TelemetryLogAppender(loggerService, environmentService)); const config: ITelemetryServiceConfig = { appenders, - commonProperties: resolveWorkbenchCommonProperties(storageService, productService.commit, productService.version, environmentService.remoteAuthority, productService.embedderIdentifier, productService.removeTelemetryMachineId, environmentService.options && environmentService.options.resolveCommonTelemetryProperties), + commonProperties: resolveWorkbenchCommonProperties(storageService, productService.commit, productService.version, isInternal, environmentService.remoteAuthority, productService.embedderIdentifier, productService.removeTelemetryMachineId, environmentService.options && environmentService.options.resolveCommonTelemetryProperties), sendErrorTelemetry: this.sendErrorTelemetry, }; diff --git a/src/vs/workbench/services/telemetry/browser/workbenchCommonProperties.ts b/src/vs/workbench/services/telemetry/browser/workbenchCommonProperties.ts index 16bce794a23..81c10410b03 100644 --- a/src/vs/workbench/services/telemetry/browser/workbenchCommonProperties.ts +++ b/src/vs/workbench/services/telemetry/browser/workbenchCommonProperties.ts @@ -24,12 +24,13 @@ export async function resolveWorkbenchCommonProperties( storageService: IStorageService, commit: string | undefined, version: string | undefined, + isInternalTelemetry: boolean, remoteAuthority?: string, productIdentifier?: string, removeMachineId?: boolean, resolveAdditionalProperties?: () => { [key: string]: any } -): Promise<{ [name: string]: string | undefined }> { - const result: { [name: string]: string | undefined } = Object.create(null); +): Promise<{ [name: string]: string | boolean | undefined }> { + const result: { [name: string]: string | boolean | undefined } = Object.create(null); const firstSessionDate = storageService.get(firstSessionDateStorageKey, StorageScope.APPLICATION)!; const lastSessionDate = storageService.get(lastSessionDateStorageKey, StorageScope.APPLICATION)!; @@ -75,6 +76,11 @@ export async function resolveWorkbenchCommonProperties( // __GDPR__COMMON__ "common.isTouchDevice" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } result['common.isTouchDevice'] = String(Gesture.isTouchDevice()); + if (isInternalTelemetry) { + // __GDPR__COMMON__ "common.msftInternal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } + result['common.msftInternal'] = isInternalTelemetry; + } + // dynamic properties which value differs on each call let seq = 0; const startTime = Date.now(); diff --git a/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts b/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts index a91c4a23be9..0645973a7bb 100644 --- a/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts +++ b/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { ITelemetryService, ITelemetryInfo, ITelemetryData, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; -import { supportsTelemetry, NullTelemetryService, getPiiPathsFromEnvironment } from 'vs/platform/telemetry/common/telemetryUtils'; +import { supportsTelemetry, NullTelemetryService, getPiiPathsFromEnvironment, isInternalTelemetry } from 'vs/platform/telemetry/common/telemetryUtils'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { Disposable } from 'vs/base/common/lifecycle'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; @@ -37,10 +37,11 @@ export class TelemetryService extends Disposable implements ITelemetryService { super(); if (supportsTelemetry(productService, environmentService)) { + const isInternal = isInternalTelemetry(productService, configurationService); const channel = sharedProcessService.getChannel('telemetryAppender'); const config: ITelemetryServiceConfig = { appenders: [new TelemetryAppenderClient(channel)], - commonProperties: resolveWorkbenchCommonProperties(storageService, fileService, environmentService.os.release, environmentService.os.hostname, productService.commit, productService.version, environmentService.machineId, productService.msftInternalDomains, environmentService.installSourcePath, environmentService.remoteAuthority), + commonProperties: resolveWorkbenchCommonProperties(storageService, fileService, environmentService.os.release, environmentService.os.hostname, productService.commit, productService.version, environmentService.machineId, isInternal, environmentService.installSourcePath, environmentService.remoteAuthority), piiPaths: getPiiPathsFromEnvironment(environmentService), sendErrorTelemetry: true }; diff --git a/src/vs/workbench/services/telemetry/electron-sandbox/workbenchCommonProperties.ts b/src/vs/workbench/services/telemetry/electron-sandbox/workbenchCommonProperties.ts index 9d5b4282308..83f9b2c2a7b 100644 --- a/src/vs/workbench/services/telemetry/electron-sandbox/workbenchCommonProperties.ts +++ b/src/vs/workbench/services/telemetry/electron-sandbox/workbenchCommonProperties.ts @@ -18,11 +18,11 @@ export async function resolveWorkbenchCommonProperties( commit: string | undefined, version: string | undefined, machineId: string, - msftInternalDomains: string[] | undefined, + isInternalTelemetry: boolean, installSourcePath: string, remoteAuthority?: string ): Promise<{ [name: string]: string | boolean | undefined }> { - const result = await resolveCommonProperties(fileService, release, hostname, process.arch, commit, version, machineId, msftInternalDomains, installSourcePath); + const result = await resolveCommonProperties(fileService, release, hostname, process.arch, commit, version, machineId, isInternalTelemetry, installSourcePath); const firstSessionDate = storageService.get(firstSessionDateStorageKey, StorageScope.APPLICATION)!; const lastSessionDate = storageService.get(lastSessionDateStorageKey, StorageScope.APPLICATION)!; diff --git a/src/vs/workbench/services/telemetry/test/browser/commonProperties.test.ts b/src/vs/workbench/services/telemetry/test/browser/commonProperties.test.ts index c3f2b77b696..42640f42bc7 100644 --- a/src/vs/workbench/services/telemetry/test/browser/commonProperties.test.ts +++ b/src/vs/workbench/services/telemetry/test/browser/commonProperties.test.ts @@ -23,7 +23,7 @@ suite('Browser Telemetry - common properties', function () { }; }; - const props = await resolveWorkbenchCommonProperties(testStorageService, commit, version, undefined, undefined, false, resolveCommonTelemetryProperties); + const props = await resolveWorkbenchCommonProperties(testStorageService, commit, version, false, undefined, undefined, false, resolveCommonTelemetryProperties); assert.ok('commitHash' in props); assert.ok('sessionID' in props); @@ -53,10 +53,10 @@ suite('Browser Telemetry - common properties', function () { }); }; - const props = await resolveWorkbenchCommonProperties(testStorageService, commit, version, undefined, undefined, false, resolveCommonTelemetryProperties); + const props = await resolveWorkbenchCommonProperties(testStorageService, commit, version, false, undefined, undefined, false, resolveCommonTelemetryProperties); assert.strictEqual(props['userId'], 1); - const props2 = await resolveWorkbenchCommonProperties(testStorageService, commit, version, undefined, undefined, false, resolveCommonTelemetryProperties); + const props2 = await resolveWorkbenchCommonProperties(testStorageService, commit, version, false, undefined, undefined, false, resolveCommonTelemetryProperties); assert.strictEqual(props2['userId'], 2); }); }); diff --git a/src/vs/workbench/services/telemetry/test/electron-browser/commonProperties.test.ts b/src/vs/workbench/services/telemetry/test/electron-browser/commonProperties.test.ts index 8ae51addd3e..a4813d2485a 100644 --- a/src/vs/workbench/services/telemetry/test/electron-browser/commonProperties.test.ts +++ b/src/vs/workbench/services/telemetry/test/electron-browser/commonProperties.test.ts @@ -46,7 +46,7 @@ suite('Telemetry - common properties', function () { test('default', async function () { await Promises.mkdir(parentDir, { recursive: true }); fs.writeFileSync(installSource, 'my.install.source'); - const props = await resolveWorkbenchCommonProperties(testStorageService, testFileService, release(), hostname(), commit, version, 'someMachineId', undefined, installSource); + const props = await resolveWorkbenchCommonProperties(testStorageService, testFileService, release(), hostname(), commit, version, 'someMachineId', false, installSource); assert.ok('commitHash' in props); assert.ok('sessionID' in props); assert.ok('timestamp' in props); @@ -66,7 +66,7 @@ suite('Telemetry - common properties', function () { // machine id et al assert.ok('common.machineId' in props, 'machineId'); fs.unlinkSync(installSource); - const props_1 = await resolveWorkbenchCommonProperties(testStorageService, testFileService, release(), hostname(), commit, version, 'someMachineId', undefined, installSource); + const props_1 = await resolveWorkbenchCommonProperties(testStorageService, testFileService, release(), hostname(), commit, version, 'someMachineId', false, installSource); assert.ok(!('common.source' in props_1)); }); @@ -74,14 +74,14 @@ suite('Telemetry - common properties', function () { testStorageService.store('telemetry.lastSessionDate', new Date().toUTCString(), StorageScope.APPLICATION, StorageTarget.MACHINE); - const props = await resolveWorkbenchCommonProperties(testStorageService, testFileService, release(), hostname(), commit, version, 'someMachineId', undefined, installSource); + const props = await resolveWorkbenchCommonProperties(testStorageService, testFileService, release(), hostname(), commit, version, 'someMachineId', false, installSource); assert.ok('common.lastSessionDate' in props); // conditional, see below assert.ok('common.isNewSession' in props); assert.strictEqual(props['common.isNewSession'], '0'); }); test('values chance on ask', async function () { - const props = await resolveWorkbenchCommonProperties(testStorageService, testFileService, release(), hostname(), commit, version, 'someMachineId', undefined, installSource); + const props = await resolveWorkbenchCommonProperties(testStorageService, testFileService, release(), hostname(), commit, version, 'someMachineId', false, installSource); let value1 = props['common.sequence']; let value2 = props['common.sequence']; assert.ok(value1 !== value2, 'seq'); diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 4800d7148ee..b0c26eacd7f 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -347,4 +347,7 @@ import 'vs/workbench/contrib/audioCues/browser/audioCues.contribution'; // Deprecated Extension Migrator import 'vs/workbench/contrib/deprecatedExtensionMigrator/browser/deprecatedExtensionMigrator.contribution'; +// Bracket Pair Colorizer 2 Telemetry +import 'vs/workbench/contrib/bracketPairColorizer2Telemetry/browser/bracketPairColorizer2Telemetry.contribution'; + //#endregion diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index a71d4de98e3..3176a04cdb2 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -176,7 +176,7 @@ import 'vs/workbench/contrib/offline/browser/offline.contribution'; // // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -import { create, commands, env, window, logger } from 'vs/workbench/browser/web.factory'; +import { create, commands, env, window, workspace, logger } from 'vs/workbench/browser/web.factory'; import { IWorkbench, ICommand, ICommonTelemetryPropertiesResolver, IDefaultEditor, IDefaultLayout, IDefaultView, IDevelopmentOptions, IExternalUriResolver, IExternalURLOpener, IHomeIndicator, IInitialColorTheme, IPosition, IProductQualityChangeHandler, IRange, IResourceUriProvider, ISettingsSyncOptions, IShowPortCandidate, ITunnel, ITunnelFactory, ITunnelOptions, ITunnelProvider, IWelcomeBanner, IWelcomeBannerAction, IWindowIndicator, IWorkbenchConstructionOptions, Menu } from 'vs/workbench/browser/web.api'; import { UriComponents, URI } from 'vs/base/common/uri'; import { IWebSocketFactory, IWebSocket } from 'vs/platform/remote/browser/browserSocketFactory'; @@ -281,6 +281,9 @@ export { // Env env, + // Workspace + workspace, + // Development IDevelopmentOptions }; |